字符设备驱动框架(字符设备基础一)

  编写字符设备驱动框架:首先驱动向 Linux 内核进行设备号申请,之后的字符设备注册时,会对申请的设备号进行使用。而 Linux 内核会将字符设备抽象成一个具体的struct cdev结构体,该结构体记录了字符设备的字符设备号、内核对象等信息,cdev_init(…)函数对结构体进行初始化之后,cdev_add(…)函数将设备号和 cdev 结构体进行链接,这时设备号才真正指向了内核中注册的设备。设备注册成功之后,此时还不能对字符设备进行文件操作,所以需要设备节节点来充当内核和用户层通信的桥梁。

一、申请字符设备号

  字符设备是指在I/O 传输过程中以字符为单位进行传输的设备,可以使用与普通文件相同的文件操作命令(打开、关闭、读、写等)对字符设备进行操作,是 Linux 驱动中最基本的一类设备驱动,例如最常见的LED、按键、IIC、SPI,LCD 等都属于字符设备的范畴。要想对字符设备进行操作,需要通过设备号来对相应的设备进行查找

1.1、设备号申请

  在 Linux 系统中每一个设备都有相应的设备号,通过该设备号查找对应的设备,从而进行之后的文件操作。设备号有主设备号与次设备号之分,主设备号用来表示一个特定的驱动,次设备号用来管理下面的设备。
  在 Linux 驱动中可以使用以下两种方法进行设备号的申请:
  1.通过 register_chrdev_region(dev_t from, unsigned count, const char *name)函数进行静态申请设备号。
  2. 通 过 alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char*name)函数进行动态申请设备号
  两个函数在“内核源码/include/linux/fs.h”文件中引用(在编写驱动程序的时候要加入该文件的引用)

1.1.1、静态申请设备号:

  函数原型:

register_chrdev_region(dev_t from, unsigned count, const char *name)

  函数作用:静态申请设备号,对指定好的设备号进行申请。
  参数含义:
    from: 自定义的 dev_t 类型设备号
    count: 申请设备的数量
    name: 申请的设备名称
  函数返回值:申请成功返回 0,申请失败返回负数

1.1.2、动态申请设备号:

  函数原型:

alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)

  函数作用:动态申请设备号,内核会自动分配一个未使用的设备号,相较于静态申请设备号,动态申请会避免注册设备号相同引发冲突的问题。
  参数含义:
    dev *: 会将申请完成的设备号保存在 dev 变量中
    baseminor: 次设备号可申请的最小值
    count: 申请设备的数量
    name: 申请的设备名称
  函数返回值:申请成功返回 0,申请失败返回负数

1.2、设备号类型

  申请的设备号类型为 dev_t ,在“内核源码/include/linux/types.h”文件中定义如下

typedef u32 __kernel_dev_t;
.... 
typedef __kernel_dev_t dev_t;

  dev_t 为 u32 类型,__u32 为 unsigned int 类型,所以 dev_t 是一个无符号的32 位整形类型。其中高12位表示主设备号,低 20 位表示次设备号。在“内核源码/include/linux/kdev_t.h”中提供了设备号相关的宏定义,如下

#define MINORBITS 20 /*次设备号位数*/
#define MINORMASK ((1U << MINORBITS) - 1) /*次设备号掩码*/
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))/*dev 右移 20 位得到主设备号*/
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) /*与次设备掩码与,得到次设备号*/
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))/*MKDEV 宏将主设备号(ma)左移20 位,然后与次设备号(mi)相与,得到设备号*/

  在静态申请设备号时需要将指定的主设备号和从设备号通过 MKDEV(ma,mi)宏进行设备号的转换,在动态申请设备号时可以用 MAJOR(dev) 和 MINOR(dev)宏将动态申请的设备号转化为主设备号和从设备号。

二、注册字符设备

  注册字符设备可以分为两个步骤:
  1.字符设备初始化
  2.字符设备的添加

2.1、字符设备初始化

  Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作 file_operations 结构体(设备的打开、读写、关闭等操作接口)等信息,struct cdev 结构体定义在“内核源码/include/linux/cdev.h”文件中(在编写驱动程序的时候要加入该文件的引用)

struct cdev {
	struct kobject kobj; //内嵌的内核对象. 
	struct module *owner; //该字符设备所在的内核模块的对象指针. 
	const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
	struct list_head list; //用来将已经向内核注册的所有字符设备形成链表. 
	dev_t dev; //字符设备的设备号,由主设备号和次设备号构成. 
	unsigned int count; //隶属于同一主设备号的次设备号的个数.
 };

  设备初始化所用到的函数为cdev_init(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用如下

void cdev_init(struct cdev *, const struct file_operations *);

  该函数的详细内容在“内核源码/include/fs/char_dev.c”文件中定义

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);//将整个结构体清零;
	INIT_LIST_HEAD(&cdev->list);//初始化 list 成员使其指向自身;
	kobject_init(&cdev->kobj, &ktype_cdev_default);//初始化 kobj 成员;
	cdev->ops = fops;//初始化 ops 成员,建立 cdev 和 file_operations 之间的连接
}

  函数作用:
    初始化传入的 cdev 类型的结构体,并与自定义的file_operations * 类型的结构体进行链接。
  参数含义:
    cdev: 要传入的 cdev 类型结构体,为要初始化的字符设备。
    fops:要传入的 file_operations * 类型结构体,
  函数返回值:无返回值。

