电子工程师技术服务社区
公告
登录
|
注册
首页
技术问答
厂商活动
正点原子
板卡试用
资源库
下载
文章
社区首页
文章
【i.MX6ULL】驱动开发2——新字符设备开发模板
分 享
扫描二维码分享
【i.MX6ULL】驱动开发2——新字符设备开发模板
i.MX6ULL
嵌入式
驱动
码农爱学习
关注
发布时间: 2021-09-03
丨
阅读: 1747
上篇文章介绍了字符设备的开发模板,但那是一种**旧版本**的驱动开发模式,设备驱动**需要手动分配设备号**再使用 register_chrdev进行注册,加载成功以后**还需要手动使用mknod命令创建设备节点,比较麻烦**。 目前Linux内核推荐的新字符设备驱动API函数,使得驱动的使用更加自动化,本篇就来一起研究下。 先看目录: [TOC] # 1 旧字符设备驱动的弊端 使用register_chrdev函数注册字符设备,需要指定一个设备号,这就造成: - 需要事先确定好哪些主设备号没有使用 - 会将一个主设备号下的所有次设备号都使用掉,比如主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被占用了 **回顾上一篇的操作**,先是加载驱动: ![](https://cf05.ickimg.com/bbsimages/202108/4fa336fff74ca180e6d9e5894e137539.png) 加载完,还有**手动使用mknod指令来手动创建该设备节点**,并且指定驱动程序中写死的设备号: ![](https://cf05.ickimg.com/bbsimages/202108/3d40350dc45dadec04acb63a1afce0d8.png) 本篇,就要使用一种**新的字符驱动编写方式,实现设备号的自动分配,省去mknod指令操作**。 # 2 新字符设备驱动原理 ## 2.1 分配和释放设备号 使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。 使用如下函数来**申请设备号**(该函数在上篇提到过): ```c /@@* * dev:保存申请到的设备号 * baseminor:次设备号起始地址,一般baseminor为0 (次设备号以baseminor为起始地址地址开始递) * count:要申请的设备号数量 * name:设备名字 */ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) ``` 如果给定了设备的主设备号和次设备号就使用如下所示函数来**注册设备号**即可: ```c /@@* * from:要申请的起始设备号 * count:要申请的设备号数量 * name:设备名字 */ int register_chrdev_region(dev_t from, unsigned count, const char *name) ``` 注销字符设备之后要**释放设备号**,不管是通过alloc_chrdev_region函数的动态分配还是register_chrdev_region函数手动指定的设备号,统一使用(和上篇使用的一样)的释放函数: ```c /@@* * from:要释放的设备号 * count:表示从from开始,要释放的设备号数量 */ void unregister_chrdev_region(dev_t from, unsigned count) ``` 新字符设备驱动下,**设备号分配示例代码**如下: ```c int major; /@@*主设备号*/ int minor; /@@*次设备号*/ dev_t devid; /@@*设备号*/ /@@*定义了主设备号*/ if (major) { devid = MKDEV(major, 0); /@@*大部分驱动次设备号都选择0*/ register_chrdev_region(devid, 1, "test"); } /@@*没有定义设备号*/ else { alloc_chrdev_region(&devid, 0, 1, "test"); /@@*申请设备号*/ major = MAJOR(devid); /@@*获取分配号的主设备号*/ minor = MINOR(devid); /@@*获取分配号的次设备号*/ } ``` ## 2.2 字符设备注册 ### 2.2.1 cdev字符设备结构 在Linux中使用**cdev结构体**表示一个字符设备,其定义在include/linux/cdev.h文件中: ```c struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; /@@*文件操作函数集合*/ struct list_head list; dev_t dev; /@@*设备号*/ unsigned int count; }; ``` ### 2.2.2 cdev_init 函数 定义好cdev变量以后就要使用cdev_init函数对其进行**初始化**: ```c /@@* * cdev:要初始化的cdev结构体变量 * fops:字符设备文件操作函数集合 */ void cdev_init(struct cdev *cdev, const struct file_operations *fops) ``` 该函数的使用示例如下: ```c /@@*要初始化的cdev结构体*/ struct cdev testcdev; /@@* 设备操作函数 */ static struct file_operations test_fops = { .owner = THIS_MODULE, /@@* 其他具体的初始项 */ }; testcdev.owner = THIS_MODULE; /@@* 初始化cdev*/ cdev_init(&testcdev, &test_fops); ``` ### 2.2.3 cdev_add函数 该函数用于向Linux系统**添加字符设备**,即cdev结构体变量: ```c /@@* * cdev:要初始化的cdev结构体变量 * dev:字符设备所使用的设备号 * count:要添加的设备数量 */ int cdev_add(struct cdev *p, dev_t dev, unsigned count) ``` ### 2.2.4 cdev_del函数 卸载驱动的时候要使用cdev_del函数从Linux内核中**删除字符设备**: ```c /@@* * p:要删除的字符设备 */ void cdev_del(struct cdev *p) ``` ## 2.3 自动创建设备节点 上篇的Linux驱动实验中,在使用modprobe加载驱动程序以后还需要使用“mknod”命令手动创建设备节点,比较麻烦,这里就来研究一下如何实现自动创建设备节点。 ### 2.3.1 mdev机制 在Linux下通过**udev来实现设备文件的自动创建与删除**。使用busybox构建根文件系统的时候,busybox会创建一个udev的**简化版本mdev**。 所以,在嵌入式开发中使用mdev来实现设备节点文件的自动创建与删除。Linux系统中的**热插拔事件**也由mdev 管理,在/etc/init.d/rcS 文件中如下语句: ```sh echo /sbin/mdev > /proc/sys/kernel/hotplug ``` ### 2.3.2 创建和删除类 自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添 加自动创建设备节点相关代码。 首先要**创建一个class类**,其实是个结构体,定义在include/linux/device.h里面。class_create是类创建函数(宏定义): ```c #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ }) struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key) ``` 卸载驱动程序的时候需要使用函数为class_destroy**删除掉类**: ```c /@@* * cls:要删除的类 */ void class_destroy(struct class *cls); ``` ### 2.3.3 创建设备 创建好类以后还不能实现自动创建设备节点,还需要**在这个类下创建一个设备**。使用device_create函数创建设备: ```c /@@* * class:设备要创建哪个类下面 * parent:父设备, 一般为 NULL * devt:设备号 * drvdata:设备可能会使用的一些数据,一般为 NULL * fmt:设备名字 */ struct device *device_create(struct clas *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) ``` 参数最后的`...`表示这在是一个**可变参数**的函数。 ## 2.4 设置文件私有数据 每个硬件设备都有一些**属性**, 比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式: ```c dev_t devid; /@@*设备号*/ struct cdev cdev; /@@*cdev*/ struct class *class; /@@*类*/ struct device *device; /@@*设备*/ int major; /@@*主设备号*/ int minor; /@@*次设备号*/ ``` 可以将所有属性信封装到结构体中, 并在编写驱动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; } ``` # 3 驱动程序编写 **在上篇的基础上进行修改**,因为只是更换的驱动程序的编写方式,与应用程序无关,因此只修改驱动程序即可。 ## 3.1 添加一些定义 因为上篇文章的代码中使用的是chrdevbase这个名称,为了减少修改量,这里仅把结构体类型定义为带有new标志的newchr_dev,变量名仍使用**chrdevbase**这个名称。 ```c #define CHRDEVBASE_CNT 1 /@@* 设备号个数 */ #define CHRDEVBASE_NAME "chrdevbase" /@@* 名字 */ /@@*newchr设备结构体 */ struct newchr_dev{ dev_t devid; /@@* 设备号 */ struct cdev cdev; /@@* cdev */ struct class *class; /@@* 类 */ struct device *device; /@@* 设备 */ int major; /@@* 主设备号 */ int minor; /@@* 次设备号 */ }; struct newchr_dev chrdevbase; /@@* 自定义字符设备 */ ``` ## 3.2 修改open函数 在上篇程序的基础上增加了一条“设置私有数据” ```c static int chrdevbase_open(struct inode *inode, struct file *filp) { printk("chrdevbase open!\r\n"); filp->private_data = &chrdevbase; /@@* 设置私有数据 */ return 0; } ``` ## 3.3 修改init函数 这个修改比较大,因为要在init函数中使用**设备号的自动分配**。 ```c static int __init chrdevbase_init(void) { /@@* 注册字符设备驱动 */ /@@* 1、创建设备号 */ if (chrdevbase.major) /@@* 定义了设备号 */ { chrdevbase.devid = MKDEV(chrdevbase.major, 0); register_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT, CHRDEVBASE_NAME); } else /@@* 没有定义设备号 */ { alloc_chrdev_region(&chrdevbase.devid, 0, CHRDEVBASE_CNT, CHRDEVBASE_NAME); /@@* 申请设备号 */ chrdevbase.major = MAJOR(chrdevbase.devid); /@@* 获取分配号的主设备号 */ chrdevbase.minor = MINOR(chrdevbase.devid); /@@* 获取分配号的次设备号 */ } printk("chrdevbase major=%d,minor=%d\r\n",chrdevbase.major, chrdevbase.minor); /@@* 2、初始化cdev */ chrdevbase.cdev.owner = THIS_MODULE; cdev_init(&chrdevbase.cdev, &chrdevbase_fops); /@@* 3、添加一个cdev */ cdev_add(&chrdevbase.cdev, chrdevbase.devid, CHRDEVBASE_CNT); /@@* 4、创建类 */ chrdevbase.class = class_create(THIS_MODULE, CHRDEVBASE_NAME); if (IS_ERR(chrdevbase.class)) { return PTR_ERR(chrdevbase.class); } /@@* 5、创建设备 */ chrdevbase.device = device_create(chrdevbase.class, NULL, chrdevbase.devid, NULL, CHRDEVBASE_NAME); if (IS_ERR(chrdevbase.device)) { return PTR_ERR(chrdevbase.device); } printk("chrdevbase init done!\r\n"); return 0; } ``` ## 3.4 修改exit函数 因为init修改较大,对应的exit也要进行大的修改: ```c static void __exit chrdevbase_exit(void) { /@@* 注销字符设备驱动 */ cdev_del(&chrdevbase.cdev);/@@* 删除cdev */ unregister_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT); /@@* 注销设备号 */ device_destroy(chrdevbase.class, chrdevbase.devid); class_destroy(chrdevbase.class); printk("chrdevbase exit done!\r\n"); } ``` 至此,修改完毕,其它的与之前的一样。 ## 3.5 新旧驱动方式对比 通过一张图来**对比新旧两种驱动编写方式的区别**: - 旧方式编写驱动的流程 ![](https://cf05.ickimg.com/bbsimages/202108/479928d350f1fd6412483fa14ce6ba3f.png) - 新方式编写驱动的流程 ![](https://cf05.ickimg.com/bbsimages/202108/fdca60850e5cd9aa598d88bf3d5070d0.png) 可以看出主要区别在驱动的加载和卸载。 # 4 编译驱动 和上次编译驱动的方式一样,使用makefile,因为驱动的c文件名由chrdevbase.c改为了newchrdevbase.c,因此makefile文件中也要把名字改掉。 编译完之后,将编译出的ko文件先复制到ubuntu虚拟机的**tftpboot目录**中,为后面的测序做准备。 ![](https://cf05.ickimg.com/bbsimages/202108/cd48b85059cd5ae17ca1f7768976e27f.png) 复制后,看一下tftpboot目录: ![](https://cf05.ickimg.com/bbsimages/202108/1814c38ee93f79335b85ecadea28d4e6.png) # 5 程序测试 ## 5.1 文件发送到板子 和上篇一样,**使用tftp传输,将ubuntu虚拟机编译出的ko文件发送到linux板子中**。 再来看下tftp传输的硬件环境示意图: ![](https://cf05.ickimg.com/bbsimages/202108/0afd4d687ce4c53526020daabb2a551c.png) 然后是传输指令以及传输结果,可以看到newchrdevbase.ko已经从ubuntu虚拟机的tftpboot目录传输到了linux板子的/lib/modules/4.1.15目录中了。 ![](https://cf05.ickimg.com/bbsimages/202108/e63f17684328205a76aba6707d8902da.png) ## 5.2 测试 输入如下两条指令加载 newchrdevbase.ko 驱动模块: ```sh depmod //第一次加载驱动的时候需要运行此命令 modprobe newchrdevbase.ko //加载驱动 ``` 驱动加载成功后,可以看到自动申请到的主设备号和次设备号,如下图,主设备号为249。 再输入`ls /dev/chrdevbase -l`指令验证/dev/chrdevbase 这个设备节点文件是否存在,如下图,可以看到设备存在,注意和上篇旧驱动方式操作上的不同之处,**旧的驱动方式需要额外使用mknod指令来手动创建该设备节点**。 ![](https://cf05.ickimg.com/bbsimages/202108/8f935417b7362a465c677ac6e83b1e90.png) 驱动已经加载成功,再来测试APP程序,理论上和上篇的效果一样,实测也是: ![](https://cf05.ickimg.com/bbsimages/202108/22cf091255517322c0d55cfd8a20d22e.png) OK,测试完毕,测试完使用rmmod指令卸载驱动。 # 6 总结 此篇文章针对上篇文章使用旧字符驱动编写方式存在的不足,介绍了一种新的字符驱动编写方式,对比两种方式编写的主要区别,在上篇驱动代码的基础上进行修改,并测试通过,和上篇实现一样的效果,但驱动的加载更加方便,不再需要人为指定设备号。
原创作品,未经权利人授权禁止转载。详情见
转载须知
。
举报文章
点赞
(
0
)
码农爱学习
关注
评论
(0)
登录后可评论,请
登录
或
注册
相关文章推荐
MK-米客方德推出工业级存储卡
Beetle ESP32 C3 蓝牙数据收发
Beetle ESP32 C3 wifi联网获取实时天气信息
开箱测评Beetle ESP32-C3 (RISC-V芯片)模块
正点原子数控电源DP100测评
DP100试用评测-----开箱+初体验
Beetle ESP32 C3环境搭建
【花雕体验】16 使用Beetle ESP32 C3控制8X32位WS2812硬屏之二
X
你的打赏是对原创作者最大的认可
请选择打赏IC币的数量,一经提交无法退回 !
100IC币
500IC币
1000IC币
自定义
IC币
确定
X
提交成功 ! 谢谢您的支持
返回
我要举报该内容理由
×
广告及垃圾信息
抄袭或未经授权
其它举报理由
请输入您举报的理由(50字以内)
取消
提交