2. 字符设备驱动基础:file_operations 结构体、cdev 注册、设备号分配与注销
字符设备驱动是嵌入式 Linux 驱动开发中最基础的驱动类型,触摸屏驱动在底层同样遵循字符设备的框架。本章将深入剖析字符设备驱动的三大核心要素:file_operations 结构体、cdev 注册机制以及设备号的分配与注销,为后续与 input 子系统的联动打下坚实基础。
2.1 字符设备驱动核心架构
字符设备驱动的整体工作流程可以概括为:用户空间通过系统调用(open、read、write 等)访问设备文件,VFS(虚拟文件系统)根据设备文件的主设备号找到对应的驱动程序,然后调用驱动中注册的 file_operations 回调函数。下图展示了这一核心架构:
2.2 file_operations 结构体详解
file_operations 是字符设备驱动的核心接口,定义在 <linux/fs.h> 中。它是一个函数指针结构体,每个成员对应一个系统调用。驱动开发者只需实现需要的回调函数,未实现的成员可以设为 NULL,内核会使用默认行为。
| 成员名称 | 对应系统调用 | 功能描述 | 触摸屏驱动常用性 |
|---|---|---|---|
owner |
— | 指向拥有该模块的指针,通常设为 THIS_MODULE |
必须 |
open |
open() |
打开设备时调用,用于初始化硬件、分配资源 | 常用 |
release |
close() |
关闭设备时调用,用于释放资源、关闭硬件 | 常用 |
read |
read() |
从设备读取数据,返回给用户空间 | 常用(读取触摸坐标) |
write |
write() |
向设备写入数据,如发送配置命令 | 较少用 |
unlocked_ioctl |
ioctl() |
设备控制命令,用于设置参数、获取状态 | 常用(校准、模式切换) |
poll |
poll() / select() |
查询设备是否可读写,用于非阻塞 I/O | 常用(事件通知) |
mmap |
mmap() |
将设备内存映射到用户空间 | 较少用 |
static struct file_operations touch_fops = {
.owner = THIS_MODULE,
.open = touch_open,
.release = touch_release,
.read = touch_read,
.poll = touch_poll,
.unlocked_ioctl = touch_ioctl,
};
2.3 设备号分配与注销
设备号是内核识别字符设备的唯一标识,由主设备号(major)和次设备号(minor)组成。主设备号对应驱动程序,次设备号对应同一驱动管理的不同设备实例。
2.3.1 设备号的数据结构
内核使用 dev_t 类型(32 位无符号整数)表示设备号,其中高 12 位为主设备号,低 20 位为次设备号。操作宏定义在 <linux/kdev_t.h>:
| 宏/函数 | 功能 | 示例 |
|---|---|---|
MAJOR(dev) |
从 dev_t 中提取主设备号 | int major = MAJOR(dev); |
MINOR(dev) |
从 dev_t 中提取次设备号 | int minor = MINOR(dev); |
MKDEV(major, minor) |
由主次设备号合成 dev_t | dev_t dev = MKDEV(200, 0); |
2.3.2 静态分配 vs 动态分配
内核提供两种设备号分配方式:
- 静态分配:使用
register_chrdev_region(),需要开发者手动指定主设备号。适用于已知且未被占用的设备号,但容易与已有驱动冲突。 - 动态分配:使用
alloc_chrdev_region(),由内核自动分配未使用的主设备号。推荐用于大多数驱动,避免冲突。
#include <linux/fs.h>
#include <linux/device.h>
static dev_t touch_dev_num; // 全局设备号
static int __init touch_init(void)
{
int ret;
// 动态分配设备号:起始次设备号 0,数量 1,名称 "touch"
ret = alloc_chrdev_region(&touch_dev_num, 0, 1, "touch");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated device number: major=%d, minor=%d\n",
MAJOR(touch_dev_num), MINOR(touch_dev_num));
return 0;
}
static void __exit touch_exit(void)
{
// 注销设备号
unregister_chrdev_region(touch_dev_num, 1);
pr_info("Device number unregistered\n");
}
unregister_chrdev_region() 正确释放,否则下次加载时可能分配失败。
2.4 cdev 结构体注册与注销
cdev 是内核中表示字符设备的核心结构体,定义在 <linux/cdev.h>。它将设备号与 file_operations 绑定在一起,并加入内核的设备链表。
2.4.1 cdev 初始化与注册流程
注册 cdev 需要以下步骤:
- 分配 cdev 对象:使用
cdev_alloc()动态分配,或静态定义后使用cdev_init()初始化。 - 设置 file_operations:将 cdev 的 ops 成员指向已实现的 file_operations 结构体。
- 设置 owner:通常设为
THIS_MODULE,用于模块引用计数。 - 注册到内核:调用
cdev_add(),将 cdev 与设备号关联。
#include <linux/cdev.h>
static struct cdev touch_cdev; // 静态定义 cdev 对象
static int __init touch_init(void)
{
int ret;
dev_t dev_num;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "touch");
if (ret < 0)
return ret;
// 2. 初始化 cdev
cdev_init(&touch_cdev, &touch_fops);
touch_cdev.owner = THIS_MODULE;
// 3. 注册 cdev 到内核
ret = cdev_add(&touch_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
pr_err("Failed to add cdev\n");
return ret;
}
pr_info("Touch device registered: major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
static void __exit touch_exit(void)
{
// 1. 注销 cdev
cdev_del(&touch_cdev);
// 2. 释放设备号
unregister_chrdev_region(touch_dev_num, 1);
pr_info("Touch device unregistered\n");
}
2.4.2 cdev 注册与注销的完整生命周期
下图展示了从设备号分配到 cdev 注册,再到模块卸载时释放的完整流程:
2.5 设备节点自动创建:udev 与 class
仅注册 cdev 还不够,用户空间需要通过设备文件(如 /dev/touch0)访问设备。传统方式使用 mknod 手动创建设备节点,现代 Linux 系统使用 udev 自动管理。驱动开发者需要创建 device class 和 device,udev 会自动在 /dev/ 下生成对应的设备文件。
#include <linux/device.h>
static struct class *touch_class;
static struct device *touch_device;
static int __init touch_init(void)
{
// ... 分配设备号、注册 cdev ...
// 创建设备类
touch_class = class_create(THIS_MODULE, "touch_class");
if (IS_ERR(touch_class)) {
// 错误处理
}
// 创建设备节点 /dev/touch0
touch_device = device_create(touch_class, NULL, dev_num,
NULL, "touch%d", 0);
if (IS_ERR(touch_device)) {
// 错误处理
}
return 0;
}
static void __exit touch_exit(void)
{
// 销毁设备节点
device_destroy(touch_class, dev_num);
// 销毁设备类
class_destroy(touch_class);
cdev_del(&touch_cdev);
unregister_chrdev_region(dev_num, 1);
}
2.6 本章小结
| 核心概念 | 关键函数/结构体 | 注意事项 |
|---|---|---|
| file_operations | struct file_operations |
未使用的回调设为 NULL;owner 必须设为 THIS_MODULE |
| 设备号分配 | alloc_chrdev_region() / register_chrdev_region() |
优先使用动态分配;模块退出时必须释放 |
| cdev 注册 | cdev_init() / cdev_add() |
cdev_add 成功后,设备立即可用;注销时先 cdev_del |
| 设备节点 | class_create() / device_create() |
配合 udev 自动生成 /dev 节点;卸载时需销毁 |