
1. 从 board_init_r 切入搞懂 u-boot 设备模型到底在干什么玩过 u-boot 移植的人都有一个共同的体感板子能跑到board_init_r打印出那一行U-Boot 20xx.xx心里就踏实了一半。但紧接着串口没输出、网卡不识别、MMC 找不到设备一堆问题又冒出来。这些问题的根子十有八九都落在设备模型driver model简称 dm这套骨架上。board_init_r就是这套骨架真正开始“跑起来”的分水岭——在这之前是极早期初始化在这之后dm 驱动开始接管外设。我先把结论摆出来u-boot 的 dm 驱动骨架本质上是一套“先扫描、再绑定、后探测”的三段式流程而board_init_r里的dm_init_and_scan就是这套流程的总开关。你只要把这一段的调用链捋清楚后面不管是加一个新驱动、调一个 probe 顺序还是排查“设备树写了但驱动没起来”的问题都能顺藤摸瓜。这篇文章适合谁看如果你已经能编译 u-boot、能烧录、能看串口 log但对 dm 的理解还停留在“知道有这回事”的阶段那这篇就是给你写的。我会从board_init_r的代码路径开始一层层拆到device_bind、device_probe把每个环节“为什么这么设计”讲透再配上我自己踩过的坑和排查手法。全程不堆术语尽量用生活化的类比把机制说清楚。先给一个整体印象。u-boot 的 dm 模型可以类比成一家公司的组织架构udevice 是岗位driver 是岗位说明书uclass 是部门设备树是编制表。board_init_r做的事情就是拿着编制表设备树去建岗位udevice、匹配说明书driver、然后让每个岗位正式上岗干活probe。这个类比你先记住后面所有细节都能往上面套。2. board_init_r 之前dm 骨架其实已经埋好了伏笔2.1 为什么 dm 初始化要放在 board_init_r 而不是更早很多人会问既然 dm 这么重要为什么不放在board_init_f里做答案藏在内存布局里。board_init_f阶段运行在极早期的环境可能是 SRAM、可能是 cache-as-RAM空间小得可怜而且此时 DDR 还没初始化完。dm 模型需要为每个 udevice 分配结构体内存、需要解析设备树、需要维护 uclass 链表这些都是“吃内存”的操作。放到board_init_f里等于让一个还没搬进新办公室的团队去处理全公司的档案根本不现实。所以 u-boot 的设计是board_init_f只做最关键的早期初始化时钟、串口 debug、DDR 控制器把内存准备好然后重定位到 DDR进入board_init_r这时候才有足够的内存和完整的 C 运行环境去搭建 dm 骨架。这个“先搭台再唱戏”的顺序是理解整个 dm 初始化时机的关键。2.2 board_init_r 里 dm 相关的调用顺序在board_init_r里dm 相关的初始化不是孤立的一行而是和一堆其他初始化交织在一起。典型的调用顺序大致是这样的不同版本略有差异但主干一致void board_init_r(gd_t *new_gd, ulong dest_addr) { // ... 其他早期设置 initr_dm(); // 关键dm 初始化与扫描 // ... 其他外设初始化 initr_serial(); initr_net(); // ... run_main_loop(); }initr_dm()内部最终会调用到dm_init_and_scan()。这个函数是整个 dm 骨架的“总装配线”它做了两件大事初始化 dm 的全局数据结构以及扫描设备树并绑定驱动。注意这里只是“绑定”真正的“探测”probe是延迟的等到具体驱动被使用时才触发。这个延迟探测的设计非常关键后面会专门讲。提示不同 u-boot 版本里initr_dm的位置和名字可能不同有的版本直接叫dm_init_and_scan有的封装在initr_系列里。看代码时以你手上的版本为准但逻辑是一致的。2.3 dm_init_and_scan 的两阶段设计dm_init_and_scan这个名字本身就透露了信息init 和 scan 是两件事。init 阶段做的是“准备舞台”——初始化gd-dm_root、gd-uclass_root这些全局链表头把 dm 的根节点建起来。scan 阶段做的是“按图索骥”——遍历设备树为每个有compatible属性的节点创建 udevice并尝试匹配 driver。这个两阶段设计的好处是init 只做一次代价小scan 可以按需触发甚至支持“懒扫描”。比如有些平台为了加快启动会先只扫描关键设备串口、时钟其他设备等真正用到再扫。这种灵活性在启动时间敏感的嵌入式场景里非常值钱。3. 设备模型三大件udevice、driver、uclass 到底怎么串起来3.1 udevice设备的“身份证”struct udevice是 dm 模型里最核心的结构体你可以把它理解成每个设备的“身份证 档案袋”。它里面存了什么我挑几个关键字段说driver指向这个设备匹配到的驱动没匹配上就是 NULL。uclass指向这个设备所属的“部门”比如所有串口设备都属于 UCLASS_SERIAL。parent父设备指针dm 是一棵树不是一张表。plat平台数据通常来自设备树的解析结果。priv驱动私有数据驱动自己用dm 不管。flags状态标志比如DM_FLAG_ACTIVATED表示已经 probe 过了。这里有个容易混淆的点plat和priv到底谁是谁我见过不少新手在这上面栽跟头。简单说plat 是“设备树告诉驱动的信息”priv 是“驱动自己运行时的状态”。plat 在绑定阶段就可能被填充priv 一般在 probe 阶段才分配。分清楚这两个你写驱动时就不会把该放 plat 的东西塞进 priv。3.2 driver设备的“岗位说明书”struct driver描述了一个驱动能干什么、怎么干。关键字段包括name驱动名调试时看 log 就靠它。id驱动 ID配合 uclass 使用。of_match设备树匹配表这是 scan 阶段匹配的核心依据。bind绑定回调创建 udevice 时调用。probe探测回调真正初始化硬件时调用。remove移除回调一般用不到但规范上要有。of_match里的compatible字符串就是设备和驱动之间的“暗号”。设备树里写compatible vendor,device驱动里of_match表里也有这一项两者一对上就算匹配成功。这个机制和 Linux 的设备树匹配是一脉相承的学过 Linux 驱动的同学会觉得很亲切。3.3 uclass设备的“部门”struct uclass是 dm 模型里最容易被低估的部分。很多人觉得它就是个分类标签其实它的作用远不止于此。uclass 提供了统一的操作接口。比如所有串口设备都属于 UCLASS_SERIAL那么上层代码想发一个字符不需要知道具体是哪个串口芯片直接调serial_putc()就行uclass 会帮你找到对应的设备并调用它的 ops。这就是 dm 模型“解耦”的精髓上层不依赖具体驱动只依赖 uclass 接口。你换一个串口芯片只要它注册到 UCLASS_SERIAL 并实现标准 ops上层代码一行都不用改。这种设计在有多款板子、多种外设的 u-boot 里价值巨大。uclass 还有自己的uclass_driver里面定义了post_bind、post_probe这些钩子。这些钩子在批量管理设备时特别有用比如 UCLASS_SERIAL 可以在post_probe里把当前串口设为 console。3.4 三者关系的一张表说清概念类比关键结构体核心作用udevice岗位struct udevice代表一个具体设备实例driver岗位说明书struct driver描述驱动能力和回调uclass部门struct uclass提供统一接口和分类管理设备树编制表device tree描述硬件拓扑和属性这张表建议你存下来每次看 dm 代码卡住的时候对照一下很快就能定位自己在哪一层。4. 绑定与探测dm 骨架最核心的两步4.1 device_bind给设备“上户口”scan 阶段遍历设备树时每遇到一个节点就会尝试匹配驱动。匹配成功后调用device_bind_common不同版本名字略有差异核心动作是分配一个struct udevice。把 driver、parent、uclass 这些指针填好。解析设备树节点填充 plat 数据。调用 driver 的bind回调如果有。把 udevice 挂到父设备的子链表和 uclass 的设备链表上。注意第 4 步bind回调是“轻量级”的它不应该做硬件初始化只应该做数据结构层面的准备。我见过有人在 bind 里直接去读写寄存器结果 probe 顺序一乱就出问题。bind 只做“登记”probe 才做“干活”这条界线要守住。4.2 device_probe让设备“正式上岗”probe 才是真正初始化硬件的地方。它的核心动作是检查DM_FLAG_ACTIVATED已经 probe 过就直接返回保证只 probe 一次。递归 probe 父设备确保依赖顺序正确。分配 priv 数据如果驱动声明了 priv 大小。调用 driver 的probe回调这里才是读写寄存器、配置硬件的地方。设置DM_FLAG_ACTIVATED标志。第 2 步的“递归 probe 父设备”是 dm 模型保证依赖顺序的关键。比如一个 I2C 设备它的父设备是 I2C 控制器probe 这个设备之前必须先 probe 控制器否则总线都没通怎么通信dm 用递归的方式自动帮你保证了这一点你不需要手动排顺序。4.3 延迟探测为什么 scan 完不立刻 probe这是 dm 模型最巧妙的设计之一。scan 阶段只 bind 不 probeprobe 推迟到设备真正被使用时。好处有三个加快启动不是所有设备启动时都要用没必要全部初始化。避免顺序问题如果 scan 时就 probe遇到依赖关系复杂的设备树很容易因为顺序不对而失败。支持按需加载有些设备可能整个启动过程都用不到那就永远不 probe省电省时间。那 probe 是什么时候触发的通常是上层调用uclass_get_device或device_probe时。比如串口初始化时调uclass_get_device_by_seq(UCLASS_SERIAL, 0, dev)这个调用内部会触发 probe。所以你在 log 里看到的 probe 顺序往往不是设备树里的顺序而是“谁先被用到谁先 probe”。注意如果你发现某个设备一直没 probe先别怀疑驱动写错了先确认有没有代码去“用”它。dm 不会主动 probe 一个没人用的设备。5. 实操从零加一个 dm 驱动把骨架跑通5.1 准备工作确认你的板子支持 dm不是所有 u-boot 配置都开了 dm。先检查.config里有没有这几项CONFIG_DMy CONFIG_DM_SERIALy CONFIG_OF_CONTROLy CONFIG_OF_LIVEy # 可选但推荐CONFIG_DMy是总开关没有它后面都免谈。CONFIG_OF_CONTROLy表示用设备树来控制驱动这是 dm 的标准玩法。如果这几项没开先去menuconfig里打开再谈加驱动。5.2 写一个最简单的 dm 驱动骨架假设我们要加一个“虚拟 LED”驱动设备树里写compatible demo,led。驱动代码大致长这样#include common.h #include dm.h struct demo_led_plat { int gpio; }; struct demo_led_priv { bool on; }; static int demo_led_probe(struct udevice *dev) { struct demo_led_plat *plat dev_get_plat(dev); struct demo_led_priv *priv dev_get_priv(dev); priv-on false; printf(demo_led: probe, gpio%d\n, plat-gpio); return 0; } static int demo_led_bind(struct udevice *dev) { printf(demo_led: bind\n); return 0; } static const struct udevice_id demo_led_ids[] { { .compatible demo,led }, { } }; U_BOOT_DRIVER(demo_led) { .name demo_led, .id UCLASS_MISC, .of_match demo_led_ids, .bind demo_led_bind, .probe demo_led_probe, .plat_auto sizeof(struct demo_led_plat), .priv_auto sizeof(struct demo_led_priv), };几个关键点解释一下。U_BOOT_DRIVER宏会把驱动注册到一个特殊的链接段里scan 阶段就是遍历这个段来找驱动的。plat_auto和priv_auto告诉 dm 自动分配多大的 plat 和 priv 空间省得你手动 malloc。of_match表里的 compatible 必须和设备树里写的一模一样一个字符都不能差。5.3 设备树里加节点在对应的.dts文件里找个合适的位置加demo_led: demo-led { compatible demo,led; gpio gpio0 5 GPIO_ACTIVE_HIGH; };注意gpio这个属性名要和驱动里demo_led_plat的字段对应。dm 在 bind 阶段会自动解析设备树属性并填充 plat但前提是属性名和结构体字段名一致或者你用dev_read_*系列函数手动读。这个“自动填充”机制依赖of_to_plat回调简单场景下 dm 能自动处理复杂场景建议自己写of_to_plat。5.4 验证驱动是否被绑定和探测编译烧录后看串口 log。如果一切正常你应该能看到demo_led: bind demo_led: probe, gpio5如果只看到 bind 没看到 probe说明设备没被使用。这时候你可以在某个初始化函数里手动调一下struct udevice *dev; int ret uclass_get_device_by_driver(UCLASS_MISC, DM_DRIVER_GET(demo_led), dev);这一调probe 就会被触发。这个手法在调试新驱动时特别有用能快速验证 probe 逻辑对不对。6. 排查实录dm 驱动起不来的常见坑6.1 设备树节点没被扫描到最常见的现象是驱动代码写了设备树也加了但 log 里连 bind 都没有。这时候按这个顺序排查确认设备树节点在正确的文件里。u-boot 的设备树可能是u-boot.dtsi、-u-boot.dts或者板级 dts不同平台不一样。改错文件是新手最常犯的错。确认节点没有被 status disabled。有些节点默认是 disabled 的需要显式打开。确认 compatible 字符串完全一致。大小写、连字符、厂商前缀一个都不能错。确认 CONFIG_OF_CONTROL 开了。没开的话设备树根本不参与驱动匹配。我踩过最坑的一次是设备树节点加在了soc下面但那个 soc 节点的ranges没配好导致节点虽然存在但扫描时被跳过。这种问题看 log 看不出来得用fdtgrep或者dtc反编译 dtb 来确认节点真的在。6.2 bind 成功但 probe 失败bind 成功说明匹配没问题probe 失败通常是硬件相关的问题。常见原因父设备没 probedm 会递归 probe 父设备但如果父设备 probe 失败子设备也会失败。看 log 里父设备有没有报错。时钟没使能很多外设 probe 时需要时钟如果时钟驱动没起来或者时钟 ID 配错probe 就会卡住或返回错误。GPIO 没配好GPIO 驱动没 probe 或者引脚号写错probe 时读不到正确状态。寄存器访问越界地址映射没配好读写寄存器直接挂掉。排查手法在 probe 函数里加printf一步步缩小范围。别嫌土这是最有效的方法。dm 的 log 虽然详细但有时候不够直观自己加打印最快。6.3 probe 顺序不对导致依赖失败dm 的递归 probe 能解决大部分依赖问题但有一种情况例外循环依赖。比如 A 依赖 BB 又依赖 A递归就会死循环或者栈溢出。这种情况一般出现在设计不合理的设备树里解决办法是引入一个中间层或者把公共依赖抽出来单独 probe。还有一种情况是“隐式依赖”A 的 probe 里用到了 B但设备树里 A 不是 B 的子节点dm 不知道这个依赖关系。这时候要么调整设备树让依赖显式化要么在 A 的 probe 里手动调device_probe(B)。后者不优雅但有效紧急情况下可以用。6.4 常见问题速查表现象可能原因排查方向无 bind log设备树没扫描到检查 dts 文件、compatible、status有 bind 无 probe设备没被使用手动调 uclass_get_deviceprobe 返回错误硬件初始化失败检查时钟、GPIO、寄存器probe 卡死死循环或总线挂起检查循环依赖、I2C/SPI 总线父设备报错依赖链上游失败从根节点往下逐级排查这张表建议打印出来贴在工位上遇到问题先对照一遍能省不少时间。7. 几个让我少走弯路的实操心得7.1 善用 dm 的调试命令u-boot 命令行里有dm系列命令dm tree能打印出完整的设备树和绑定状态dm uclass能列出所有 uclass 和它们的设备。这两个命令在排查“设备到底绑没绑上”时特别好用。前提是编译时开了CONFIG_CMD_DM。我一般的排查流程是先dm tree看设备在不在树里再看它的状态是“bound”还是“probed”。如果连树里都没有那就是 scan 阶段的问题如果在树里但没 probed那就是使用时机的问题。这个二分法能快速定位问题层级。7.2 plat 数据尽量在 of_to_plat 里解析虽然 dm 能自动填充 plat但自动填充只支持简单类型遇到 phandle、数组、复杂结构就力不从心了。我的习惯是只要 plat 里有非标量字段就自己写of_to_plat回调用dev_read_*系列函数显式解析。这样代码可读性好出问题也容易定位。of_to_plat的调用时机是在 bind 之后、probe 之前这时候设备树节点已经可用但硬件还没初始化正好适合做数据解析。7.3 probe 里不要做耗时操作probe 是在启动关键路径上的如果里面做了耗时操作比如等待某个硬件稳定、做大量计算会拖慢整个启动。我的原则是probe 只做“让设备可用”的最小操作其他事情推迟到实际使用时再做。比如网卡 probe 只初始化 MAC 和 PHY 的基本寄存器链路协商这种耗时的事情放到start回调里。7.4 版本差异要心里有数u-boot 的 dm 接口在不同版本之间有变化比如dev_get_plat在老版本里叫dev_get_platdataplat_auto是后来才加的。看代码和写代码时先确认自己用的是哪个版本别照着新版本的教程去改老版本的代码会踩一堆编译错误。我一般会在项目根目录放一个VERSION文件备注当前版本省得来回查。8. 把骨架吃透之后你还能往哪走dm 骨架跑通之后你会发现很多之前觉得神秘的东西都变得清晰了。比如uclass_get_device为什么能拿到设备、device_probe为什么能保证只 probe 一次、设备树里的phandle是怎么变成代码里的指针的这些问题的答案都在你刚刚捋过的这条调用链里。再往深走可以研究 uclass 的post_probe机制看看串口是怎么在 probe 后自动成为 console 的可以研究DM_FLAG_PRE_RELOC看看哪些驱动能在重定位之前就工作还可以研究of-platdata这是 u-boot 为了进一步加快启动而搞的一套“把设备树编译成 C 结构体”的机制在资源极度受限的场景下很有用。我自己在实际项目里的体会是dm 这套东西看一遍代码只能懂个大概真正理解是在你加了三五个驱动、踩了七八个坑之后。所以别怕动手找个简单的板子从加一个 GPIO 驱动开始把 bind、probe、uclass 这三步走一遍比看十篇文章都管用。最后再分享一个小技巧调试 dm 问题时把CONFIG_DM_DEBUG打开log 会详细到每一步的调用虽然刷屏但定位问题时真的香。