1. 字符设备驱动基础:从分类到设备号管理
大家好,我是你们这门课的老朋友。今天咱们正式开篇,聊聊字符设备驱动的基础。说实话,我做了十几年嵌入式,接触最多的就是字符设备。它简单、直接,是理解 Linux 驱动模型的绝佳入口。
你想想看,一个驱动工程师如果连字符设备都搞不定,那后面的网络设备、块设备就更别想了。所以这一章,咱们把地基打牢。
1.1 Linux 设备驱动分类:三大门派
Linux 下的设备驱动,说白了就分三类:字符设备、块设备和网络设备。我习惯用一张表来对比它们,清晰明了。
| 类型 | 数据访问方式 | 典型代表 | 关键特征 |
|---|---|---|---|
| 字符设备 | 顺序字节流,无缓冲 | 串口、GPIO、I2C、SPI | 按字节读写,open/read/write/close |
| 块设备 | 随机块访问,有缓冲 | 硬盘、eMMC、U盘 | 以块为单位,支持挂载文件系统 |
| 网络设备 | 数据包收发 | 以太网、WiFi、蓝牙 | 通过 socket 接口,不映射到文件节点 |
嗯,这里要注意:字符设备虽然叫“字符”,但传输的未必是 ASCII 字符。它只是强调数据流是顺序的、没有缓冲的。我在项目中遇到过有人把 SPI Flash 当成字符设备来操作,结果因为没处理页对齐,数据写进去读出来全是乱的。这就是没理解“字符设备无缓冲”这个本质。
1.2 字符设备的核心数据结构:struct cdev
每个字符设备在内核里都有一个“身份证”——struct cdev。这个结构体定义在 <linux/cdev.h> 里。我刚开始学驱动时,总觉得它很神秘,后来发现它其实就干三件事:
- 记录设备号(主设备号 + 次设备号)
- 绑定文件操作函数集(
struct file_operations) - 把自己挂到内核的设备链表上
咱们直接看代码,比空讲强得多:
struct cdev {
struct kobject kobj; // 内核对象,用于 sysfs
struct module *owner; // 所属模块,通常设为 THIS_MODULE
const struct file_operations *ops; // 文件操作函数指针
struct list_head list; // 内核链表节点
dev_t dev; // 设备号(主+次)
unsigned int count; // 次设备号数量
};
我个人习惯在写驱动时,把 struct cdev 嵌入到自己的设备结构体里,而不是单独分配。这样内存管理更干净,不容易泄漏。比如:
struct my_device {
struct cdev cdev;
unsigned long base_addr;
int irq_num;
/* 其他私有数据 */
};
container_of() 宏可以从 struct cdev 指针反推出你的设备结构体。这是内核编程的经典手法,后面实战中会反复用到。
1.3 设备号:主设备号与次设备号
设备号是内核识别设备的“门牌号”。dev_t 类型是一个 32 位无符号整数,高 12 位是主设备号,低 20 位是次设备号。主设备号对应驱动,次设备号对应具体设备实例。
举个例子:你电脑上可能有两个串口 /dev/ttyS0 和 /dev/ttyS1,它们的主设备号都是 4(串口驱动),但次设备号分别是 64 和 65。
我当年犯过一个低级错误:手动指定了主设备号,结果跟系统里已有的驱动冲突了,模块加载直接报 register_chrdev_region: -16(设备或资源忙)。从那以后,我学乖了——能用动态分配就别用静态。
1.4 设备号注册与注销:两种方式
内核提供了两套 API 来管理设备号。我整理成表格,方便你对比记忆:
| 方式 | 函数 | 适用场景 | 注意事项 |
|---|---|---|---|
| 静态分配 | register_chrdev_region() |
已知主设备号(如 240-255 预留范围) | 可能冲突,需查 /proc/devices |
| 动态分配 | alloc_chrdev_region() |
不关心具体主设备号 | 内核自动分配,推荐使用 |
动态分配的代码模板是这样的:
dev_t dev_num;
int major, minor;
/* 动态分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_driver");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("Allocated major=%d, minor=%d\n", major, minor);
注销时一定要成对释放:
unregister_chrdev_region(dev_num, 1);
unregister_chrdev_region(),结果再次加载时动态分配拿到了同一个主设备号,但 cdev_add() 却失败了。内核不会自动帮你清理,必须手动释放。记住:注册和注销要成对出现。
1.5 知识体系总览:一张图看懂
说了这么多,咱们用一张 SVG 图把整个字符设备驱动的框架串起来。这张图我画了很多遍,每次给团队新人培训都用它。
这张图从左到右展示了用户空间到硬件层的完整路径。你注意看中间那三个红色、紫色、绿色的方块——它们就是字符设备驱动的三要素:struct cdev、file_operations 和 dev_t。少了任何一个,驱动都跑不起来。
1.6 本章小结
咱们这一章把字符设备驱动的基础骨架搭起来了。你记住三件事:
- 分类:字符设备、块设备、网络设备,各有各的脾气
- 数据结构:
struct cdev是核心,file_operations是灵魂 - 设备号:动态分配是王道,注册注销要成对