驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。
linux系统将设备分为3类:字符设备、块设备、网络设备。
先看一张图,图中描述了流程,有助了解驱动。
用户态:
内核态:
驱动链表:管理所有设备的驱动,添加或查找,添加是发生在我们编写完驱动程序,加载到内核。查找是在调用驱动程序,由应用层用户空间去查找使用open函数。驱动插入链表的顺序由设备号检索,就是说主设备号和次设备号除了能区分不同种类的设备和不同类型的设备,还能起到将驱动程序加载到链表的某个位置,在下面介绍的驱动代码的开发无非就是添加驱动(添加设备号、设备名和设备驱动函数)和调用驱动。
补充:
字符设备驱动工作原理在linux的世界里一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。我们知道如果应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核有那么多驱动程序,应用怎么才能精确的调用到底层的驱动程序呢?
(1)当open函数打开设备文件时,可以根据设备文件对应的structinode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个structfile结构体。
(2)根据structinode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个structcdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。
(3)找到structcdev结构体后,linux内核就会将structcdev结构体所在的内存空间首地址记录在structinode结构体i_cdev成员中,将structcdev结构体中的记录的函数操作接口地址记录在structfile结构体的f_ops成员中。
(4)任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和structfile结构体对应的。接下来上层应用程序就可以通过fd找到structfile,然后在structfile找到操作字符设备的函数接口file_operation了。
其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作。
如图,在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。
在Linux字符设备驱动中,模块加载函数通过register_chrdev_region()或alloc_chrdev_region()来静态或者动态获取设备号,通过cdev_init()建立cdev与file_operations之间的连接,通过cdev_add()向系统添加一个cdev以完成注册。模块卸载函数通过cdev_del()来注销cdev,通过unregister_chrdev_region()来释放设备号。
用户空间访问该设备的程序通过Linux系统调用,如open()、read()、write(),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数。
Linux内核就是由各种驱动组成的,内核源码中有大约85%是各种驱动程序的代码。内核中驱动程序种类齐全,可以在同类驱动的基础上进行修改以符合具体单板。
编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。
一般来说,编写一个linux设备驱动程序的大致流程如下:
下面就以一个简单的字符设备驱动框架代码来进行驱动程序的开发、编译等。
上层调用代码
驱动框架代码
驱动框架设计流程
1.确定主设备号
2.定义结构体类型file_operations
3.实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体
4.实现驱动入口:安装驱动程序时,就会去调用这个入口函数,执行工作:
①把file_operations结构体告诉内核:注册驱动程序register_chrdev.
②创建类class_create.
③创建设备device_create.
5.实现出口:卸载驱动程序时,就会去调用这个出口函数,执行工作:
①把file_operations结构体从内核注销:unregister_chrdev.
②销毁类class_create.
③销毁设备结点device_destroy.
6.其他完善:GPL协议、入口加载
1、确定主设备、变量定义
2、定义file_operations结构体,加载到内核驱动链表中
这是Linux内核中的file_operations结构体
根据上层调用函数定义结构体成员
staticstructfile_operationspin4_fops={.owner=THIS_MODULE,.open=pin4_open,.write=pin4_write,.read=pin4_read,};3、实现结构体成员pin4_read等函数
4、驱动入口
int__initpin4_drv_init(void)//真实驱动入口{intret;devno=MKDEV(major,minor);//创建设备号ret=register_chrdev(major,module_name,&pin4_fops);//注册驱动告诉内核,把这个驱动加入到内核驱动的链表中pin4_class=class_create(THIS_MODULE,'myfirstdemo');//由代码在dev自动生成设备pin4_class_dev=device_create(pin4_class,NULL,devno,NULL,module_name);//创建设备文件return0;}其中pin4_class=class_create(THIS_MODULE,'myfirstdemo');//由代码在dev自动生成设备,除此之外还可以手动生成设备,在dev目录下sudomknod+设备名字+设备类型(c表示字符设备驱动)+主设备号+次设备号。
5、出口
6、GPI协议,入口加载,出口加载
module_init(pin4_drv_init);//入口,内核加载该驱动(insmod)的时候,这个宏被使用module_exit(pin4_drv_exit);MODULE_LICENSE('GPLv2');驱动模块代码编译和测试编译阶段
驱动模块代码编译(模块的编译需要配置过的内核源码,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。)
将该驱动代码拷贝到linux-rpi-4.14.y/drivers/char目录下文件中(也可选择设备目录下其它文件)
修改该文件夹下Makefile(驱动代码放到哪个目录,就修改该目录下的Makefile),将上面的代码编译生成模块,文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的),所以只需要将obj-m+=pin4drive.o添加到Makefile中即可。
回到linux-rpi-4.14.y/编译驱动文件
使用指令:ARCH=armCROSS_COMPILE=arm-linux-gnueabihf-KERNEL=kernel7makemodules进行编译生成驱动模块。
编译生成驱动模块会生成以下几个文件:
.o的文件是object文件,.ko是kernelobject,与.o的区别在于其多了一些sections,比如.modinfo。.modinfosection是由kernelsource里的modpost工具生成的,包括MODULE_AUTHOR,MODULE_DESCRIPTION,MODULE_LICENSE,deviceIDtable以及模块依赖关系等等。depmod工具根据.modinfosection生成modules.dep,modules.*map等文件,以便modprobe更方便的加载模块。
编译过程中,经历了这样的步骤:先进入Linux内核所在的目录,并编译出pin4drive.o文件,运行MODPOST会生成临时的pin4drive.mod.c文件,而后根据此文件编译出pin4drive.mod.o,之后连接pin4drive.o和pin4drive.mod.o文件得到模块目标文件pin4drive.ko,最后离开Linux内核所在的目录。
将生成的.ko文件发送给树莓派:scppin4drive.kopi@192.168.1.106:/home/pi
将pin4test.c(上层调用代码)进行交叉编译后发送给树莓派,就可以看到pi目录下存在发送过来的.ko文件和pin4test这两个文件,
加载内核驱动
lsmod查看系统的驱动模块,执行上层代码,赋予权限
查看内核打印的信息,
如下图所示:表示驱动调用成功
在装完驱动后可以使用指令:sudormmod+驱动名(不需要写ko)将驱动卸载。
调用流程:
我们上层空间的open去查找dev下的驱动(文件名),文件名背后包含了驱动的主设备号和次设备号,此时用户open触发一个系统调用,系统调用经过vfs(虚拟文件系统),vfs根据文件名背后的设备号去调用sys_open去判断,找到内核中驱动链表的驱动位置,再去调用驱动里面自己的dev_open函数
为什么生成驱动模块需要在虚拟机上生成?树莓派不行吗?
生成驱动模块需要编译环境(linux源码并且编译,需要下载和系统版本相同的Linux内核源代码),也可以在树莓派上面编译,但在树莓派里编译,效率会很低,要非常久。