经过之前两篇笔记的实战操作,已经掌握了Linux字符设备驱动开发的基本步骤,字符设备驱动开发重点是使用register_chrdev函数注册字符设备,当不再使用设备的时候就使用unregister_chrdev函数注销字符设备,驱动模块加载成功以后还需要手动使用mknod命令创建设备节点 。register_chrdev和unregister_chrdev这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动 API函数。本节就学习一下如何编写新字符设备驱动,并且在驱动模块加载的时候自动创建设备节点文件。
新字符设备驱动原理
使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来问题:需要实现确定主设备号的使用情况,且会将该主设备号下的所有此设备号都使用掉。
解决这两个问题最好的方法就是在使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。如果没有指定设备号就可以这样申请:
|---------------------------------------------------------------------------------------------|
| int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) |
如果给定了主设备号和次设备号可以这样申请:
|---------------------------------------------------------------------------|
| int register_chrdev_region(dev_t from, unsigned count, const char *name) |
from就是申请的其实设备号,即给定设备号;count就是申请数量,name即设备名。
注销字符设备后要释放设备号,可统一使用如下函数:
|-----------------------------------------------------------|
| void unregister_chrdev_region(dev_t from, unsigned count) |
可以如此来分配设备号:
c
1 int major; /* 主设备号 */
2 int minor; /* 次设备号 */
3 dev_t devid; /* 设备号 */
4
5 if (major) { /* 定义了主设备号 */
6 devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择0 */
7 register_chrdev_region(devid, 1, "test");
8 } else { /* 没有定义设备号 */
9 alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
10 major = MAJOR(devid); /* 获取分配号的主设备号 */
11 minor = MINOR(devid); /* 获取分配号的次设备号 */
12 }
13
14 unregister_chrdev_region(devid, 1); /* 注销设备号 */
新的字符设备注册方法
字符设备结构
可以在Linux中使用cdev结构体表示字符设备,定义在include/linux/cdev.h中:
c
1 struct cdev {
2 struct kobject kobj;
3 struct module *owner;
4 const struct file_operations *ops;
5 struct list_head list;
6 dev_t dev;
7 unsigned int count;
8 } __randomize_layout
这其中,重要的是ops和dev,即字符设备文件操作函数集合file_operations以及设备号dev_t。使用如下:
c
struct cdev test_cdev
cdev_init函数
定义好cdev变量之后要调用cdev_init函数来初始化:
c
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
cdev就是要初始化的cdev结构体变量,fops就是字符设备文件操作函数集合。
cdev_add函数
用于像Linux系统添加字符设备。首先cdev初始化,之后使用cdev_add来向Linux系统添加,原型如下:
c
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
p指向要添加的字符设备(cdev结构体变量),dev就是设备号,count是设备数量。
cdev_del函数
卸载驱动就是调用这个函数从Linux内核删除相应字符设备,原型如下:
c
void cdev_del(struct cdev *p)
p就是要删除的设备。
cdev_del和unregister_chedev_region合起来的功能相当于unregister_chedev函数。
自动创建设备节点
在前面的Linux驱动实验中,使用modprobe加载驱动程序以后还需要使用命令"mknod"手动创建设备节点。本节讲解一下如何实现自动创建设备节点 ,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件。
mdev机制
udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。在使用buildroot构建根文件系统的时候选择了udev的简化版本mdev,所以在嵌入式Linux中用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理,如果使用busybox构建根文件系统,会在/etc/init.d/rcS文件中如下语句:
|---------------------------------------------|
| echo /sbin/mdev > /proc/sys/kernel/hotplug |
上述命令设置热插拔事件由mdev来管理。 buildroot构建的根文件系统已经全部处理好mdev了,不需要在修改什么文件。
创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个 class类,class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义。最后将宏class_create展开后如下:
|------------------------------------------------------------------------|
| struct class *class_create (struct module *owner, const char *name) |
owner一般为THIS_MODULE,name是类名字,返回值是指向结构体class的指针。
卸载驱动程序也需要删除类,函数为class_destroy,原型如下:
c
void class_destroy(struct class *cls);
cls就是要删除的类。
创建设备
创建好类之后,还需要在这个类下创建一个设备,使用函数device_create,原型如下:
c
struct device *device_create(struct class *cls,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create是个可变参数函数,cls就是设备要创建哪个类下面;parent是父设备,一般为NULL,也就是没有父设备;devt是设备号;drvdata是设备可能会使用的一些数据,一般为NULL;fmt是设备名字,如果设置fmt=xxx的话,就会生成 /dev/xxx这个设备文件,返回值就是创建好的设备。
同样,卸载驱动的时候需要删除创建的设备,函数为device_destroy,原型如下:
c
void device_destroy(struct class *cls, dev_t devt)
class是要删除设备所处类,devt是删除的设备号。
参考示例
c
struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 创建类 */
class = class_create(THIS_MODULE, "xxx");
/* 创建设备 */
device = device_create(class, NULL, devid, NULL, "xxx");
return 0; 1
}
/* 驱动出口函数 */
static void __exit led_exit(void)
{
/* 删除设备 */
device_destroy(newchrled.class, newchrled.devid);
/* 删除类 */
class_destroy(newchrled.class);
}
module_init(led_init);
module_exit(led_exit);
设备私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,但最好的做法试讲所有属性信息做成结构体。编写驱动open的时候将设备结构体作为私有数据添加到设备文件中。举例如下:
c
/* 设备结构体 */
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct test_dev testdev;
/* open函数 */
static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; /* 设置私有数据 */
return 0;
}
设置好后,在write、read、close函数中直接读取private_data即可获得设备结构体。
实验程序编写
与之前LED的实验相比,重点就是使用了新的字符设备驱动,设置了文件四有数据,添加了自动创建设备节点相关内容。
LED灯驱动程序编写
区别点在于,申请好__iomem*的映射后虚拟地址指针,就申请一个newchrled_dev的设备结构体newchrdev;然后在led_open中设置私有数据private_data指向newchrdev;最后在led_init中申请设备号、添加字符设备、创建类和设备,并在led_exit中注销字符新设备、删除类和设备。
编写测试APP
直接使用LED实验的APP即可。
运行测试
编译驱动程序和测试APP
把Makefile中obj-m的值改为newchrled.o即可,"make"之后就会有"newchrled.ko"驱动模块文件。
ledAPP.c则通过下述命令编译:
|-------------------------------------------------|
| arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp |
运行测试
将两个程序拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板并进入到目录lib/modules/5.4.31中,输入如下命令加载newchrled.ko:
|------------------------------------------------------|
| depmod //第一次加载驱动的时候需要运行此命令 modprobe newchrled //加载驱动 |
加载成功后会输出申请到的主设备号和次设备号,如下图所示:
驱动加载成功后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看:
|----------------------|
| ls /dev/newchrled -l |
驱动节点创建成功后就可以使用ledApp软件来测试,测试命令是一样的:
|-------------------------------------------------------------------------|
| ./ledApp /dev/newchrled 1 //打开 LED灯 ./ledApp /dev/newchrled 0 //关闭 LED灯 |
卸载也是一样:
|-----------------|
| rmmod newchrled |
总结
本篇还是在LED驱动的基础上,完成了自动创建驱动节点的代码编写,在led_open将私有数据private_data指向事先声明的结构体来管理设备文件;驱动open函数中添加类和设备。如此在加载好驱动之后,可以直接通过测试APP来测试,不需要自己设定驱动节点了。