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 总线会遍历已注册的另一方,尝试匹配。匹配顺序如下:
- ID 表匹配: 比较 platform_driver.id_table 中的名字与 platform_device.name
- 名字直接匹配: 比较 platform_driver.driver.name 与 platform_device.name
- 设备树匹配: 如果启用了 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 总线上相遇,通过匹配机制找到彼此,最终调用 probe 完成绑定。匹配失败也没关系——设备和驱动会继续等待,直到另一方出现。
嗯,这一章的内容就到这里。platform 总线模型是理解 Linux 内核驱动框架的基石,后面的章节会在此基础上展开更复杂的模型。你先把这些基础打牢,后面遇到 SPI、I2C 等总线驱动时,会发现它们都遵循类似的思路。