编写字符设备驱动框架:首先驱动向 Linux 内核进行设备号申请,之后的字符设备注册时,会对申请的设备号进行使用。而 Linux 内核会将字符设备抽象成一个具体的struct cdev结构体,该结构体记录了字符设备的字符设备号、内核对象等信息,cdev_init(…)函数对结构体进行初始化之后,cdev_add(…)函数将设备号和 cdev 结构体进行链接,这时设备号才真正指向了内核中注册的设备。设备注册成功之后,此时还不能对字符设备进行文件操作,所以需要设备节节点来充当内核和用户层通信的桥梁。
字符设备是指在I/O 传输过程中以字符为单位进行传输的设备,可以使用与普通文件相同的文件操作命令(打开、关闭、读、写等)对字符设备进行操作,是 Linux 驱动中最基本的一类设备驱动,例如最常见的LED、按键、IIC、SPI,LCD 等都属于字符设备的范畴。要想对字符设备进行操作,需要通过设备号来对相应的设备进行查找
在 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”文件中引用(在编写驱动程序的时候要加入该文件的引用)
函数原型:
register_chrdev_region(dev_t from, unsigned count, const char *name)
函数作用:静态申请设备号,对指定好的设备号进行申请。
参数含义:
from: 自定义的 dev_t 类型设备号
count: 申请设备的数量
name: 申请的设备名称
函数返回值:申请成功返回 0,申请失败返回负数
函数原型:
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
函数作用:动态申请设备号,内核会自动分配一个未使用的设备号,相较于静态申请设备号,动态申请会避免注册设备号相同引发冲突的问题。
参数含义:
dev *: 会将申请完成的设备号保存在 dev 变量中
baseminor: 次设备号可申请的最小值
count: 申请设备的数量
name: 申请的设备名称
函数返回值:申请成功返回 0,申请失败返回负数
申请的设备号类型为 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.字符设备的添加
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 * 类型结构体,
函数返回值:无返回值。
字符设备添加所用到的函数为 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,添加失败返回负数。
字符设备删除所用到的函数为 cdev_del(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用
函数原型:
void cdev_del(struct cdev *p)
函数作用:该函数会向内核删除一个 struct cdev 类型结构体
参数含义:该函数只有一个参数,为要删除的 struct cdev 类型的结构体
函数返回值:无返回值
在 Linux 操作系统中一切皆文件,设备访问也是通过文件的方式来进行的,对于用来进行设备访问的文件称之为设备节点,设备节点被创建在/dev 目录下,将内核中注册的设备与用户层进行链接,这样应用程序才能对设备进行访问。
根据设备节点的创建方式不同,分为了手动创建设备节点和自动创建设备节点
使用 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
设备文件的自动创建是利用 udev(mdev)机制来实现,多数情况下采用自动创建设备节点的方式。udev(mdev)可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。在驱动中首先使用 class_create(…)函数对class 进行创建,这个类存放于/sys/class/ 目录下,之后使用 device_create(…)函数创建相应的设备,在进行模块加载时,用户空间中的 udev 会自动响应 device_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 * 类型的结构体。
该函数在“内核源码/include/linux/device.h”文件中所引用
extern void class_destroy(struct class *cls);
函数作用:用于删除设备的逻辑类,即从 Linux 内核系统中删除设备的逻辑类。
参数含义:
owner:struct module 结构体类型的指针,指向函数即将创建的这个struct class 的模块。一般赋值为 THIS_MODULE。
name:char 类型的指针,代表即将创建的 struct class 变量的名字。
返回值:无
该函数在“内核源码/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 * 类型结构体
在“内核源码/include/linux/device.h”文件中所引用
extern void device_destroy(struct class *cls, dev_t devt);
函数作用:用来删除 class 类中的设备属性文件,udev 会自动识别从而进行设备节点的删除。
参数含义:
cls:指定所要创建的设备所从属的类。
devt:指定创建设备的设备号。
返回值:无