4、总线与设备分离:总线模型概念、设备链表管理、驱动匹配机制
好,咱们进入第四章。这一章要聊的东西,是嵌入式驱动开发里一个非常核心的思想转变——总线与设备分离。
我记得刚入行那会儿,写驱动还是“大泥球”风格。一个驱动文件里,硬件地址、中断号、寄存器操作全揉在一起。换个板子?重写。换个引脚?改代码。那叫一个痛苦。
后来接触了 Linux 的设备驱动模型,才恍然大悟——原来驱动可以写得这么清爽。说白了,就是把“硬件是什么”和“怎么操作它”彻底分开。这就是总线模型要做的事。
4.1 为什么需要总线模型?
你想想看,一个嵌入式系统里有多少外设?I2C 的传感器、SPI 的屏幕、UART 的 GPS 模块……如果每个驱动都直接硬编码资源信息,那代码的复用性几乎为零。
总线模型的核心思路就一句话:设备只描述自己“是什么”,驱动只描述自己“能干什么”,总线负责牵线搭桥。
核心三要素:
- 设备(Device):描述硬件资源(基地址、中断号、时钟频率等)
- 驱动(Driver):描述操作逻辑(probe、remove、suspend、resume 等)
- 总线(Bus):管理设备和驱动的注册、匹配、绑定
我在一个项目里遇到过这样的情况:同一颗加速度传感器,用在两款不同主控的板子上。按照老办法,得写两份驱动。用了总线模型后,只需要写一份驱动,两份设备描述,总线自动帮我把它们配对。省下来的时间,够喝好几杯咖啡了。
4.2 设备链表管理
设备和驱动怎么管理?答案就是——链表。
内核里维护了两条核心链表:
- 设备链表:挂载所有注册进来的 device 结构体
- 驱动链表:挂载所有注册进来的 driver 结构体
每条总线都有自己的链表。比如 I2C 总线维护一个 i2c_bus_type,它下面挂着所有 I2C 设备和 I2C 驱动。
我习惯把这两条链表想象成两个“相亲池”。设备池里是待嫁的姑娘(硬件资源),驱动池里是求偶的小伙(操作逻辑)。总线就是那个媒人,负责在两边牵线。
小技巧:调试时可以用 ls /sys/bus/<bus_name>/devices/ 和 ls /sys/bus/<bus_name>/drivers/ 查看当前系统里有哪些设备和驱动。这是排查匹配问题的第一站。
来看一个简化的设备注册流程:
// 设备端
struct platform_device my_device = {
.name = "my-sensor",
.id = -1,
.num_resources = 2,
.resource = my_resources,
};
platform_device_register(&my_device);
// 驱动端
struct platform_driver my_driver = {
.driver = {
.name = "my-sensor",
.of_match_table = my_of_match,
},
.probe = my_probe,
.remove = my_remove,
};
platform_driver_register(&my_driver);
注册完成后,设备和驱动就分别挂到了 platform_bus_type 的设备和驱动链表上。接下来,就轮到匹配机制上场了。
4.3 驱动匹配机制
匹配机制,说白了就是总线怎么判断“这个驱动能管这个设备”。
不同的总线有不同的匹配策略。我整理了一个表格,方便你对比:
| 总线类型 | 匹配方式 | 匹配优先级 | 典型场景 |
|---|---|---|---|
| Platform 总线 | 设备树 compatible 匹配 / 名字匹配 / ID 表匹配 | 设备树 > ID 表 > 名字 | SoC 内部外设(GPIO、UART、I2C 控制器) |
| I2C 总线 | 设备地址匹配 + 驱动 ID 表 | ID 表精确匹配 | I2C 传感器、EEPROM |
| SPI 总线 | 设备名称匹配 + 驱动 ID 表 | ID 表精确匹配 | SPI 屏幕、ADC、Flash |
| USB 总线 | VID/PID 匹配 / 类匹配 | VID/PID > 类匹配 | USB 鼠标、键盘、存储设备 |
匹配成功后,总线会调用驱动的 probe 函数。probe 函数里,驱动拿到设备传过来的资源信息,开始干活。
我曾经踩过一个坑:设备树里 compatible 属性写成了 "my,sensor",驱动里 of_match_table 写的是 "my-sensor"。就差一个斜杠,匹配死活不成功。查了两天才发现。所以,字符串必须一字不差,包括大小写和标点。
匹配失败怎么办?别急,总线不会放弃。每次有新的设备或驱动注册进来,总线都会重新扫描一遍链表,看看有没有新配对成功的。这就是所谓的动态匹配。
4.4 核心数据结构
理解总线模型,必须搞懂几个关键结构体。我挑最重要的三个说:
- struct bus_type:总线的抽象。包含总线名称、匹配函数、uevent 处理等。
- struct device:设备的抽象。包含父设备、总线、资源列表、电源管理信息等。
- struct device_driver:驱动的抽象。包含名称、总线、probe/remove 回调、PM 操作等。
它们之间的关系,我用一张图来展示:
从图里可以看得很清楚:总线是中心,设备和驱动分列两侧。匹配成功后,驱动就能拿到设备的资源信息,开始初始化硬件。
4.5 匹配流程详解
匹配到底是怎么一步步发生的?我拆解一下:
- 注册触发:设备或驱动注册到总线时,总线调用
bus->match()函数。 - 遍历链表:如果是设备注册,遍历驱动链表;如果是驱动注册,遍历设备链表。
- 调用匹配函数:每种总线实现自己的 match 逻辑。比如 platform 总线会先查设备树,再查 ID 表,最后查名字。
- 绑定:匹配成功,将 device 的 driver 指针指向匹配到的 driver,然后调用 driver->probe()。
- probe 执行:驱动在 probe 里获取资源、注册中断、初始化硬件、创建设备文件。
调试小技巧:如果 probe 没被调用,先检查设备和驱动是否注册到了同一条总线。可以用 cat /sys/bus/platform/devices/ 和 cat /sys/bus/platform/drivers/ 确认。我每次排查匹配问题,第一件事就是看这两个目录。
4.6 实际项目中的经验
最后分享几个我在项目中积累的经验:
- 设备树优先:新项目尽量用设备树描述硬件,不要用 platform_device 硬编码。设备树让硬件描述和驱动代码彻底解耦。
- 命名规范:设备名、驱动名、compatible 字符串要保持一致。我习惯用 "厂商,设备名" 的格式,比如 "ti,ads1115"。
- 资源获取:probe 函数里用
platform_get_resource()或device_property_read_*()获取资源,不要直接硬编码地址。 - 错误处理:probe 失败时,记得释放已申请的资源。否则下次 probe 可能会因为资源泄漏而失败。
我曾经犯过一个低级错误:在 probe 里申请了 GPIO,但 probe 中途失败返回了 -ENOMEM,忘了释放 GPIO。结果下次 insmod 时 GPIO 被占用,probe 直接返回 -EBUSY。从那以后,我写 probe 都会先用 goto 语句做好错误处理路径。
总线与设备分离,说白了就是让驱动更干净、更可复用。你只要把“硬件是什么”和“怎么操作它”分开写,剩下的交给总线去配对。这个思想贯穿整个 Linux 驱动模型,搞懂了它,后面学 I2C、SPI、USB 子系统都会轻松很多。