一、驱动结构
如上篇博客所说驱动开发关注的是两个重要的结构体:file_operations
、inode
,而对字符驱动设备来讲,我们关注的是 inode
中的 cdev
结构体和 file_operations
中的相关的操作函数。cdev
结构体的定义如下:
其中的 dev_t
成员定义了设备号,我们可以使用相应的宏从中获取主次设备号,或者使用相应的设备号生成 dev_t
:
操作 cdev
结构体的函数如下所示:
在设备注册和注销前,还需要向系统申请注册和释放设备号:
二、加载和卸载
字符设备驱动模块加载函数中应实现设备号的申请和 cdev 的注册,在卸载函数中应实现设备号的释放和 cdev 注销。相应的代码如下:
三、file_operations
结构体函数
字符设备驱动通常要实现字符设备 file_operations
结构体中的读写函数,相应的结构体模板是:
我们需要将我们实现的操作函数在 file_operations
结构体中指定其实现函数。相应的实现函数模板是:
分别介绍写通常要使用的实现函数:
3.1 打开函数
int xxx_open(struct inode *inode, struct file *filp)
打开函数中给驱动初始化,通常我们应实现以下工作:
- 检查设备的错误
- 如果设备初次打开,对其初始化
- 如果必要,更新操作函数指针
- 分配私有数据指针
inode
参数指的是我们打开设备的文件结构体,其中包含了我们的字符设备cdev结构体指针。如上文所说,我们通常把该设备涉及到的cdev、私有数据及信号量等信息全包含进设备结构体中。这里有个技巧可以从inode
指针中获取我们的设备结构体,使用宏container_of
,使用该宏可以从结构体成员指针找到对应的结构体指针:container_of(pointer, container_type, container_field)
其中,pointer
是我们已知的指针,这里我们可以知道设备结构体中的cdev
成员的指针为inode->i_cdev
;container_type
是我们想要获取的结构体类型,我们这里想要获取的结构体是自定义的设备结构体xxx_dev_t
container_field
是pointer
的类型,这里cdev
的类型是cdev
。
这样,该宏就返回了我们想要的自定义的设备结构体指针,使用该指针可以很方便的找到我们的私有数据指针等和设备相关的信息。
3.2 关闭函数
int xxx_release(struct inode *inode, struct file *filp)
关闭函数和打开函数相反,完成了以下的工作:
- 释放由
open
分配的,保存在filp->private_data
中的所有数据。 - 在最后一次关闭操作的时候关闭设备。
3.3 读写函数
在设备读写函数中,filp
是文件结构体指针,buf
是用户空间内存地址,count
是要读写的字节数,f_ops
是读写的位置偏移量。
因为内核空间和用户空间不能直接互相访问,所以需要在读写函数中实现用户空间到内核空间的复制。使用下述两个函数:
上述函数返回不能复制的字节数,如果完全复制成功,返回0。 __user
是一个宏,表明后面的指针指向用户空间。
3.4 ioctl函数
ioctl 是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。在 xxx_ioctl
函数中的 cmd
参数我们可以自己来直接指定,它可能仅仅是一个整数集,但 Linux 中的 ioctl 命令号都是有特定含义的,因此通常我们不么做。Linux 建议以 设备类型/幻数(8bit)+序列号(8bit)+方向(2bit)+数据尺寸(13/14bit)
的方式来定义 cmd
。
Linux 内核提供了相应的宏来自动生成 ioctl 命令号:
上面的命令已经定义了方向,我们要传的是幻数(type)、序号(nr)和大小(size)。在这里szie的参数只需要填参数的类型,如int,上面的命令就会帮你检测类型的正确然后赋值 sizeof(int)。
有生成cmd的命令就必有拆分cmd的命令:
3.5 定位函数
static loff_t xxx_llseek(struct file *filp, loff_t offset, int orig)
通过对 orig
的判断可以将起始地址设为文件开头(SEEK_SET, 0)、当前位置(SEELK_CUL,1)和文件尾(SEEK_END,2)。