4. platform总线驱动模型:platform_device与platform_driver,匹配机制与资源管理

好,我们进入第四章。这一章要聊的是 platform 总线驱动模型。说实话,这是 Linux 内核里最常用、也最容易被误解的一个模型。我刚开始接触内核驱动时,总觉得它很神秘——为什么要有 platform 总线?直接写驱动不行吗?后来踩过坑才明白,它的存在是为了解决一个很实际的问题:那些不挂在 PCI、USB、I2C 等标准总线上的设备,怎么被内核统一管理?

4.1 为什么需要 platform 总线?

你想想看,一个嵌入式 SoC 里,除了挂在外设总线上的设备,还有很多“内置”的外设——比如 GPIO 控制器、UART、SPI 控制器、定时器。它们直接集成在芯片内部,没有枚举机制,也没有标准的发现协议。内核需要一种方式把这些设备抽象出来,让驱动代码和设备信息解耦。

platform 总线就是干这个的。它是一条虚拟总线,专门用来管理那些“自描述”能力很弱的设备。说白了,它就是一个软件层,把设备信息和驱动代码粘在一起。

核心思想: platform 总线模型将设备信息(platform_device)和驱动逻辑(platform_driver)分离,通过匹配机制动态绑定。这样,同一份驱动代码可以适配不同硬件平台,只需要修改设备信息即可。

4.2 platform_device 与 platform_driver 数据结构

我们先看两个核心结构体。我个人习惯把 platform_device 理解为“硬件的身份证”,而 platform_driver 是“操作这个身份证的流程手册”。

4.2.1 platform_device

struct platform_device {
    const char  *name;        // 设备名称,用于匹配
    int         id;           // 设备编号,-1 表示只有一个
    struct device   dev;      // 内嵌通用 device 结构
    u32         num_resources; // 资源数量
    struct resource *resource; // 资源数组(IO、中断、DMA等)
    const struct platform_device_id *id_entry; // 匹配到的ID表项
    /* ... 其他字段 */
};

这里要注意 resource 字段。我在项目中遇到过好几次,有人把资源定义错了,导致驱动 probe 时获取不到正确的中断号或地址。资源用 struct resource 描述,类型包括:

资源类型 宏定义 说明
内存区域 IORESOURCE_MEM 寄存器地址范围
中断 IORESOURCE_IRQ 中断号
DMA IORESOURCE_DMA DMA通道
IO端口 IORESOURCE_IO x86平台常用

4.2.2 platform_driver

struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t);
    int (*resume)(struct platform_device *);
    struct device_driver driver;  // 内嵌通用驱动结构
    const struct platform_device_id *id_table; // 支持的设备ID表
};

嗯,这里最关键的是 probe 和 remove 回调。当匹配成功时,内核调用 probe;设备移除时调用 remove。我曾经犯过一个低级错误——在 probe 里申请了内存,但 remove 里忘了释放,结果每次热插拔都泄漏一点,系统跑了几天就 OOM 了。

经验之谈: 写 platform 驱动时,probe 和 remove 一定要成对出现。申请了什么资源,remove 里就要释放什么。我建议你在 probe 开头列一个资源清单,remove 里按倒序释放。

4.3 匹配机制:内核如何找到对的驱动?

匹配过程其实不复杂。当内核注册一个 platform_device 或 platform_driver 时,platform 总线会遍历已注册的另一方,尝试匹配。匹配顺序如下:

  1. ID 表匹配: 比较 platform_driver.id_table 中的名字与 platform_device.name
  2. 名字直接匹配: 比较 platform_driver.driver.name 与 platform_device.name
  3. 设备树匹配: 如果启用了 OF(Device Tree),还会比较 compatible 属性

为什么会设计这么多匹配方式?我个人的理解是:为了兼容不同场景。早期内核没有设备树,全靠名字匹配;后来设备树普及了,compatible 匹配更灵活;ID 表则是为了支持同一驱动处理多个设备型号。

注意: 匹配成功后,内核会调用驱动的 probe 函数。如果 probe 返回非零值,匹配失败,驱动不会绑定到设备。我曾经见过一个 case:probe 里因为获取资源失败返回 -ENOMEM,结果驱动一直加载不上,排查了半天才发现是 resource 定义错了。

