4. 设备树匹配机制:compatible属性、of_match_table、设备树与驱动绑定的核心流程
设备树(Device Tree)是嵌入式Linux系统中描述硬件拓扑结构的核心机制。驱动与设备之间的绑定,本质上是通过设备树节点中的compatible属性与驱动中的of_match_table进行字符串匹配完成的。本节将深入剖析这一匹配机制的核心流程与数据结构。
4.1 compatible属性:设备树的“身份证”
每个设备树节点都通过compatible属性向内核声明自己支持的驱动类型。该属性是一个字符串列表,遵循“厂商,型号”的命名规范,优先级从高到低排列。
关键规范:compatible 属性中的第一个字符串通常是最精确的型号,后续字符串为兼容型号。内核匹配时按顺序尝试,直到找到匹配的驱动。
// 设备树中 CAN 控制器节点的典型 compatible 声明
can0: can@fdc00000 {
compatible = "microchip,mcp2515", "bosch,can-controller";
reg = <0x0 0xfdc00000 0x0 0x1000>;
interrupts = <0 42 4>;
clocks = <&can_clk>;
...
};
上述节点中,"microchip,mcp2515"是精确型号,"bosch,can-controller"是通用兼容型号。如果内核没有为MCP2515编写专用驱动,但存在一个通用的CAN控制器驱动,后者仍可绑定该设备。
4.2 of_match_table:驱动的“准入名单”
驱动开发者通过of_match_table结构体数组,向内核声明该驱动能够支持哪些设备。每个表项是一个of_device_id结构,核心字段就是.compatible字符串。
// 驱动中 of_match_table 的定义示例
static const struct of_device_id mcp251x_of_match[] = {
{ .compatible = "microchip,mcp2515", },
{ .compatible = "microchip,mcp2517", },
{ .compatible = "microchip,mcp2518", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mcp251x_of_match);
提示:MODULE_DEVICE_TABLE 宏用于将 of_match_table 导出到模块的符号表中,使得内核在热插拔或模块加载时能够自动进行匹配,无需手动绑定。
4.3 核心匹配流程:从设备树节点到驱动 probe
当内核启动或驱动模块加载时,设备树匹配流程按以下步骤执行:
- 设备节点注册:内核解析设备树(DTB),为每个包含
compatible属性的节点创建struct platform_device。 - 驱动注册:驱动调用
platform_driver_register(),将of_match_table注册到内核的总线驱动模型中。 - 总线匹配:平台总线(platform bus)的
.match函数遍历驱动的of_match_table,与设备节点的compatible属性进行字符串比较。 - 绑定与probe:匹配成功后,内核调用驱动的
.probe()函数,并将设备节点的struct device_node指针传递给驱动。
注意:匹配顺序是“驱动优先”还是“设备优先”取决于注册顺序。通常设备节点在系统启动早期就已注册,驱动模块加载时触发匹配。如果驱动编译进内核,则在设备节点创建时立即匹配。
4.4 匹配流程可视化
下图展示了从设备树节点到驱动probe的完整匹配链路:
4.5 关键数据结构对比
设备树匹配涉及三个核心数据结构,它们的职责与关系如下表所示:
| 数据结构 | 所属层级 | 核心字段 | 作用 |
|---|---|---|---|
struct device_node |
设备树层 | full_name, properties |
表示一个设备树节点,包含所有属性链表 |
struct of_device_id |
驱动层 | .compatible, .data |
驱动支持的设备匹配表项 |
struct platform_driver |
总线驱动层 | .driver.of_match_table |
驱动注册时挂载匹配表,供总线调用 |
4.6 匹配过程中的关键函数调用链
从驱动注册到probe调用,内核内部执行以下函数链:
platform_driver_register()
└─> driver_register()
└─> bus_add_driver()
└─> driver_attach()
└─> bus_for_each_dev()
└─> __driver_attach()
└─> driver_match_device()
└─> platform_match()
└─> of_match_device()
└─> of_match_node()
└─> __of_match_node()
└─> 字符串比较 compatible
核心函数解析:
of_match_device() 遍历驱动的 of_match_table,对每个表项调用 of_match_node(),后者从设备节点的 compatible 属性中提取字符串列表,与表项进行精确字符串匹配。匹配成功则返回对应的 of_device_id 指针,驱动可通过 .data 字段获取私有数据。
4.7 实际案例:CAN驱动绑定过程
以MCP2515 CAN控制器为例,设备树节点与驱动的匹配过程如下:
- 设备树节点:包含
compatible = "microchip,mcp2515",以及reg、interrupts、clocks等资源描述。 - 驱动注册:
mcp251x_can_driver注册时携带mcp251x_of_match[]表,其中包含"microchip,mcp2515"。 - 匹配触发:平台总线调用
platform_match(),通过of_match_device()找到匹配项。 - probe执行:内核调用
mcp251x_can_probe(),驱动通过devm_platform_ioremap_resource()获取寄存器地址,通过platform_get_irq()获取中断号,完成CAN控制器的初始化与注册。
// 驱动 probe 函数中获取设备树资源的典型代码
static int mcp251x_can_probe(struct platform_device *pdev)
{
struct net_device *net;
struct mcp251x_priv *priv;
void __iomem *base;
int irq, ret;
// 获取寄存器基地址(来自 reg 属性)
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
// 获取中断号(来自 interrupts 属性)
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 获取时钟(来自 clocks 属性)
priv->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(priv->clk))
return PTR_ERR(priv->clk);
// 注册 CAN 网络设备
ret = register_candev(net);
...
}
最佳实践:驱动开发者应优先使用
devm_* 系列API(如 devm_platform_ioremap_resource)获取设备树资源,这些API自动管理资源生命周期,在驱动卸载或probe失败时自动释放,避免内存泄漏。
4.8 匹配失败时的回退机制
如果 of_match_table 匹配失败,平台总线还会尝试其他匹配方式:
- ACPI匹配:在支持ACPI的平台上,尝试使用ACPI表进行匹配。
- ID表匹配:如果驱动提供了
id_table(传统平台驱动),则进行名称匹配。 - 名称回退:将设备节点的
name属性与驱动名称进行字符串比较(已废弃,仅用于兼容旧驱动)。
注意:对于现代嵌入式Linux系统,强烈建议仅使用
of_match_table 进行匹配。依赖名称回退的方式不可靠,且已被内核社区标记为过时。
通过上述机制,设备树匹配实现了硬件描述与驱动代码的解耦:硬件工程师修改设备树即可适配不同外设,驱动开发者只需关注 of_match_table 的声明与 probe 函数的实现,双方通过 compatible 这一契约完成协同工作。