4、驱动开发基础:字符设备驱动框架、platform总线驱动模型、设备树与驱动匹配机制
驱动开发,说白了就是让操作系统认识你的硬件。座舱系统里那么多外设——触摸屏、摄像头、音频编解码器、CAN控制器——每个都需要驱动来“翻译”硬件信号。我做了这么多年嵌入式,见过太多硬件调不通、驱动加载失败的问题,根源往往就是没搞懂这三个基础:字符设备框架、platform总线、设备树匹配。
这一章,咱们就把这三块骨头啃下来。
4.1 字符设备驱动框架
Linux驱动分三大类:字符设备、块设备、网络设备。座舱里90%的外设都是字符设备——数据按字节流读写,像串口、I2C、SPI、GPIO都是。我刚开始写驱动时,第一个练手的就是字符设备,因为它结构最清晰,也最容易理解内核的“文件即设备”思想。
4.1.1 核心数据结构:file_operations
字符设备驱动的灵魂,就是 file_operations 结构体。它定义了设备文件能支持哪些操作:
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
每个成员都是一个函数指针。用户空间调用 open()、read()、write() 时,内核会通过这个结构体找到对应的驱动函数。嗯,这里要注意:.owner 必须赋值为 THIS_MODULE,防止模块被卸载时还有进程在使用它。
4.1.2 设备号与注册流程
每个字符设备在内核里都有一个唯一的设备号——主设备号 + 次设备号。主设备号标识驱动类型,次设备号标识具体设备。
| 步骤 | 函数/宏 | 说明 |
|---|---|---|
| 1 | alloc_chrdev_region() |
动态分配设备号(推荐,避免冲突) |
| 2 | cdev_init() |
初始化 cdev 结构体,绑定 file_operations |
| 3 | cdev_add() |
向内核注册字符设备 |
| 4 | class_create() |
创建设备类(自动生成 /sys/class 目录) |
| 5 | device_create() |
创建设备节点(自动生成 /dev 下的文件) |
我个人习惯用动态分配设备号,省心。以前在项目里手动指定主设备号,结果跟另一个模块冲突了,调试了一下午才发现——从那以后我再也不偷懒了。
关键点:字符设备驱动的本质就是“注册 + 回调”。你在模块初始化时注册设备号、绑定操作函数,内核在用户访问设备文件时回调你的函数。就这么简单。
4.1.3 完整的驱动骨架
static int __init my_driver_init(void)
{
// 1. 分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
// 2. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 注册字符设备
cdev_add(&my_cdev, dev_num, 1);
// 4. 创建类与设备节点
my_class = class_create(THIS_MODULE, "my_class");
device_create(my_class, NULL, dev_num, NULL, "my_device");
return 0;
}
static void __exit my_driver_exit(void)
{
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
这段代码是所有字符设备驱动的模板。你写座舱里的GPIO按键驱动、LED驱动、甚至简单的传感器驱动,都可以直接套这个架子。
4.2 platform总线驱动模型
字符设备框架解决了“怎么操作设备”的问题,但没解决“设备在哪里、怎么找到它”的问题。在嵌入式系统里,很多设备不挂在PCIe、USB这类可枚举的总线上,而是直接集成在SoC内部——比如I2C控制器、SPI控制器、DMA控制器。它们怎么被驱动发现?
答案就是 platform总线。说白了,它是内核虚拟出来的一条总线,专门用来管理那些“没有真实总线”的设备。
4.2.1 总线、设备、驱动
platform总线模型有三个核心角色:
- platform_bus:虚拟总线,负责匹配设备和驱动
- platform_device:描述硬件资源(地址、中断、DMA通道等)
- platform_driver:实现驱动的 probe/remove 逻辑
匹配成功后,内核自动调用驱动的 probe() 函数。你在 probe 里做硬件初始化、注册字符设备、申请中断——所有“干活”的代码都放在这里。
我的经验:写platform驱动时,一定要把硬件资源(寄存器基址、中断号)从 device 里拿,不要硬编码。这样同一个驱动可以支持多个同类型设备,只需要改设备树就行。
4.2.2 platform_driver 结构
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
.of_match_table = my_of_match,
},
};
module_platform_driver(my_platform_driver);
module_platform_driver() 是个宏,它帮你把 module_init 和 module_exit 都包好了。我刚开始用的时候还自己写 init/exit,后来发现这个宏更干净。
4.3 设备树与驱动匹配机制
设备树(Device Tree)是座舱系统开发里绕不开的东西。它用文本文件描述硬件拓扑结构,内核在启动时解析它,自动创建设备节点。你想想看,如果没有设备树,每换一个硬件平台就得重新编译内核——那得多痛苦?
4.3.1 设备树的基本语法
一个典型的设备树节点长这样:
/ {
soc {
i2c@ff110000 {
compatible = "vendor,i2c-controller";
reg = <0xff110000 0x1000>;
interrupts = <0 56 4>;
clocks = <&clk 12>;
};
};
};
- compatible:匹配字符串,驱动通过它找到设备
- reg:寄存器地址和大小
- interrupts:中断号、类型
- clocks:时钟源
4.3.2 匹配机制:compatible 的优先级
驱动和设备树怎么配对?核心就是 of_match_table:
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,i2c-controller", },
{ .compatible = "vendor,i2c-controller-v2", },
{ /* sentinel */ }
};
内核匹配时,按以下顺序查找:
- 设备树节点的
compatible属性 - 驱动的
of_match_table列表 - 如果匹配成功,调用
probe()
避坑指南:我曾经在项目里把 compatible 字符串写错了一个字母——"vendor,i2c" 写成了 "vendor,i2c"。内核不会报错,驱动就是不加载。调试了两天才发现是拼写问题。所以,compatible 字符串必须一字不差,包括大小写和标点。
4.3.3 从设备树获取资源
在 probe 函数里,你可以用标准API获取设备树里的资源:
static int my_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
int irq;
// 获取寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
// 获取中断号
irq = platform_get_irq(pdev, 0);
// 注册字符设备...
return 0;
}
devm_ 系列API是 managed 版本,设备移除时自动释放资源。我建议你尽量用这些函数,能省掉不少 cleanup 代码。
4.4 知识结构总览
下面这张图把三者的关系串起来了:
4.5 实战要点总结
写座舱驱动时,记住这几条:
- 字符设备是基础——所有数据交互最终都落到 read/write/ioctl 上
- platform总线是桥梁——它把设备树里的硬件描述和驱动代码连接起来
- 设备树是配置——硬件资源写在 .dts 里,驱动代码里只拿不写
一句话总结:设备树告诉内核“有什么硬件”,platform总线负责“配对”,字符设备驱动负责“干活”。三个环节缺一不可,搞懂了它们,座舱里90%的外设驱动你都能拿下。