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 操作等。

它们之间的关系,我用一张图来展示:

总线模型核心结构关系图 struct bus_type 总线:负责匹配与绑定 设备链表 (devices) struct device 驱动链表 (drivers) struct device_driver 匹配函数 match() 设备A: I2C传感器 设备B: SPI屏幕 设备C: UART模块 驱动X: sensor_drv 驱动Y: display_drv 驱动Z: uart_drv 匹配成功后 → 调用驱动 probe() 函数

从图里可以看得很清楚:总线是中心,设备和驱动分列两侧。匹配成功后,驱动就能拿到设备的资源信息,开始初始化硬件。

4.5 匹配流程详解

匹配到底是怎么一步步发生的?我拆解一下:

  1. 注册触发:设备或驱动注册到总线时,总线调用 bus->match() 函数。
  2. 遍历链表:如果是设备注册,遍历驱动链表;如果是驱动注册,遍历设备链表。
  3. 调用匹配函数:每种总线实现自己的 match 逻辑。比如 platform 总线会先查设备树,再查 ID 表,最后查名字。
  4. 绑定:匹配成功,将 device 的 driver 指针指向匹配到的 driver,然后调用 driver->probe()。
  5. 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 子系统都会轻松很多。