2.2、字符设备的注册

2.2.1、字符设备的注册

  字符设备添加所用到的函数为 cdev_add(),该函数在“内核源码/include/linux/cdev.h”文件中所引用
  函数原型:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

  函数作用:该函数向内核注册一个 struct cdev 结构体
  参数含义:
    (1)第一个参数为要添加的 struct cdev 类型的结构体
    (2)第二个参数为申请的字符设备号
    (3)第三个参数为和该设备关联的设备编号的数量。
  这两个参数直接赋值给 struct cdev 的 dev 成员和 count 成员。
  函数返回值:添加成功返回 0,添加失败返回负数。

2.2.2、字符设备的注销

  字符设备删除所用到的函数为 cdev_del(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用
  函数原型:

void cdev_del(struct cdev *p)

  函数作用:该函数会向内核删除一个 struct cdev 类型结构体
  参数含义:该函数只有一个参数,为要删除的 struct cdev 类型的结构体
  函数返回值:无返回值

三、创建设备节点

  在 Linux 操作系统中一切皆文件,设备访问也是通过文件的方式来进行的,对于用来进行设备访问的文件称之为设备节点,设备节点被创建在/dev 目录下,将内核中注册的设备与用户层进行链接,这样应用程序才能对设备进行访问。
  根据设备节点的创建方式不同,分为了手动创建设备节点和自动创建设备节点

3.1、手动创建设备节点

  使用 mknod 命令手动创建设备节点,mknod 命令格式为:

mknod NAME TYPE MAJOR MINOR

  参数含义:
    NAME: 要创建的节点名称
    TYPE: b 表示块设备,c 表示字符设备,p 表示管道
    MAJOR:要链接设备的主设备号
    MINOR: 要链接设备的从设备号
  例如使用以下命令创建一个名为 device_test 的字符设备节点,链接设备的主设备号和从设备号分别为 236 和 0

mknod /dev/device_test c 236 0

3.2、自动创建设备节点

  设备文件的自动创建是利用 udev(mdev)机制来实现,多数情况下采用自动创建设备节点的方式。udev(mdev)可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。在驱动中首先使用 class_create(…)函数对class 进行创建,这个类存放于/sys/class/ 目录下,之后使用 device_create(…)函数创建相应的设备,在进行模块加载时,用户空间中的 udev 会自动响应 device_create()函数,寻找对应的类从而创建设备节点。

3.2.1、class_create(…)函数

  该函数在“内核源码/include/linux/device.h”文件中所引用

#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \ 
__class_create(owner, name, &__key); \
})

  函数作用:用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux 内核系统。
  参数含义:
    owner:struct module 结构体类型的指针,指向函数即将创建的这个struct class 的模块。一般赋值为 THIS_MODULE。
    name:char 类型的指针,代表即将创建的 struct class 变量的名字。
  返回值:struct class * 类型的结构体。

3.2.2、class_destroy(…)函数

  该函数在“内核源码/include/linux/device.h”文件中所引用

extern void class_destroy(struct class *cls);

  函数作用:用于删除设备的逻辑类,即从 Linux 内核系统中删除设备的逻辑类。
  参数含义:
  owner:struct module 结构体类型的指针,指向函数即将创建的这个struct class 的模块。一般赋值为 THIS_MODULE。
  name:char 类型的指针,代表即将创建的 struct class 变量的名字。
  返回值:无

3.2.3、device_create(…)函数

  该函数在“内核源码/include/linux/device.h”文件中所引用

struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);

  函数作用:用来在 class 类中下创建一个设备属性文件,udev 会自动识别从而进行设备节点的创建。
  参数含义:
    cls:指定所要创建的设备所从属的类。
    parent:指定该设备的父设备,如果没有就指定为 NULL。
    devt:指定创建设备的设备号。
    drvdata:被添加到该设备回调的数据,没有则指定为 NULL。
    fmt:添加到系统的设备节点名称。
  返回值:struct device * 类型结构体

3.2.4、device_destroy(…)函数

在“内核源码/include/linux/device.h”文件中所引用

extern void device_destroy(struct class *cls, dev_t devt);

  函数作用:用来删除 class 类中的设备属性文件,udev 会自动识别从而进行设备节点的删除。
  参数含义:
    cls:指定所要创建的设备所从属的类。
    devt:指定创建设备的设备号。
  返回值:无

你可能感兴趣的:(RK3568,linux驱动开发笔记(迅为),linux)