Linux字符设备驱动框架
字符设备
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
字符设备驱动工作流程
加载/卸载驱动模块
Linux的驱动有两种工作方式:
- 驱动编译进kernel,kernel启动时自动运行驱动程序
- 驱动编译为模块(
.ko),kernel启动后调用insmod加载驱动模块
模块有加载和卸载两种操作:
1 | |
module_init用于向Kernel注册一个模块加载函数,当insmod时,该函数就会被Kernel调用。
字符设备驱动的模块加载和卸载模板如下:
1 | |
驱动编译完成后,Kernel有两种命令用于加载驱动模块:insmod和modprobe:
insmod是最简单的模块加载命令,用于加载指定的.ko模块,比如insmod drv.ko。但insmod不能解决模块的依赖关系。modprobe可以自动分析模块的依赖关系,然后加载所有的依赖模块到Kernel中。
模块卸载则使用rmmod或modprobe -r。
推荐:加载使用modprobe,卸载使用rmmod。
注册与注销
模块加载成功后需要注册字符设备,卸载模块时也需要注销相关字符设备:
1 | |
register_chrdev用于注册字符设备,形参有:
major:主设备号name:设备名fops:结构体file_operations类型指针,指向操作函数集合变量
unregister_chrdev用于注册字符设备,形参有:
major:主设备号name:设备名
一半字符设备的注册在入口函数中进行,注销在出口函数中执行:
1 | |
终端中car /proc/devices可查看当前已被使用的设备号
实现设备的具体操作函数
在对test_fops进行初始化之前,首先要分析需求,即chrtest这个设备需要实现哪些功能:
- 对
chrtest能够进行打开和关闭操作。这是最基本的要求 - 对
chrtest进行读写操作。假设设备控制着一段缓冲区(内存),应用需要通过read和write对缓冲区进行读写操作,因此需要实现file_operations中的read和write两个函数。
修改代码:
1 | |
这里的函数形参需要使用Linux驱动框架所规定的标准格式:
struct inode *inode:指向设备文件的 索引节点,描述了文件的元数据(如权限、文件类型等)struct file *filp:表示 已打开的文件,包含文件的当前状态、访问模式等信息。char __user *buf:用户空间的缓冲区,内核需要将数据拷贝到这个缓冲区size_t cnt:写入的字节数,表示用户请求写入的长度loff_t *offt:文件偏移量,用于支持文件的随机访问
添加LICENSE和作者信息
LICENSE不添加编译会报错:
1 | |
设备号分配
静态分配
WIP
动态分配
推荐使用动态分配设备号:
1 | |
dev:保存申请到的设备号baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。count:要申请的设备号数量name:设备名
释放设备号函数:
1 | |
from:要释放的设备号count:从from开始要释放的设备号数量
cdev
cdev 是 Linux 内核中用于表示字符设备的一个结构体。它是 Linux 字符设备驱动中非常重要的一部分,主要用于管理字符设备的注册和文件操作的绑定。当注册一个字符设备时,cdev 会帮助内核处理与设备相关的操作。
1 | |
cdev 的主要任务是将文件操作函数(例如:open、read、write 等)和设备进行关联。当设备被打开、读写或释放时,内核会通过 cdev 结构体找到相应的操作函数并调用它们。
cdev应当按照顺序被使用:
- 初始化
1 | |
这个操作可以通过cdev_alloc()来一次性完成:
1 | |
- 注册字符设备:使用
cdev_add函数将其注册到内核。这样,内核就能够在收到对该设备的请求时,调用对应的文件操作函数。
1 | |
- 注销字符设备
1 | |
其他
驱动中还会用到copy_to_user函数:
1 | |
该函数用于将内核空间中的数据复制到用户空间。
测试APP
C库文件操作
open()
1 | |
pathname:要打开的设备或者文件名flags:文件打开模式,以下三种模式必选其一:O_RDONLY只读模式O_WRONLY只写模式O_RDWR读写模式
如果文件打开成功的话返回文件的文件描述符.
read()
1 | |
fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符buf:数据读取到此 buf 中count:要读取的数据长度,也就是字节数
读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
write()
1 | |
fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文件成功以后会得到文件描述符buf:要写入的数据count:要写入的数据长度,也就是字节数
返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回
负值,表示写入失败
close()
1 | |
0 表示关闭成功,负值表示关闭失败。
fops、cdev、class、device
- 定义 fops,提供 read/write/open/close/ioctl 操作
- 定义 cdev,并绑定 fops
- 注册 cdev,并分配 major/minor 设备号
- 创建 class,供 udev 识别
- 创建 device,在
/dev/下自动生成设备文件
完整流程
- 定义设备信息,用于注册设备至kernel
1 | |
- 定义文件操作结构体,用于封装用于操作外设的功能函数
1 | |
- 实现设备操作函数
1 | |
- 初始化和清理
1 | |
- 创建节点。如果应用程序要想使用设备,还必须创建字符设备节点:
1 | |
然后应用程序才能将设备号和具体的设备连接起来并使用。
如果初始化中创建了device和class类,会自动生成节点。
LED点灯示例
1 | |