第一百四十章:平台设备
合道期涉及内核源码:
一
林小源沿着 platform 总线的小路,走进了一片特殊的区域。
这里的设备不像 PCI 设备那样插在插槽里,也不像 USB 设备那样通过线缆连接。它们直接长在一块巨大的芯片上——像是芯片的一部分,跟 CPU 共用同一块硅片。
"这些是 platform 设备。"守山老者说,"集成在 SoC 里的设备。UART、I2C 控制器、SPI 控制器、GPIO 控制器——它们不是插上去的,而是出厂就焊在芯片里的。"
一个穿着橙色袍子的修士从芯片表面走了过来。他的袍子上绣着一个 的标志。
"我是 platform 总线的引路人。"修士说,"platform 设备是最常见的设备类型之一——尤其是在嵌入式系统中。手机、路由器、开发板里的大部分设备都是 platform 设备。"
"它们怎么被发现的?"林小源问,"PCI 设备可以通过扫描总线发现,platform 设备呢?"
"通过设备树。"修士说,"platform 设备没有物理总线可以扫描。它们的硬件信息——寄存器地址、中断号、时钟频率——全部记录在设备树中。内核启动时解析设备树,创建对应的 platform_device。"
"更准确地说,platform 是一条伪总线。"修士指着脚下的小路,"它连接的是那些总线基础设施很少、却能被 CPU 直接寻址的设备:SoC 内置控制器、旧式端口设备、外设总线的 host bridge。和 PCI、USB 这种有完整枚举协议的山门不同,platform 设备通常要由板级代码、固件表、设备树或 ACPI 先告诉内核:这里确实有这样一件硬件。"
"所以驱动不负责创造设备?"
"现代模型里不该负责。驱动只注册 ,提供 和 remove();设备枚举在驱动之外完成。老式驱动自己注册 platform_device 的写法还存在,但官方文档已经说了,那是兼容旧硬件探测模型的办法,新代码要尽量避免。"
伪总线初试
platform bus 常用于连接能被 CPU 直接寻址、但缺少完整枚举基础设施的什么设备?
/*
* platform 设备:
*
* struct platform_device:
* name — 设备名
* id — 设备 ID
* resource — 资源(内存、中断)
* dev — device 结构
*
* struct platform_driver:
* probe — 初始化
* remove — 移除
* driver — device_driver
*
* 匹配方式:
* 1. 设备树 compatible
* 2. ACPI ID
* 3. 设备名匹配
*
* 资源:
* IORESOURCE_MEM — 内存资源
* IORESOURCE_IRQ — 中断资源
*/
printf("=== platform 设备 — SoC 集成设备 ===\n\n");
printf("platform 设备:\n");
printf(" 集成在 SoC 中的设备\n");
printf(" UART、I2C、SPI、GPIO\n\n");
printf("--- 设备定义 ---\n");
printf("struct platform_device my_dev = {\n");
printf(" .name = \"my_uart\",\n");
printf(" .id = 0,\n");
printf(" .resource = my_resources,\n");
printf(" .num_resources = 2,\n");
printf("};\n\n");
printf("--- 驱动定义 ---\n");
printf("struct platform_driver my_drv = {\n");
printf(" .probe = my_probe,\n");
printf(" .remove = my_remove,\n");
printf(" .driver = {\n");
printf(" .name = \"my_uart\",\n");
printf(" },\n");
printf("};\n\n");
printf("--- 匹配方式 ---\n");
printf("1. 设备树 compatible:\n");
printf(" compatible = \"vendor,device\"\n\n");
printf("2. ACPI ID:\n");
printf(" acpi_match_table\n\n");
printf("3. 设备名:\n");
printf(" platform_device.name\n\n");
printf("--- 资源获取 ---\n");
printf("struct resource *res;\n");
printf("res = platform_get_resource(dev, IORESOURCE_MEM, 0);\n");
printf("void *regs = ioremap(res->start, size);\n\n");
printf("int irq = platform_get_irq(dev, 0);\n");
printf("request_irq(irq, handler, 0, \"uart\", dev);\n\n");
printf("--- probe 函数 ---\n");
printf("int my_probe(struct platform_device *pdev) {\n");
printf(" // 获取资源\n");
printf(" // 映射寄存器\n");
printf(" // 注册中断\n");
printf(" // 初始化设备\n");
printf(" return 0;\n");
printf("}\n");#include <stdio.h>
/*
* platform 设备:
*
* struct platform_device:
* name — 设备名
* id — 设备 ID
* resource — 资源(内存、中断)
* dev — device 结构
*
* struct platform_driver:
* probe — 初始化
* remove — 移除
* driver — device_driver
*
* 匹配方式:
* 1. 设备树 compatible
* 2. ACPI ID
* 3. 设备名匹配
*
* 资源:
* IORESOURCE_MEM — 内存资源
* IORESOURCE_IRQ — 中断资源
*/
int main() {
printf("=== platform 设备 — SoC 集成设备 ===\n\n");
printf("platform 设备:\n");
printf(" 集成在 SoC 中的设备\n");
printf(" UART、I2C、SPI、GPIO\n\n");
printf("--- 设备定义 ---\n");
printf("struct platform_device my_dev = {\n");
printf(" .name = \"my_uart\",\n");
printf(" .id = 0,\n");
printf(" .resource = my_resources,\n");
printf(" .num_resources = 2,\n");
printf("};\n\n");
printf("--- 驱动定义 ---\n");
printf("struct platform_driver my_drv = {\n");
printf(" .probe = my_probe,\n");
printf(" .remove = my_remove,\n");
printf(" .driver = {\n");
printf(" .name = \"my_uart\",\n");
printf(" },\n");
printf("};\n\n");
printf("--- 匹配方式 ---\n");
printf("1. 设备树 compatible:\n");
printf(" compatible = \"vendor,device\"\n\n");
printf("2. ACPI ID:\n");
printf(" acpi_match_table\n\n");
printf("3. 设备名:\n");
printf(" platform_device.name\n\n");
printf("--- 资源获取 ---\n");
printf("struct resource *res;\n");
printf("res = platform_get_resource(dev, IORESOURCE_MEM, 0);\n");
printf("void *regs = ioremap(res->start, size);\n\n");
printf("int irq = platform_get_irq(dev, 0);\n");
printf("request_irq(irq, handler, 0, \"uart\", dev);\n\n");
printf("--- probe 函数 ---\n");
printf("int my_probe(struct platform_device *pdev) {\n");
printf(" // 获取资源\n");
printf(" // 映射寄存器\n");
printf(" // 注册中断\n");
printf(" // 初始化设备\n");
printf(" return 0;\n");
printf("}\n");
return 0;
}二
"probe 函数怎么知道设备的寄存器地址和中断号?"林小源问。
"通过资源。"修士从袍子里掏出一颗水晶,水晶内部浮现出一个 struct resource 的结构。"每个 platform_device 都带有一组资源——内存资源描述寄存器的物理地址范围,中断资源描述中断号。probe 函数用 获取内存资源,用 获取中断号。"
"这比硬编码地址灵活多了。"林小源说。
"当然。"修士点头,"同一款 UART 控制器,可能在不同 SoC 上的地址不同。如果硬编码地址,每换一块芯片就要改驱动代码。用设备树描述资源,驱动只管调用 ,不管具体地址是什么。"
"资源也不一定够。"修士又取出几枚小符,分别写着 clock、reset、pinctrl、regulator。"平台设备的资源表常有内存和 IRQ,但很多设备还需要时钟、复位线、引脚复用、电源域等板级信息。probe 里不能只拿寄存器就开干,还要检查硬件是否真的存在、时钟是否打开、DMA mask 是否适合、依赖是否准备好。依赖暂时没好,就可能触发 deferred probe。"
"那 devm_ 函数呢?"
"那是 devres。"修士说,"用 devm_ioremap_resource()、devm_request_irq() 这类接口,把资源生命周期绑定到 device 上。probe 失败或驱动解绑时,核心会帮你按顺序释放,少写一半退路代码。"
林小源看着那颗水晶,心中感叹:设备树把硬件信息从代码中分离出来,驱动代码变得通用,同一套驱动可以跑在不同的硬件上。
资源之试
驱动中常用哪类 devres 前缀接口,把资源释放绑定到 device 生命周期?
三
"匹配有三种方式,"林小源说,"设备树 compatible、ACPI ID、设备名。哪个优先?"
修士竖起三根手指,按顺序弯下:"设备树 compatible 优先级最高——如果设备树中有 compatible 字符串,内核优先用它匹配。其次是 ACPI ID——在 x86 平台上,ACPI 表中的设备 ID 会用来匹配。最后是设备名——最简单但最不灵活,直接比较 platform_device.name 和 platform_driver.driver.name。"
"为什么设备树优先?"
"因为设备树是目前最主流的硬件描述方式。"修士说,"ARM、RISC-V 平台几乎全部用设备树。ACPI 主要用在 x86 服务器和笔记本上。设备名匹配是兼容老代码的后备方案。"
林小源点了点头。他想起了之前在设备树章节学到的知识——DTS 编译成 DTB,内核解析 DTB 创建 platform_device。整个流程一气呵成,从硬件描述到设备注册,完全自动化。
"所以写一个 platform 驱动,"林小源总结道,"首先要定义一个 platform_driver 结构体,填好 probe 和 remove,然后在设备树中添加 compatible 字符串。剩下的事——资源获取、寄存器映射、中断注册——全在 probe 里完成。"
"还有绑定时机。"修士补充,"设备注册时,会遍历总线上已有驱动;驱动注册时,也会遍历总线上尚未绑定的设备。匹配成功后 driver core 才调用 。如果用户在 sysfs 写 driver_override,某些总线还能绕过标准匹配,只考虑指定驱动。"
"原来 probe 不是发现,是确认。"
"对。match 只是说'可能是你',probe 才是驱动亲自验明正身。"
修士赞许地点了点头。
绑定之试
driver core 匹配设备和驱动后,会调用驱动的哪个回调完成初始化并确认硬件可用?
道藏笔记
内核启示
platform 设备是 SoC 中集成的设备。
platform 设备的匹配:
- 设备树 compatible
- ACPI ID
- 设备名
- 枚举通常在驱动之外完成
- 需要验证硬件、获取 clock/reset/pinctrl/regulator 等依赖
资源获取:
- — 获取内存资源
- — 获取中断号
devm_*— 资源生命周期绑定 device
probe 函数:
- 获取资源
- 映射寄存器
- 注册中断
- 初始化设备
platform 设备是"集成"的设备——SoC 中的标准组件。
platform 设备之试
平台设备没有自动枚举硬件时,本章主要依靠哪份硬件描述来匹配设备?