3. 设备树与平台设备
在 Linux 设备树驱动适配工程中,设备树负责“描述硬件”,平台设备驱动负责“驱动硬件”,而两者之间的桥梁就是标准属性与匹配机制。理解 compatible、reg、interrupts、clocks、gpios 等属性的语义,以及 platform_driver 如何与设备树节点完成匹配,是后续所有适配工作的基础。本节从属性含义、匹配流程、典型场景和常见误区四个层面展开。
一、核心原理:设备树节点如何变成平台设备
内核启动时,会解析 DTB 中的每一个设备节点。对于位于总线(如 simple-bus、platform-bus)下、且 compatible 属性存在的节点,内核会将其转换为 platform_device,并把节点中的标准属性翻译成资源:reg 变成 IORESOURCE_MEM,interrupts 变成 IORESOURCE_IRQ,clocks、gpios 等则保留在节点中由驱动通过 of_* 接口按需获取。驱动侧注册 platform_driver 后,内核用 of_device_id 表中的 compatible 字符串与设备节点逐一比对,匹配成功即调用驱动的 probe 函数。
二、标准属性语义与驱动读取接口
下面这张图展示了设备树节点中的标准属性如何被内核翻译,并最终被平台驱动通过对应 API 读取。图中左侧是节点属性,中间是内核翻译结果,右侧是驱动常用接口。
各属性的语义与读取方式如下表所示:
| 属性 | 含义 | 驱动读取接口 | 常见错误 |
|---|---|---|---|
| compatible | 匹配键,格式为 "厂商,型号" | of_device_id 表 / of_match_device() | 字符串拼写不一致、缺少厂商前缀 |
| reg | 寄存器地址与长度 | platform_get_resource() + devm_ioremap_resource() | 地址与父节点 ranges 不匹配 |
| interrupts | 中断号与触发类型 | platform_get_irq() / devm_request_irq() | 中断父节点未指定或类型写错 |
| clocks | 时钟句柄列表 | devm_clk_get() + clk_prepare_enable() | clock-names 与驱动请求名不一致 |
| gpios | GPIO 句柄与标志 | devm_gpiod_get() / gpiod_set_value() | 未配置 pinctrl 导致引脚复用错误 |
三、匹配机制与 probe 流程
平台设备与驱动的匹配由内核总线层完成,核心是 of_device_id 表中的 compatible 与设备节点 compatible 的比对。匹配成功后进入 probe,典型流程如下:
- 驱动注册时提供
of_device_id匹配表,并设置.compatible字段。 - 内核遍历平台设备,调用
of_match_device()比对 compatible。 - 匹配成功,调用驱动的
probe(),传入platform_device。 - 在 probe 中依次获取 reg、interrupts、clocks、gpios 等资源。
- 初始化硬件、注册字符设备或子系统接口,返回 0 表示成功。
static const struct of_device_id demo_of_match[] = {
{ .compatible = "vendor,demo-uart" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);
static int demo_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
int irq;
struct clk *clk;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
clk = devm_clk_get(&pdev->dev, "baud");
if (IS_ERR(clk))
return PTR_ERR(clk);
clk_prepare_enable(clk);
return 0;
}
static struct platform_driver demo_driver = {
.probe = demo_probe,
.driver = {
.name = "demo-uart",
.of_match_table = demo_of_match,
},
};
module_platform_driver(demo_driver);
四、典型场景:一块板适配多颗芯片
假设同一份驱动要支持 A 厂和 B 厂两颗 UART 控制器,寄存器布局相同但时钟名不同。设备树中分别写:
/* A 厂节点 */
uart0: serial@10000000 {
compatible = "vendorA,uart-v1";
reg = <0x10000000 0x1000>;
interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_uart0>;
clock-names = "baud";
};
/* B 厂节点 */
uart1: serial@10001000 {
compatible = "vendorB,uart-v2";
reg = <0x10001000 0x1000>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_uart1>;
clock-names = "baud";
};
驱动只需在 of_device_id 表中同时列出两个 compatible,probe 中统一用 devm_clk_get(&pdev->dev, "baud") 获取时钟,即可实现一份驱动适配两颗芯片。若 B 厂时钟名不同,可在驱动中根据 of_device_get_match_data() 返回的私有数据区分处理,而不是在 probe 中硬编码判断。
devm_* 系列接口管理资源,内核会在设备卸载时自动释放,避免手动 goto 清理带来的遗漏。五、误区与复盘
- compatible 写成通用字符串:如只写 "uart",会与其它驱动冲突,必须使用 "厂商,型号" 格式。
- reg 地址与 ranges 不匹配:子节点地址需经过父节点 ranges 转换,否则 ioremap 会失败。
- interrupts 缺少 interrupt-parent:若父节点未继承,需在节点中显式指定中断控制器。
- clocks 与 clock-names 顺序不一致:驱动按名字获取时,名字必须与 clock-names 完全一致。
- gpios 未配 pinctrl:GPIO 能申请成功但引脚功能未切换,表现为电平无变化。
- probe 返回错误码被忽略:应使用
PTR_ERR()和IS_ERR()判断,返回负值让内核延迟重试。
/sys/firmware/devicetree/base/ 下节点是否存在,再用 dmesg | grep -i "demo" 观察 probe 是否被调用。若节点存在但 probe 未执行,多半是 compatible 不匹配或驱动未编译进内核。掌握标准属性与匹配机制后,后续的 pinctrl、regulator、DMA 等属性适配都可以沿用同一套“节点描述—内核翻译—驱动读取”的思路,做到举一反三。