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 |
|