完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
1)实验平台:正点原子领航者ZYNQ开发板
2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/FPGA/zdyz_linhanz.html 4)对正点原子FPGA感兴趣的同学可以加群讨论:876744900 5)关注正点原子公众号,获取最新资料 第二十三章新字符设备驱动实验 经过前两章实验的实战操作,我们已经掌握了Linux字符设备驱动开发的基本步骤,字符设备驱动开发重点是使用register_chrdev函数注册字符设备,当不再使用设备的时候就使用unregister_chrdev函数注销字符设备,驱动模块加载成功以后还需要手动使用mknod命令创建设备节点。register_chrdev和unregister_chrdev这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。本节我们就来学习一下如何编写新字符设备驱动,并且在驱动模块加载的时候自动创建设备节点文件。 23.1新字符设备驱动原理 23.1.1分配和释放设备号 使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题: ①、需要我们事先确定好哪些主设备号没有使用。 ②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么0~1048575(2^20-1)这个区间的次设备号就全部都被LED一个设备分走了。这样太浪费次设备号了!一个LED设备肯定只能有一个主设备号,一个次设备号。 解决这两个问题最好的方法就是要使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。这个就是我们在32.3.2小节讲解的设备号的分配,如果没有指定设备号的话就使用如下函数来申请设备号: 如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可: int register_chrdev_region(dev_t from, unsigned count, const char *name) 参数from是要申请的起始设备号,也就是给定的设备号;参数count是要申请的数量,一般都是一个;参数name是设备名字。 注销字符设备之后要释放掉设备号,不管是通过alloc_chrdev_region函数还是register_chrdev_region函数申请的设备号,统一使用如下释放函数: void unregister_chrdev_region(dev_t from, unsigned count)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 }新字符设备驱动下,设备号分配示例代码如下:示例代码23.1.1.1 新字符设备驱动下设备号分配 第1~3行,定义了主/次设备号变量major和minor,以及设备号变量devid。 第5行,判断主设备号major是否有效,在Linux驱动中一般给出主设备号的话就表示这个设备的设备号已经确定了,因为次设备号基本上都选择0,这算个Linux驱动开发中约定俗成的一种规定了。 第6行,如果major有效的话就使用MKDEV来构建设备号,次设备号选择0。 第7行,使用register_chrdev_region函数来注册设备号。 第9~11行,如果major无效,那就表示没有给定设备号。此时就要使用alloc_chrdev_region函数来申请设备号。设备号申请成功以后使用MAJOR和MINOR来提取出主设备号和次设备号,当然了,第10和11行提取主设备号和次设备号的代码可以不要。 如果要注销设备号的话,使用如下代码即可: 示例代码23.1.1.2 cdev结构体 1 unregister_chrdev_region(devid, 1); /* 注销设备号 */ 注销设备号的代码很简单。 23.1.2新的字符设备注册方法 1、字符设备结构 在Linux中使用cdev结构体表示一个字符设备,cdev结构体在include/linux/cdev.h文件中的定义如下: 示例代码23.1.2.1 cdev结构体 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 }; 在cdev中有两个重要的成员变量:ops和dev,这两个就是字符设备文件操作函数集合file_operations以及设备号dev_t。编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就表示一个字符设备,如下所示:struct cdev test_cdev;复制代码 2、cdev_init函数 定义好cdev变量以后就要使用cdev_init函数对其进行初始化,cdev_init函数原型如下: 复制代码 参数cdev就是要初始化的cdev结构体变量,参数fops就是字符设备文件操作函数集合。使用cdev_init函数初始化cdev变量的示例代码如下: 示例代码23.1.2.2 cdev_init函数使用示例代码 1 struct cdev testcdev;2 3 /* 设备操作函数 */4 static struct file_operations test_fops = {5 .owner = THIS_MODULE,6 /* 其他具体的初始项 */7 };8 9 testcdev.owner = THIS_MODULE;10 cdev_init(&testcdev, &test_fops); /* 初始化cdev结构体变量 */11 cdev_add(&testcdev, devid, 1); /* 添加字符设备 */示例代码23.1.4就是新的注册字符设备代码段,Linux内核中大量的字符设备驱动都是采用这种方法向Linux内核添加字符设备。如果在加上示例代码23.1.1中分配设备号的程序,那么就它们一起实现的就是函数register_chrdev的功能。3、cdev_del函数卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备,cdev_del函数原型如下: 复制代码 参数p指向要添加的字符设备(cdev结构体变量),参数dev就是设备所使用的设备号,参数count是要添加的设备数量。完善示例代码23.1.4,加入cdev_add函数,内容如下所示: 示例代码23.1.2.3 cdev_add函数使用示例 1 cdev_del(&testcdev); /* 删除cdev */
参数p就是要删除的字符设备。如果要删除字符设备,参考如下代码: 示例代码23.1.2.4 cdev_del函数使用示例 1 #define class_create(owner, name) 2 ({ 3 static struct lock_class_key __key; 4 __class_create(owner, name, &__key); 5 })6 7 struct class *__class_create(struct module *owner, const char *name,8 struct lock_class_key *key) cdev_del和unregister_chrdev_region这两个函数合起来的功能相当于unregister_chrdev函数。 23.2自动创建设备节点 在前面的Linux驱动实验中,当我们使用modprobe加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件。 23.2.1mdev机制 udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除,比如使用modprobe命令成功加载驱动模块以后就自动在/dev目录下创建对应的设备节点文件,使用rmmod命令卸载驱动模块以后就删除掉/dev目录下的设备节点文件。使用busybox构建根文件系统的时候(我们在petalinux中编译得到的根文件系统其实就是busybox构建出来的),busybox会创建一个udev的简化版本—mdev,所以在嵌入式Linux中我们使用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理。 关于udev或mdev更加详细的工作原理我们不去讨论,本章我们重点来学习设备文件节点的自动创建与删除。 23.2.2创建和删除类 自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个class类(class的概念这里暂时不去细说,大家可以先简答地理解为设备类即可,就是某个设备属于某个类,后面我们会详细的讲),class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义,内容如下: 示例代码23.2.2.1 class_create函数 struct class *class_create (struct module *owner, const char *name) 根据上述代码,将宏class_create展开以后内容如下: struct class *class_create (struct module *owner, const char *name) class_create一共有两个参数,参数owner一般为THIS_MODULE,参数name是类名字。返回值是个指向结构体class的指针,也就是创建的类。 卸载驱动程序的时候需要删除掉类,类删除函数为class_destroy,函数原型如下:
参数cls就是要删除的类。 23.2.3创建设备 上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用device_create函数在类下面创建设备,device_create函数原型如下: struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) device_create是个可变参数函数,参数class就是设备要创建哪个类下面;参数parent是父设备,一般为NULL,也就是没有父设备;参数devt是设备号;参数drvdata是设备可能会使用的一些数据,一般为NULL;参数fmt是设备名字,如果设置fmt=xxx的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。 同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为device_destroy,函数原型如下: 1 struct class *class; /* 类 */ 2 struct device *device; /* 设备 */3 dev_t devid; /* 设备号 */ 4 5 /* 驱动入口函数 */ 6 static int __init xxx_init(void)7 {8 /* 创建类 */9 class = class_create(THIS_MODULE, "xxx");10 /* 创建设备 */11 device = device_create(class, NULL, devid, NULL, "xxx");12 return 0;13 }14 15 /* 驱动出口函数 */16 static void __exit led_exit(void)17 {18 /* 删除设备 */19 device_destroy(newchrled.class, newchrled.devid);20 /* 删除类 */21 class_destroy(newchrled.class);22 }23 24 module_init(led_init);25 module_exit(led_exit); 参数classs是要删除的设备所处的类,参数devt是要删除的设备号。 23.2.4参考示例 在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下: 示例代码23.2.4.1 创建/删除类/设备参考代码 dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */ 23.3设置文件私有数据 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示: 示例代码23.3.1 变量形式的设备属性 dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */ 这样写肯定没有问题,但是这样写不专业!对于一个设备的所有属性信息我们最好将其做成一个结构体。编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中,如下所示: 示例代码23.3.2 设备结构体作为私有数据 在open函数里面设置好私有数据以后,在write、read、close等函数中直接读取private_data即可得到设备结构体。 23.4硬件原理图分析 本章实验硬件原理图参考33.3小节即可! 23.5实验程序编写 本实验对应的例程路径为:ZYNQ开发板光盘资料(A盘)4_SourceCodeZYNQ_70103_Embedded_LinuxLinux驱动例程3_newchrled。 本章实验在上一章实验的基础上完成,重点是使用了新的字符设备驱动、设置了文件私有数据、添加了自动创建设备节点相关内容。 23.5.1LED灯驱动程序编写 在drivers目录下新建名为“3_newchrled”的文件夹,在3_newchrled目录下新建newchrled.c文件,里面输入如下内容: 示例代码23.5.1.1 newchrled.c 1 /*************************************************************** 2 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 3 文件名 : newchrled.c 4 作者 : 邓涛 5 版本 : V1.0 6 描述 : ZYNQ LED驱动文件。 7 其他 : 无 8 论坛 : www.openedv.com 9 日志 : 初版V1.0 2019/1/30 邓涛创建10 ***************************************************************/11 12 #include 第25行,宏NEWCHRLED_CNT表示设备数量,在申请设备号或者向Linux内核添加字符设备的时候需要设置设备数量,一般我们一个驱动一个设备,所以这个宏为1。 第26行,宏NEWCHRLED_NAME表示设备名字,本实验的设备名为“newchrdev”,为了方便管理,所有使用到设备名字的地方统一使用此宏,当驱动加载成功以后就生成/dev/newchrled这个设备文件。 第46~53行,创建设备结构体newchrled_dev。 第55行,定义一个设备结构体变量newchrdev,此变量表示led设备。 第64~68行,在led_open函数中设置文件的私有数据private_data指向newchrdev。 第152~241行,根据前面讲解的方法在驱动入口函数led_init中申请设备号、添加字符设备、创建类和设备。186~198行代码去申请设备号,如果提供了设备号则申请静态设备号,如果我们提供的设备号为0则采用动态申请设备号的方法,第200行使用printk在终端上显示出申请到的主设备号和次设备号。 第241~245行,根据前面讲解的方法,在驱动出口函数led_exit中注销字符新设备、删除类和设备。 总体来说newchrled.c文件中的内容不复杂,LED灯驱动部分的程序和上一章一样。重点就是使用了新的字符设备驱动方法。 驱动中的倒退式处理方法 细心的同学会发现在上面的代码当中用到了C语言中goto语句,不知道大家对这个goto熟悉不熟悉,goto顾名思义其实即使跳转的意思,例如goto out1,就是跳转到out地址所在的地方,那么goto语句在linux内核当中用的特别多,因为它非常符合linux下这种开发环境,因为内核中一个函数可能包含了很多个操作,这些操作每一步都有可能出错,如果出错之后那么后面的步骤就没有进行下去的必要性了,但是你不能直接退出,你得把你前面做过的操作给“复原”了。 例如在上面的代码当中,如果在调用device_create函数注册设备的时候失败了,没有成功,那么你就得把前面做的工作给“复原”,你创建了class类,那你就得调用class_destroy删除这个类,你添加了cdev,那你就得删除cdev,而且他这个顺序还是倒退式的,大家需要去好好理解下,以后自己开发驱动也需要养成这样的习惯。 23.5.2编写测试APP 本章直接使用上一章的测试APP,将上一章的ledApp.c文件复制到本章实验目录下(3_newchrled)即可。 23.6运行测试 23.6.1编译驱动程序和测试APP 1、编译驱动程序 将上一章使用的Makefile文件拷贝到本实验的目录下,然后打开这个Makefile文件,将obj-m变量的值改为newchrled.o,最终Makefile内容如下所示: 示例代码23.6.1.1 Makefile文件内容 1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3 2 3 obj-m := newchrled.o 4 5 all: 6 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modules 7 8 clean: 9 make -C $(KERN_DIR) M=`pwd` clean复制代码 第3行,设置obj-m变量的值为newchrled.o。 执行make命令编译出驱动模块文件: make 编译成功以后就会生成一个名为“newchrled.ko”的驱动模块文件,如下所示: 图 34.6.1 编译过程 2、编译测试APP 我们直接用上一章使用的测试程序ledApp.c,直接使用上一章编译好的ledApp可执行文件,不用再重新编译了。 23.6.2运行测试 将上一小节编译出来的newchrled.ko和ledApp这两个文件拷贝到开发板根文件系统/lib/modules/4.14.0-xilinx目录下,重启开发板,进入到/lib/modules/4.14.0-xilinx目录中,输入如下命令加载newchrled.ko驱动模块: Depmod //第一次加载驱动的时候需要运行此命令modprobe newchrled.ko //加载驱动 驱动加载成功以后会输出申请到的主设备号和次设备号,如下图所示: 图 34.6.2 加载驱动模块 从上图可以看出,申请到的主设备号为244,次设备号为0。驱动加载成功以后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看/dev/newchrdev这个设备节点文件是否存在: ls /dev/newchrled -l 结果如下图所示: 图 34.6.3 /dev/newchrled设备节点 从图中可以看出,/dev/newchrled这个设备文件存在,而且主设备号为244,此设备号为0,说明设备节点文件创建成功。 驱动节点创建成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯: ./ledApp /dev/newchrled 1 //打开LED灯 输入上述命令以后查看底板上的PS_LED0灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯: ./ledApp /dev/newchrled 0 //关闭LED灯 输入上述命令以后查看底板上的PS_LED0灯是否熄灭。如果要卸载驱动的话输入如下命令即可: rmmod newchrled.ko |
|
相关推荐
|
|
如何配置Linux操作系统设备树让我的开发板可以将板子上的GPIO接口用作 I2S输出??
1469 浏览 1 评论
1340 浏览 0 评论
2025 浏览 0 评论
2006 浏览 2 评论
1116 浏览 0 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-8-18 06:36 , Processed in 0.411083 second(s), Total 35, Slave 28 queries .
Powered by 电子发烧友网
© 2015 www.ws-dc.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号