1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html 3)对正点原子Linux感兴趣的同学可以加群讨论:935446741 4)关注正点原子公众号,获取最新资料更新
第四十章 字符设备驱动开发
本章我们从Linux驱动开发中最基础的字符设备驱动开始,重点学习Linux下字符设备驱动开发框架。本章会以一个虚拟的设备为例,讲解如何进行字符设备驱动开发,以及如何编写测试APP来测试驱动工作是否正常,为以后的学习打下坚实的基础。
40.1 字符设备驱动简介 字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动。 在详细的学习字符设备驱动架构之前,我们先来简单的了解一下Linux下的应用程序是如何调用驱动程序的,Linux应用程序对驱动程序的调用如图40.1.1所示:
图40.1.1 Linux应用程序对驱动程序的调用流程 在Linux中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫做/dev/led的驱动文件,此文件是led灯的驱动文件。应用程序使用open函数来打开文件/dev/led,使用完成以后使用close函数关闭/dev/led这个文件。open和close就是打开和关闭led驱动的函数,如果要点亮或关闭led,那么就使用write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数。如果要获取led灯的状态,就用read函数从驱动中读取相应的状态。 应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write和read等这些函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。当我们调用open函数的时候流程如图40.1.2所示:
图40.1.2 open函数调用流程 其中关于C库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了open这个函数,那么在驱动程序中也得有一个名为open的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在Linux内核文件 include/linux/fs.h中有个叫做file_operations的结构体,此结构体就是Linux内核驱动操作函数集合,内容如下所示: 示例代码40.1.1 file_operations结构体
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1592 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
1606 int (*fasync) (int, struct file *, int);
1607 int (*lock) (struct file *, int, struct file_lock *);
1608 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
1609 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
1610 int (*check_flags)(int);
1611 int (*flock) (struct file *, int, struct file_lock *);
1612 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
1613 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
1614 int (*setlease)(struct file *, long, struct file_lock **, void **);
1615 long (*fallocate)(struct file *file, int mode, loff_t offset,
1616 loff_t len);
1617 void (*show_fdinfo)(struct seq_file *m, struct file *f);
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };
简单介绍一下file_operation结构体中比较重要的、常用的函数:
第1589行,owner拥有该结构体的模块的指针,一般设置为THIS_MODULE。 第1590行,llseek函数用于修改文件当前的读写位置。 第1591行,read函数用于读取设备文件。 第1592行,write函数用于向设备文件写入(发送)数据。 第1596行,poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。 第1597行,unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应。 第1598行,compat_ioctl函数与unlocked_ioctl函数功能一样,区别在于在64位系统上,32位的应用程序调用将会使用此函数。在32位的系统上运行32位的应用程序调用的是unlocked_ioctl。 第1599行,mmap函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将帧缓冲(LCD显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。 第1601行,open函数用于打开设备文件。 第1603行,release函数用于释放(关闭)设备文件,与应用程序中的close函数对应。 第1604行,fasync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。 第1605行,aio_fsync函数与fasync函数的功能类似,只是aio_fsync是异步刷新待处理的数据。 在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,但是像open、release、write、read等都是需要实现的,当然了,具体需要实现哪些函数还是要看具体的驱动要求。 40.2 字符设备驱动开发步骤 上一小节我们简单的介绍了一下字符设备驱动,那么字符设备驱动开发都有哪些步骤呢?我们在学习裸机或者STM32的时候关于驱动的开发就是初始化相应的外设寄存器,在Linux驱动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在Linux驱动开发中我们需要按照其规定的框架来编写驱动,所以说学Linux驱动开发重点是学习其驱动框架。 40.2.1 驱动模块的加载和卸载 Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux内核中,当然也可以不编译进Linux内核中,具体看自己的需求。 模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下: module_init(xxx_init); //注册模块加载函数 module_exit(xxx_exit); //注册模块卸载函数 module_init函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候,xxx_init这个函数就会被调用。module_exit()函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit函数就会被调用。字符设备驱动模块加载和卸载模板如下所示: 示例代码40.2.1.1 字符设备驱动模块加载和卸载函数模板
1 /* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函数具体内容 */
5 return 0;
6 }
7
8 /* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函数具体内容 */
12 }
13
14 /* 将上面两个函数指定为驱动的入口和出口函数 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);
第2行,定义了个名为xxx_init的驱动入口函数,并且使用了“__init”来修饰。
第9行,定义了个名为xxx_exit的驱动出口函数,并且使用了“__exit”来修饰。
第15行,调用函数module_init来声明xxx_init为驱动入口函数,当加载驱动的时候xxx_init函数就会被调用。
第16行,调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用。
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod和modprobe,insmod是最简单的模块加载命令,此命令用于加载指定的.ko模块,比如加载drv.ko这个驱动模块,命令如下:
insmod drv.ko insmod命令不能解决模块的依赖关系,比如drv.ko依赖first.ko这个模块,就必须先使用insmod命令加载first.ko这个模块,然后再加载drv.ko这个模块。但是modprobe就不会存在这个问题,modprobe会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe命令相比insmod要智能一些。modprobe命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用modprobe命令来加载驱动。modprobe命令默认会去/lib/modules/目录中查找模块,比如本书使用的Linux kernel的版本号为4.1.15,因此modprobe命令默认会到/lib/modules/4.1.15这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。 驱动模块的卸载使用命令“rmmod”即可,比如要卸载drv.ko,使用如下命令即可: rmmod drv.ko 也可以使用“modprobe -r”命令卸载驱动,比如要卸载drv.ko,命令如下: modprobe -r drv.ko 使用modprobe命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用modprobe来卸载驱动模块。所以对于模块的卸载,还是推荐使用rmmod命令。 40.2.2 字符设备注册与注销 对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示: static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) static inline void unregister_chrdev(unsigned int major, const char *name) register_chrdev函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下: major:主设备号,Linux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。 name:设备名字,指向一串字符串。 fops:结构体file_operations类型指针,指向设备的操作函数集合变量。 unregister_chrdev函数用户注销字符设备,此函数有两个参数,这两个参数含义如下: major:要注销的设备对应的主设备号。 name:要注销的设备对应的设备名。 一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行。在示例代码40.2.2.1中字符设备的注册和注销,内容如下所示: 示例代码40.2.2.1 加入字符设备注册和注销
1 static struct file_operations test_fops;
2
3 /* 驱动入口函数 */
4 static int __init xxx_init(void)
5 {
6 /* 入口函数具体内容 */
7 int retvalue = 0;
8
9 /* 注册字符设备驱动 */
10 retvalue = register_chrdev(200, "chrtest", &test_fops);
11 if(retvalue
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?