4.4 资源管理:驱动怎么拿到硬件信息?

匹配只是第一步。驱动真正干活前,得知道硬件的寄存器地址、中断号、时钟频率等信息。这些信息通过 platform_device 的 resource 数组传递。

常用的资源获取 API 有:

// 获取内存资源
struct resource *platform_get_resource(struct platform_device *pdev,
                                       unsigned int type, unsigned int num);

// 获取中断号
int platform_get_irq(struct platform_device *pdev, unsigned int num);

// 获取并映射 IO 内存
void __iomem *devm_platform_ioremap_resource(struct platform_device *pdev,
                                             unsigned int index);

注意看最后一个函数——devm_platform_ioremap_resource。它带 devm_ 前缀,表示是设备资源管理版本。什么意思呢?就是驱动卸载时,内核会自动帮你释放映射。我强烈建议你用这个,而不是手动调用 ioremap 和 iounmap。少写代码,少出错。

4.5 完整示例:一个简单的 platform 驱动

光说不练假把式。我们写一个完整的例子,展示 device 和 driver 如何定义、注册、匹配、使用资源。

4.5.1 定义 platform_device

// 通常在 BSP 板级文件或设备树中定义
static struct resource mydev_resources[] = {
    [0] = {
        .start = 0x10000000,
        .end   = 0x10000FFF,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = IRQ_LINE_42,
        .end   = IRQ_LINE_42,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device my_device = {
    .name = "my-char-device",
    .id   = -1,
    .num_resources = ARRAY_SIZE(mydev_resources),
    .resource = mydev_resources,
};

4.5.2 定义 platform_driver

static int my_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    int irq;

    // 获取并映射 IO 内存
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 获取中断号
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    dev_info(&pdev->dev, "device probed at %p, irq %d\n", base, irq);
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "device removed\n");
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-char-device" },
    { /* sentinel */ }
};

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-char-device",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_driver);

你看,整个流程很清晰:设备提供资源,驱动获取资源,probe 里做初始化。我刚开始写时总觉得 module_platform_driver 这个宏很神奇——它其实展开后就是 module_init 和 module_exit,帮你自动注册和注销驱动。

4.6 避坑指南:我踩过的几个坑

  • 资源索引从 0 开始: platform_get_resource(pdev, IORESOURCE_MEM, 0) 获取第一个 MEM 资源。如果你定义了两个 MEM 资源,第二个要用索引 1。我见过有人一直用索引 0,结果拿到的总是第一个。
  • 设备树 compatible 要匹配: 如果用了设备树,driver 的 of_match_table 必须和设备树节点的 compatible 属性完全一致。大小写、标点都不能错。
  • probe 失败要清理: 如果 probe 中途失败,已经申请的资源要手动释放。用 devm_ 系列函数可以省心,但有些资源(比如注册 misc 设备)没有 devm 版本,得自己处理。
  • 名字冲突: 两个 platform_device 用同一个名字,但 id 不同,内核会创建 name.id 这样的设备名。如果 id 都是 -1,第二个设备注册会失败。

4.7 核心流程总结

为了让你更直观地理解整个流程,我画了一张图:

platform 总线驱动模型核心流程 platform_device_register() 设备加入 platform 总线 platform_driver_register() 驱动加入 platform 总线 匹配机制 ① ID 表匹配 ② 名字直接匹配 ③ 设备树匹配 调用 probe() 获取资源,初始化硬件 匹配失败则 probe 不被调用 驱动和设备各自等待

这张图把整个流程串起来了。设备注册和驱动注册是两条独立的路径,它们在 platform 总线上相遇,通过匹配机制找到彼此,最终调用 probe 完成绑定。匹配失败也没关系——设备和驱动会继续等待,直到另一方出现。

嗯,这一章的内容就到这里。platform 总线模型是理解 Linux 内核驱动框架的基石,后面的章节会在此基础上展开更复杂的模型。你先把这些基础打牢,后面遇到 SPI、I2C 等总线驱动时,会发现它们都遵循类似的思路。