u-boot设备模型解析:从board_init_r到驱动绑定与probe实战

发布时间:2026/10/1 15:07:34
u-boot设备模型解析:从board_init_r到驱动绑定与probe实战 1. 从 board_init_r 切入搞懂 u-boot 设备模型到底在搭什么玩过 u-boot 移植的人都有一个共同的体感板子能跑到board_init_r基本就说明串口、时钟、DDR 这些最底层的东西已经活了剩下的就是“把驱动一个个接上电”。但很多人卡就卡在这一步——明明board_init_f阶段串口已经能打印了为什么到了board_init_r还要重新折腾一遍驱动dm_init_and_scan到底扫了什么gd-dm_root这棵设备树是怎么从无到有长出来的这篇东西就是把这层窗户纸捅破。我默认你手里已经有一份能编译、能跑起来的 u-boot 源码也大概知道board_init_r是 C 运行环境的第二阶段入口但对driver model后面统一叫 dm的内部骨架还是一团浆糊。读完你应该能做到三件事第一清楚board_init_r里 dm 初始化的完整调用链第二明白uclass、udevice、driver这三者是怎么绑到一起的第三自己动手往这棵树上挂一个新驱动时知道该在哪一步、改哪个文件、填哪些结构体。先把结论摆前面u-boot 的 dm 不是“运行时动态发现设备”的那种模型它更像是一套编译期登记、运行期绑定的静态骨架。设备节点在U_BOOT_DEVICE或者设备树里声明驱动用U_BOOT_DRIVER注册board_init_r阶段做的事情就是把这堆散落的声明按uclass归类、按of_match匹配、按probe顺序激活。理解了这个“登记-匹配-激活”三段式整个 dm 就没有神秘感了。我见过太多人一上来就去啃drivers/core/device.c几千行代码结果越看越晕。正确的姿势是先抓住board_init_r这个时间锚点看它在什么时机、以什么顺序触发 dm 的初始化再顺着调用栈往下钻。这样你看到的每一行代码都有明确的上下文而不是孤立地读函数。2. board_init_r 里 dm 初始化的完整调用链拆解2.1 先定位 board_init_r 在启动流程中的位置u-boot 的启动分两大阶段board_init_f和board_init_r。前者跑在重定位之前用的是临时栈和只读数据段主要任务是初始化 DDR、串口、定时器这些“能让 C 代码跑起来”的基础设施然后把 u-boot 自身重定位到 RAM 高端最后跳转到board_init_r。board_init_r的签名是void board_init_r(gd_t *new_gd, ulong dest_addr)它拿到的是重定位后的全局数据指针。这个阶段内存已经可用堆malloc也能用了所以 dm 这种需要动态分配udevice结构的框架才有条件展开。你在common/board_r.c里能找到它的定义里面是一长串initr_xxx函数的调用序列dm 相关的就在其中。关键的一点board_init_r里的初始化是有序的顺序错了驱动就会 probe 失败。比如initr_dm必须排在需要 dm 的驱动之前而initr_dm自己又依赖gd和堆已经就绪。这个顺序不是随便排的是无数人踩坑之后定下来的。2.2 initr_dm 到 dm_init_and_scan 的调用链在common/board_r.c里dm 的入口是initr_dm。它的核心就一行ret dm_init_and_scan(false);这个false参数是pre_reloc_only表示“不只扫描重定位前的设备全部都要扫”。在board_init_f阶段其实也调过一次dm_init_and_scan(true)那次只初始化了极少数必须在重定位前就工作的设备比如串口真正的全量扫描留到了board_init_r。dm_init_and_scan在drivers/core/root.c里它干了两件大事dm_init()初始化 dm 的根节点gd-dm_root创建根设备root和根驱动root_driver并把uclass链表、udevice链表这些全局结构准备好。dm_scan()扫描所有已登记的设备和驱动按uclass归类触发匹配和 probe。dm_init里最核心的是调用device_bind_by_name把根设备绑到根驱动上。根设备是个特殊存在它的uclass是UCLASS_ROOT是所有设备的祖先。你可以把它理解成设备树的“树根”后面所有设备都是它的子孙。2.3 dm_scan 到底扫了哪些来源dm_scan不是只扫一个地方它按优先级扫了多个来源顺序如下dm_scan_platdata()扫描用U_BOOT_DEVICE宏静态声明的平台数据设备。这是最老式的方式现在新板子基本不用了但很多老代码还在。dm_scan_fdt()扫描设备树Device Tree。这是现代 u-boot 的主流方式设备节点从.dts编译成.dtb运行时解析。dm_scan_other()板级自定义扫描弱符号函数板子可以覆盖它来手动绑定一些特殊设备。这三个来源扫完之后所有udevice都已经创建并绑定了对应的driver但还没有 probe。probe 是延迟的等到真正有人调用uclass_get_device或者device_probe时才触发。这个“延迟 probe”设计很关键它避免了启动时一次性把所有驱动都初始化节省时间也避免依赖顺序问题。2.4 为什么 dm 初始化要放在这个时间点有人会问为什么不在board_init_f里一次性把 dm 全初始化完答案是内存和依赖。board_init_f阶段堆还没准备好udevice结构需要动态分配没堆就没法搞。而且很多驱动依赖 DDR 初始化完成、时钟树配置好这些在board_init_f早期还没做。放在board_init_r里内存、堆、基础时钟都就绪了驱动 probe 的成功率最高。但也不能太晚因为后面initr_xxx里很多设备比如 MMC、网络、USB都要用 dm 接口去拿设备句柄所以initr_dm必须排在它们前面。这个“不早不晚”的位置就是board_init_r调用序列里精心安排的结果。3. uclass、udevice、driver 三者的绑定关系与核心数据结构3.1 三个结构体各自的职责要理解 dm 骨架必须先把这三个结构体的分工搞清楚。我用一个生活化的类比driver是“工种说明书”udevice是“具体某个工人”uclass是“工种分类”。struct driver描述一类驱动怎么干活。里面有name、id、of_match设备树匹配表、probe、remove、ops操作函数集等。它是编译期就确定的用U_BOOT_DRIVER宏注册到链接段里。struct udevice描述一个具体的设备实例。里面有driver指针、uclass指针、parent指针、platdata平台数据、priv私有数据等。它是运行期动态分配的。struct uclass描述一个设备类别比如UCLASS_GPIO、UCLASS_MMC。它管理同一类设备提供uclass_ops给上层调用。每个 uclass 有一个uclass_driver来描述它的行为。三者的关系是一个driver可以绑定多个udevice比如两个相同的 GPIO 控制器每个udevice属于一个uclassuclass通过uclass_driver提供统一接口。上层代码通常只跟uclass打交道不直接碰driver。3.2 U_BOOT_DRIVER 宏展开后是什么样U_BOOT_DRIVER这个宏是理解 dm 注册机制的钥匙。它展开后大致是这样#define U_BOOT_DRIVER(__name) \ ll_entry_declare(struct driver, __name, driver)ll_entry_declare会把驱动结构体放到一个特殊的链接段.u_boot_list_2_driver_2_xxx里。链接脚本u-boot.lds里有一个.u_boot_list段专门收集这些登记项。运行时dm 框架通过遍历这个段就能拿到所有已注册的驱动不需要任何动态注册调用。这就是“编译期登记”的含义。你写一个驱动只要用了U_BOOT_DRIVER宏它就会自动出现在驱动列表里dm_scan时就能被找到。这个设计非常巧妙避免了手动维护驱动列表的麻烦。3.3 设备树节点如何匹配到 driver设备树里的每个设备节点通过compatible属性和驱动的of_match表匹配。of_match是一个struct udevice_id数组每个元素有compatible字符串和data。匹配过程在driver_check_compatible里就是字符串比较。匹配成功后device_bind_common会创建udevice把driver指针填进去然后调用uclass_bind_device把设备挂到对应 uclass 的链表上。如果这个 uclass 还没创建会先创建 uclass 实例。这里有个细节uclass的创建是懒加载的。第一个属于某 uclass 的设备被绑定时才创建这个 uclass。这样避免了启动时创建一堆用不到的 uclass。3.4 probe 的触发时机与顺序绑定完成不等于驱动工作。probe才是真正初始化硬件的地方。probe 的触发有两种主动 probe上层调用uclass_get_device、device_probe等接口时触发。自动 probe如果设备有DM_FLAG_PRE_RELOC或者 uclass 有DM_UC_FLAG_SEQ_ALIAS等标志会在扫描后自动 probe。probe 顺序遵循“父设备先于子设备”的原则。因为子设备的 probe 往往依赖父设备已经初始化好比如 I2C 从设备依赖 I2C 控制器。dm 框架通过device_probe里的递归逻辑保证这个顺序probe 一个设备前先 probe 它的 parent。注意如果你自己写的驱动 probe 里访问了父设备的资源但父设备还没 probe就会拿到空指针。这是新手最常见的崩溃原因之一。4. 动手搭一个 dm 驱动骨架的完整实操4.1 确定驱动类型和 uclass假设我们要加一个虚拟的“LED 控制器”驱动它控制板子上几个 GPIO 灯。第一步是确定它属于哪个 uclass。如果只是简单 GPIO 操作可以直接用UCLASS_GPIO但如果我们想提供led_on、led_off这种语义化接口就应该定义一个新的 uclass比如UCLASS_LED_CTRL。定义新 uclass 需要写一个uclass_driverUCLASS_DRIVER(led_ctrl) { .id UCLASS_LED_CTRL, .name led_ctrl, .post_bind led_ctrl_post_bind, .per_device_auto sizeof(struct led_ctrl_priv), };per_device_auto指定每个设备自动分配的私有数据大小这样你不用手动 malloc。post_bind在设备绑定后调用适合做一些初始化。4.2 编写 driver 结构体和 opsdriver 结构体是核心static const struct led_ctrl_ops led_ctrl_ops { .on led_ctrl_on, .off led_ctrl_off, }; U_BOOT_DRIVER(led_ctrl_gpio) { .name led_ctrl_gpio, .id UCLASS_LED_CTRL, .of_match led_ctrl_ids, .ops led_ctrl_ops, .probe led_ctrl_probe, .bind led_ctrl_bind, .priv_auto sizeof(struct led_ctrl_priv), };of_match表static const struct udevice_id led_ctrl_ids[] { { .compatible myvendor,led-ctrl }, { } };probe函数里做硬件初始化比如申请 GPIO、配置方向static int led_ctrl_probe(struct udevice *dev) { struct led_ctrl_priv *priv dev_get_priv(dev); int ret; ret gpio_request_by_name(dev, led-gpios, 0, priv-gpio, GPIOD_IS_OUT); if (ret) return ret; return 0; }注意gpio_request_by_name这个调用它内部会去设备树里找led-gpios属性并触发对应 GPIO 控制器的 probe。这就是 dm 的依赖链自动解析。4.3 设备树节点的写法设备树里加节点led_ctrl: led-ctrl0 { compatible myvendor,led-ctrl; led-gpios gpio0 12 GPIO_ACTIVE_HIGH; status okay; };compatible必须和of_match里的字符串完全一致大小写敏感。led-gpios属性会被gpio_request_by_name解析gpio0指向 GPIO 控制器节点。4.4 编译配置的开关别忘了在Kconfig里加配置项并在Makefile里根据配置编译obj-$(CONFIG_LED_CTRL) led_ctrl_gpio.oKconfig里config LED_CTRL bool Enable LED controller driver depends on DM_GPIO help Say Y here to enable the LED controller driver.depends on DM_GPIO很重要因为驱动里用了 GPIO 接口没有这个依赖编译会报错。4.5 验证驱动是否被正确加载编译烧录后在 u-boot 命令行里用dm tree命令查看设备树。你应该能看到led_ctrl这个 uclass 和下面的设备节点。用dm uclass看 uclass 列表用dm dev看设备详情。如果设备没出现先检查compatible是否匹配、Kconfig 是否开启、设备树节点status是否为okay。这三个是最常见的“设备不出现”原因。5. 常见问题与排查技巧实录5.1 设备绑定失败compatible 不匹配最常见的现象是dm tree里看不到你的设备。九成是compatible字符串对不上。设备树里的字符串和of_match里的必须逐字符一致包括厂商前缀和连字符。我见过有人把myvendor,led-ctrl写成myvendor,led_ctrl下划线和连字符混了排查半天。排查方法在driver_check_compatible里加debug打印或者用fdtgrep工具确认设备树里节点的compatible值。5.2 probe 顺序导致的空指针驱动 probe 里访问父设备资源但父设备还没 probe拿到空指针崩溃。典型场景是 I2C 从设备 probe 时访问 I2C 总线但总线控制器还没初始化。dm 框架本身保证父设备先 probe但前提是你的设备在设备树里正确嵌套。如果从设备节点没有放在 I2C 控制器节点下面而是放在根节点下父子关系就断了probe 顺序就乱了。排查方法用dm tree看设备的层级关系确认嵌套正确。5.3 私有数据分配失败priv_auto和platdata_auto没设置或者设置的大小不对导致dev_get_priv返回的指针指向错误位置。这个错误很隐蔽因为不一定立刻崩溃可能只是数据错乱。排查方法确认priv_auto大小和struct xxx_priv的sizeof一致。用dev_get_priv拿到的指针打印一下地址和内容看是否符合预期。5.4 常见问题速查表现象可能原因排查手段设备不出现在 dm treecompatible 不匹配对比设备树和 of_match 字符串probe 时崩溃父设备未 probe检查设备树嵌套层级priv 数据错乱priv_auto 大小不对核对 sizeof 和配置值驱动没编译进去Kconfig 未开启检查 .config 和 Makefileuclass 找不到uclass_driver 未注册确认 UCLASS_DRIVER 宏存在probe 返回 -ENODEV依赖的资源缺失检查 gpio/clk/reset 属性5.5 几个独家避坑技巧第一写新驱动时先用dm tree确认设备节点出现了再写 probe 逻辑。很多人一上来就写一堆 probe 代码结果设备根本没绑定白忙活。第二of_match表最后一定要有一个空元素{ }作为结束标志否则遍历会越界。这个空元素不是可选的是必须的。第三probe 函数里尽量用dev_read_xxx系列接口读设备树属性不要直接操作dev-platdata。前者会处理各种边界情况后者容易踩坑。第四调试 dm 问题时打开CONFIG_DM_DEBUG和CONFIG_DEBUG_UART能看到详细的绑定和 probe 日志。日志量大但值得。第五如果驱动 probe 依赖时钟或复位用clk_get_by_index和reset_get_by_index它们会自动触发对应控制器的 probe比手动找设备靠谱。6. 从骨架到实战把 dm 用顺手的几个进阶思路6.1 用 uclass 接口隔离上层和驱动dm 最大的价值不是“能自动匹配驱动”而是接口隔离。上层代码调用led_ctrl_on(dev)不关心底层是 GPIO 控制的还是 I2C 扩展芯片控制的。这种隔离让驱动替换变得容易也让代码可测试性提升。写驱动时ops 里的函数应该只做“这件事怎么做”不做“这件事什么时候做”。时机由上层决定驱动只管执行。这个边界划清楚了驱动就干净。6.2 利用 post_bind 和 post_probe 做延迟初始化post_bind在设备绑定后、probe 前调用适合做一些不依赖硬件的准备工作比如解析设备树属性存到 priv 里。post_probe在 probe 后调用适合做一些依赖硬件状态的收尾工作。这两个钩子用好了能把 probe 函数拆得更清晰。我习惯把设备树解析放post_bind硬件初始化放probe状态检查放post_probe。6.3 多设备实例的 seq 管理同一个驱动绑定多个设备时需要一个序号来区分。dm 提供dev-seq和uclass_get_device_by_seq接口。seq可以从设备树的reg属性或alias节点获取。比如两个相同型号的 GPIO 控制器用seq区分后上层可以精确指定操作哪一个。alias节点里写gpio0 gpio1000;dm 会自动分配 seq。6.4 驱动卸载与资源释放u-boot 里驱动卸载用得少但在一些热插拔场景比如 USB会用到。remove函数里要释放 probe 时申请的资源比如gpio_free、clk_free。不释放会导致资源泄漏下次 probe 时申请失败。remove的调用顺序和 probe 相反子设备先于父设备。dm 框架自动处理这个顺序你只要保证remove里释放干净就行。6.5 把 dm 调试信息用起来dm tree、dm uclass、dm dev、dm drivers这几个命令是调试 dm 的利器。dm tree看层级dm uclass看分类dm dev看详情dm drivers看所有已注册驱动。我习惯在板子 bring-up 阶段每次改完驱动就dm tree看一眼确认设备出现、层级正确、probe 状态是active。这个习惯帮我省了大量调试时间。7. 我个人在实际操作中的体会u-boot 的 dm 框架刚接触时确实有点绕但它的设计逻辑其实很朴素编译期登记、运行期匹配、按需 probe。抓住这三句话再看board_init_r里的调用链就不会迷路。我踩过最大的坑是早期不理解“绑定”和“probe”的区别以为设备出现在dm tree里就代表驱动工作了。实际上绑定只是建立了udevice和driver的关联probe 才是真正初始化硬件。很多“设备在但功能不正常”的问题都是 probe 没触发或者 probe 失败被忽略了。另一个体会是设备树的嵌套关系比想象中重要。父子关系决定了 probe 顺序probe 顺序决定了依赖能否满足。写设备树时多花五分钟确认嵌套能省后面几小时的调试。最后分享一个小技巧如果你不确定某个驱动是否支持 dm看它的U_BOOT_DRIVER宏里有没有.ops和.probe。有这两个基本就是 dm 驱动没有的话可能是老式的非 dm 驱动需要单独处理。这个判断方法在移植老代码时特别有用。