Linux设备驱动层之设备树匹配流程解析

发布时间:2026/9/11 22:20:26
Linux设备驱动层之设备树匹配流程解析 一、前言设备树是 Linux 驱动开发中绕不开的一环但许多开发者对它停留在“照着模板改”的阶段——知道要写 compatible、要写 reg却不清楚内核究竟如何处理这些节点也不确定一个新设备该在节点里放哪些属性。本文试图理清从设备树节点到驱动 probe 的完整路径。我们将从内核启动开始追踪 DTB 如何被解析成 device_node 树如何被转换为 platform_device 并挂载到总线上以及驱动注册后总线匹配机制如何工作。过程中会涉及 of_platform_default_populate_init() 的核心逻辑也会对比 simple-bus、I2C、SPI 等不同总线对设备树子节点的不同处理方式。理解这条路径的意义在于设备树节点中该写什么属性答案其实就藏在对应总线子系统的解析代码中。 掌握了内核“读”设备树的方式自然就知道该“写”什么。二、从设备树节点到 platform_device2.1 设备树在内核中的启动设备树源文件.dts经过编译生成二进制的 DTBDevice Tree Blob随内核一起加载到内存中。但此时的 DTB 只是一块二进制数据内核需要将它解析成可供驱动使用的结构化数据。这个过程在内核启动早期就开始了。沿着start_kernel()的调用链一路追踪start_kernel() └── setup_arch() // 架构相关的初始化 └── unflatten_device_tree() // 将 DTB 解析成 device_node 树 └── arch_call_rest_init() └── rest_init() └── kernel_init() └── kernel_init_freeable() └── do_basic_setup() └── of_platform_default_populate_init() // 将节点转换为设备注意这是 Linux 内核的第一个 C 语言函数 汇编代码的最后一步就是跳转到 start_kernel() 这里。回忆起过去学习汇编的时候写过一个汇编代码启动STM32MP157裸机最后跳转到我们的主函数Linux系统也是一样。start_kernel函数的目录是init/main.c 。unflatten_device_tree() 的作用是把平坦的 DTB 二进制数据“展开”成一棵由 structdevice_node组成的树形结构。每个设备树节点都对应一个device_node 结构体其中包含了节点的 compatible、reg、interrupts 等属性信息。这一步完成后内核就拥有了设备树的完整内存表示后续所有对设备树的访问都基于这棵 device_node 树进行。2.2 核心转换入口of_platform_default_populate_init()将 device_node 转换为 platform_device 的关键步骤发生在 do_basic_setup() 阶段调用的 of_platform_default_populate_init() 中。// drivers/of/platform.c static int __init of_platform_default_populate_init(void) { struct device_node *root of_find_node_by_path(/); // 扫描根节点下的直接子节点将它们转换为 platform_device of_platform_default_populate(root, NULL, NULL); return 0; }这个函数的核心逻辑可以简化为// 伪代码of_platform_default_populate 的遍历逻辑 void of_platform_default_populate(struct device_node *root) { for each child in root-children: if (child has compatible property): // 为这个节点创建一个 platform_device dev platform_device_alloc(device_name, id); dev-dev.of_node child; // 绑定设备树节点 platform_device_add(dev); // 注册到 platform_bus 上 }遍历完成后根节点下所有带有 compatible 属性的子节点都被转换成了 platform_device并且挂载在 platform_bus 上。2.3 为什么是根节点的“直接子节点”一个容易被忽略的细节是of_platform_default_populate_init() 只遍历根节点的直接子节点而不会递归遍历所有后代节点。这是什么意思呢看一个典型的设备树/ { model Foo Board; compatible foo,board; // 直接子节点 → 会被转换为 platform_device uart1000 { compatible ns16550; reg 0x1000 0x100; }; i2c2000 { compatible i2c-controller; reg 0x2000 0x100; // 子节点 → 不会被这里转换 eeprom50 { compatible atmel,24c02; reg 0x50; }; }; };在这个例子中uart1000和i2c2000是根节点的直接子节点会被转换为platform_deviceeeprom50是i2c2000的子节点不会在这个阶段被转换成platform_device那么 eeprom50 什么时候才会变成设备呢答案是当 i2c2000 对应的 I2C 控制器驱动 probe 时它会调用 I2C 核心的解析函数将子节点转换为 i2c_client。也就是说子节点的转换工作由父节点对应的总线驱动负责而不是由内核统一完成。2.4 一个关键例外simple-bus如果根节点下有一个容器节点我们希望它的所有子节点也被自动转换为 platform_device该怎么做设备树中的 simple-bus 就是为此设计的/ { soc { compatible simple-bus; // 告诉内核我是总线桥 // 这些子节点也会被转换为 platform_device uart1000 { compatible ns16550; }; spi2000 { compatible spi-controller; }; }; };内核在处理 simple-bus 节点时会递归遍历它的子节点并为每个带有 compatible 属性的子节点创建 platform_device。simple-bus 相当于一个“递归信号”告诉内核“请继续往下遍历”。三、驱动注册、总线匹配与 probe 执行机制内核启动完成设备节点解析、设备创建挂载后系统中已经存在完整的设备链表与总线架构但此时设备处于“有设备无驱动”的空闲状态。后续驱动模块加载、总线匹配、设备初始化的完整流程是设备树硬件配置落地、硬件正常工作的核心闭环。本章将从驱动注册流程、总线匹配规则、不同总线差异化处理、probe 执行逻辑四个维度拆解完整工作机制。3.1 驱动模块的注册流程内核设备树机制负责创建设备而驱动模块负责注册驱动二者分离、独立执行最终通过总线完成配对。在嵌入式 Linux 系统中驱动可以以内核内置模块形式编译进内核也可以通过 insmod 命令动态加载两种方式最终都会执行驱动注册逻辑。所有外设驱动都会通过模块入口函数完成初始化注册核心入口为 module_init 宏定义。以平台驱动为例驱动代码的基础结构如下// 驱动模块入口 module_init(my_driver_init); static int __init my_driver_init(void) { // 注册平台驱动到platform总线 return platform_driver_register(my_platform_driver); }驱动注册的完整调用链路层层递进最终将驱动挂载到对应总线的驱动链表中等待设备匹配my_driver_init() └── platform_driver_register(my_driver) └── driver_register(my_driver.driver) └── bus_add_driver() // 将驱动加入对应总线的驱动管理列表注册完成后驱动并不会立即执行初始化逻辑而是被内核总线子系统统一管理。内核会遍历当前总线上所有未匹配的设备逐一进行匹配校验匹配成功后触发后续 probe 流程。3.2 核心总线匹配机制Linux 内核所有设备与驱动的配对都遵循总线统一匹配的设计思想不同总线拥有独立的 match 匹配函数定义专属的匹配规则。其中 platform 总线作为最通用的基础总线匹配逻辑覆盖了设备树开发的绝大多数场景也是理解其他总线匹配机制的基础。platform 总线的匹配函数定义在 drivers/base/platform.c 中内核会严格按照优先级依次尝试四种匹配方式只要任意一种匹配成功即判定设备与驱动适配// drivers/base/platform.c static int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev to_platform_device(dev); struct platform_driver *pdrv to_platform_driver(drv); // 1. 最高优先级设备树OF匹配日常开发主流方式 if (of_driver_match_device(dev, drv)) return 1; // 2. 次优先级ACPI匹配X86设备主流 // 3. 通用ID表匹配 // 4. 最低优先级设备与驱动名称字符串匹配 return 0; }可以清晰看出**设备树OF匹配优先级最高**这也是嵌入式ARM、RISC-V架构设备几乎全部依赖 compatible 属性完成驱动匹配的根本原因。3.2.1 设备树OF匹配核心逻辑of_driver_match_device 是设备树匹配的核心函数核心逻辑极其简单比对设备节点的 compatible 属性与驱动 of_match_table 中的兼容字符串一致则匹配成功。// drivers/of/device.c int of_driver_match_device(struct device *dev, struct device_driver *drv) { struct of_device_id *ids drv-of_match_table; struct device_node *np dev-of_node; // 遍历驱动支持的所有兼容型号 for (; ids-compatible[0]; ids) { // 比对设备树节点与驱动的compatible字符串 if (of_device_is_compatible(np, ids-compatible)) return 1; } return 0; }对应驱动代码中必须定义 of_match_table 匹配表绑定设备树 compatible 字段示例如下static const struct of_device_id my_driver_of_match[] { { .compatible maxim,ds1621 }, { /* 哨兵结束遍历 */ } }; MODULE_DEVICE_TABLE(of, my_driver_of_match);这也印证了前文的核心结论设备树节点必须配置 compatible 属性且字符串必须与驱动匹配表完全对应否则设备与驱动无法配对硬件无法初始化。3.3 不同总线的设备匹配差异化规则前文提到根节点直接子节点、simple-bus子节点会被统一创建为 platform_device而 I2C、SPI 等总线的子节点不会进入 platform 总线体系其设备创建、匹配、初始化由各自的总线子系统独立完成这是 Linux 总线分层设计的关键。3.3.1 platform总线含simple-bus设备来源内核启动阶段 of_platform_default_populate_init() 统一创建匹配规则优先设备树 compatible 匹配适用场景串口、GPIO、时钟、控制器、片上总线容器simple-bus等片上外设。3.3.2 I2C总线设备来源I2C控制器驱动 probe 成功后主动解析自身设备树子节点核心解析逻辑位于 drivers/i2c/i2c-core-of.c总线会主动读取子节点关键属性完成设备注册// drivers/i2c/i2c-core-of.c static struct i2c_client *of_i2c_register_device(struct i2c_adapter *adap, struct device_node *node) { struct i2c_board_info info {}; // 必需属性reg对应I2C设备物理地址 of_property_read_u32(node, reg, addr); info.addr addr; // 匹配属性compatible对应驱动 of_property_read_string(node, compatible, info.name); strlcpy(info.type, info.name, sizeof(info.type)); // 可选属性中断号 info.irq irq_of_parse_and_map(node, 0); // 注册I2C设备 return i2c_new_device(adap, info); }由此可知标准I2C设备节点必须包含compatible和reg两个核心属性中断等属性为可选配置所有属性的定义依据均来自内核I2C总线解析源码。3.3.3 SPI总线SPI总线逻辑与I2C总线完全一致SPI控制器作为platform设备先完成初始化再由SPI总线核心代码解析子节点创建 spi_device 设备通过 compatible 属性匹配对应SPI外设驱动。3.4 匹配成功后的probe执行流程当总线完成设备与驱动的匹配校验、判定适配成功后内核会自动调用驱动中定义的 probe 函数这是硬件初始化的真正入口。probe 函数的核心工作就是读取设备树节点的所有硬件配置信息完成硬件资源初始化。probe 函数的核心工作流程1. 获取设备树节点句柄读取硬件属性reg物理地址、中断号、时钟、电压、自定义配置等2. 解析物理地址完成内存映射得到虚拟地址供驱动操作3. 注册中断、初始化硬件寄存器4. 创建设备文件字符设备、sysfs节点等向上层应用提供操作接口5. 完成硬件自检与初始化设备正式投入工作。整个流程形成完整闭环DTS设备树配置 → 内核解析生成设备 → 驱动注册 → 总线匹配 → probe初始化硬件。四、整体架构总结1. 编译DTS生成DTB内核启动后通过 unflatten_device_tree() 解析为 device_node 树形结构2. of_platform_default_populate_init() 遍历根节点及simple-bus子节点批量创建 platform_device 挂载到platform总线3. I2C/SPI等控制器节点作为platform设备初始化成功后递归解析自身子节点创建专属总线设备i2c_client/spi_device4. 驱动加载后注册到对应总线总线通过compatible属性完成设备与驱动匹配5. 匹配成功触发probe函数读取设备树硬件参数完成硬件初始化与设备注册。归根结底设备树的所有配置规则都由内核总线子系统的解析代码定义读懂内核源码的解析逻辑就能彻底摆脱模板化开发自主完成任意外设的设备树节点配置与驱动开发。