
解读RIFFA驱动
基于Linux
感谢西安电子科技大学锤王投稿。
Part3字符设备驱动框架
13.1 Linux设备分类
Linux将设备主要分为三种:字符设备(char device)、块设备(block device)和网络设备(net device)。
字符设备一般指串并行I/O设备,比如串口、USB、PCIe等。字符设备是Linux中最简单的驱动设备,可以通过文件I/O进行访问。
块设备一般指硬盘、光盘驱动器等存储设备。
网络设备主要基于BSD UNIX的socket机制,用于网络通信。
一个具体的设备可能不仅仅属于上述其中一种分类,比如网卡,它既是字符设备(因为有PCIe)又是网络设备。而RIFFA是一种PCIe框架,它仅处理PCIe业务,自然仅属于字符设备,但因为它使用了PCIe,所以也需要处理PCI设备的一些内容。
23.2 设备号
Linux中需要为每个字符设备都分配一个主设备号(major)和一个次设备号(minor)。主设备号用来标识一个驱动程序,次设备号标识一个具体的设备。例如在SPI总线上挂载了一个陀螺仪、一个OLED屏和一个ADC,这三个设备使用同一个SPI总线,那么这个SPI总线拥有一个主设备号,这三个具体的设备主设备号相同但次设备号不同。在一般的驱动开发中通常是分配主设备号,然后让次设备号为0。
Linux为设备号提供了一种宏类型dev_t,这个宏类型里包含了主、次设备号。
33.3 字符设备操作函数结构体
因为字符设备驱动在内核态运行,而用户想使用驱动需要通过系统调用陷入内核,所以字符设备驱动需要对用户暴露一些可供读写或其他操作的接口(即驱动编写者需要定义系统调用的功能)作为内核与用户应用间的桥梁。
这些系统调用就是C语言中常使用的文件I/O,即open、close、write、read、lseek这类函数,当然也可以通过标准I/O(fcopen、fclose等,标准I/O有缓存,但最终也是调用了文件I/O来陷入内核)以及内存映射(mmap函数,实现存储器到用户空间的零拷贝功能)。
总而言之,这些系统函数对于驱动来说是一副空壳,函数参数作为接口传递给用户应用,而函数内部什么都没有,是需要驱动编写者填充的,你在系统函数里写一个特定的功能,用户调用系统函数访问你的驱动时就会执行这个功能。举例如下所示:

用户应用程序使用open系统函数打开这个驱动的节点文件时,CPU就会陷入内核并将open函数所传入的形参送给驱动里的这个my_open函数的参数中。
向open一样的这些系统调用函数都需要被注册到内核中,成为这个字符设备驱动的一部分。
Linux使用了一个struct file_operations结构体来存储这些系统调用函数的首地址,结构体在/linux/fs.h中定义,如下所示:
别看这个结构体成员这么多,其实不用全部填充,你的驱动需要用到什么功能就填充对应的函数指针,比如驱动一个蜂鸣器,那么我完全用不到read这个函数,就不需要把对应read函数的函数指针传递给这个结构体中的对应成员。

这个结构体中比较常用的成员有owner、open、release、write、read和unlocked_ioctl。其中owner一般都直接填写THIS_MODULE,这是一个宏,表示struct file_operations结构体的所有者是这个驱动模块。
open函数就是打开文件、release就是close函数,关闭文件、write和read就是读写文件了,那么还有一个比较重要的系统调用,即unlocked_ioctl,因为不可能所有设备都只有读和写这两种操作,可能还需要比如弹出设备、复位等操作,那么这些操作不归类于读或写任何一种,那么在哪写才能使用呢,就是ioctl,ioctl会有一个操作类型选择的输入参数,在函数内部可以用switch语句来判断事务类型并进行相应的操作。
43.5 注册字符设备
RIFFA驱动中的字符设备注册使用的是2.6内核以前的老API register_chrdev,这种注册方式是向内核注册cdev结构体并将设备号、前面的struct file_operations结构体对象等信息填充这个结构体然后再注册给内核,这种注册方式的缺点是他不用指定次设备号,因为它会把0~255次设备号全部注册,极大浪费了次设备号,2.6内核以后的版本推荐自己填充cdev并使用register_chrdev_region函数来注册给内核,它可以自己指定分配多少次设备号。
当然RIFFA驱动里用的是老API,那么我们就讲讲这个老API的使用方法。
register_chrdev函数的定义在/linux/fs.h中,如下图所示:

参数major是要注册的主设备号、参数*name是一个字符串常量,表示这个字符设备的名称,随便定义,这个名称最终会显示在/proc/devices中、参数*fops是struct file_operations结构体对象的首地址,若函数返回负数代表注册失败,正数为注册成功。
53.6 驱动在Linux中的表现形式
驱动经过编译后会产生扩展名为".ko"的二进制文件,通过命令insmod可以将驱动动态加载到内核中或者ko文件复制到内核目录下,使用depmod和modprobe命令也可以动态加载驱动。
驱动动态加载后会在/proc/devices文件中注册该设备的主次设备号,用户就可以用设备号通过mknod命令在/dev目录下创建设备节点,以后应用程序访问该设备就只需要访问/dev目录下创建的那个文件即可。
63.7 自动创建设备节点
加载驱动模块后,需用mknod命令创建设备节点,但这样比较麻烦,调试时每次加载驱动都需要手动创建。Linux 2.6内核以后引入了设备文件管理模块udev(udev是一个用户程序),要使驱动能在加载时自动创建设备节点,需要在驱动中先调用class_create函数创建一个class,这个class挂载在sysfs上;然后调用
device_create函数,这个函数指定了上面创建的class、设备号和设备节点名称,当驱动加载时,用户空间的udev会响应device_create,去sysfs下寻找到对应的class并创建设备节点。
class_create和device_create函数定义在/linux/device.h中,原型声明如下:


其中class_create函数返回值为class结构体指针,这个指针需要传递给device_create函数的*cls参数。
device_create函数中的*parent参数为父节点,一般都没有,填NULL、devt是设备号(主和次都在里面)、*drvdata和*fmt用于填写节点名称,类似printf中的字符串打印用法。
73.8 模块插入/卸载函数的关联
上述的字符设备注册过程需要有一个时间点去执行,而最好的时间点就是在终端中加载驱动模块的时候,同样,使用rmmod命令卸载驱动时也需要执行一个卸载函数。
Linux提供了两个宏来完成驱动模块初始化函数和卸载函数的绑定,如下所示,fpga_init和fpga_exit是初始化和卸载函数的函数指针。

然后驱动编写者需要完成这两个函数的内部功能,如下所示定义了fpga_init函数,__init代表将这个函数存储到.init段中,关于段,将在Linux内存管理中详细说明。

另外需要说明的是,因为Linux本身是个开源操作系统,它的源码都是开放的,并且很大一部分都是各种驱动,所以在自己编写的驱动中需要指定开源协议,常见开源协议有GPL、BSD、MPL等,不加开源协议加载驱动模块时会报错。
RIFFA驱动中指定开源协议如下所示,使用MODULE_LICENSE宏即可:

如果需要,也可以用宏MODULE_DESCRIPTION为驱动添加介绍,用宏MODULE_AUTHOR为驱动添加作者信息,这些信息在编译后可在终端用modinfo命令查看到。
暂无评论哦,快来评论一下吧!
