
1. 从 board_init_r 这个总装车间说起很多人第一次翻 u-boot 源码看到board_init_r这个函数第一反应是这不就是个初始化函数吗挨个调用一遍就完事了。我当初也是这么想的直到有一次板子起不来串口只打印到Board init就卡死我才意识到这个函数远没有表面看起来那么简单。它其实是整个 u-boot 从裸奔状态切换到设备模型驱动状态的分水岭dmdriver model的骨架就是在这里被真正搭起来的。先把结论摆出来board_init_r是 u-boot 第二阶段重定位之后的主初始化入口它承担的核心任务之一就是把 dm 设备模型从内存里的一堆描述结构变成可以真正被驱动绑定、探测、使用的活体。你后面能调用uclass_get_device、能通过dev-ops拿到操作函数、能让网卡、MMC、串口各就各位全靠这个函数里那几行看起来不起眼的调用。这篇文章适合谁看如果你已经能编译 u-boot、能烧录、能看串口 log但对 dm 模型什么时候初始化、谁先谁后、绑定和探测到底在哪一步发生始终模模糊糊那这篇就是写给你的。我会沿着board_init_r的执行顺序把 dm 骨架的搭建过程一层层剥开讲清楚每一步在干什么、为什么这么排、以及我踩过的那些坑。需要提前说明的是u-boot 版本差异比较大我这里以主流的 dm 成熟版本2018 年之后基本稳定为基准不同厂商的 BSP 会有裁剪和魔改但主干逻辑是一致的。你对照自己手上的代码看大方向不会错。2. board_init_r 里 dm 初始化的真实调用链2.1 先搞清楚 board_init_r 到底在哪个阶段跑要理解 dm 骨架怎么搭得先知道board_init_r运行在什么时间点。u-boot 的启动分两个大阶段第一阶段是board_init_f跑在重定位之前此时代码还在只读的 flash 或者 ROM 里内存布局还没最终确定第二阶段就是board_init_r跑在重定位之后代码已经搬到 RAM 里堆栈、全局数据都就位了。这个区别非常关键。因为 dm 模型需要动态分配内存来存放struct udevice、struct uclass、struct driver的绑定关系如果内存管理还没就绪这些结构根本没法建。所以 dm 的正式初始化必须放在board_init_r里而不是board_init_f。有些新手会问为什么不在第一阶段就把驱动都初始化好答案很简单第一阶段连 malloc 都还不能正常用你拿什么去建设备树。board_init_r的典型结构大致是这样一条主线不同版本函数名略有出入void board_init_r(gd_t *new_gd, ulong dest_addr) { // 各种早期基础设施初始化 ... initr_dm(); // dm 骨架搭建的关键入口 ... initr_serial(); // 串口在 dm 之后才能真正工作 ... initr_net(); // 网络依赖 dm 里的 eth 设备 ... main_loop(); // 进入命令行或启动内核 }注意initr_dm()的位置它排在很多外设初始化之前。这个顺序不是随便定的而是因为后面的串口、网卡、存储设备全都要通过 dm 框架来获取如果 dm 没先建好后面全是空指针。2.2 initr_dm 内部到底做了哪几件事initr_dm()本身代码量不大但它调用的dm_init_and_scan()才是真正干活的地方。我把这条链拆成几个关键动作第一件事是初始化 dm 的全局根节点。dm 模型是一棵树树得有根。这个根就是root设备它不对应任何真实硬件纯粹是个逻辑容器。dm_init()会创建这个根设备并把gd-dm_root指向它。你可以把它理解成整个设备树的挂载点后面所有设备都是它的子孙。第二件事是扫描并绑定bind设备。这一步会遍历设备树DTB或者平台数据结构把每一个有compatible属性的节点找到对应的 driver然后创建struct udevice并挂到父节点下面。注意绑定阶段只是建立关系还没有真正去操作硬件。很多人误以为绑定就等于驱动跑起来了其实不是绑定只是把设备和驱动配对登记。第三件事是按顺序探测probe设备。探测才是真正调用驱动的probe函数、去读写寄存器、初始化硬件的过程。dm 会按照设备树的层级和u-boot,dm-pre-reloc、u-boot,dm-pre-proper这些标记决定哪些设备先探测、哪些延后。这三件事的顺序不能乱先有根才能挂设备先绑定才能探测。这个逻辑链就是 dm 骨架的钢筋结构。2.3 为什么串口初始化必须排在 dm 之后这里有个特别典型的坑我当年就栽过。串口在调试阶段太重要了很多人想让它越早工作越好于是把串口初始化往前提。但在 dm 框架下串口设备本身就是一个 dm 设备它需要先被绑定、再被探测才能拿到struct udevice和对应的 ops。如果你在initr_dm()之前就去调serial_init()而此时 dm 还没建好串口驱动里的dev指针是空的轻则打印不出来重则直接跑飞。所以正确的顺序永远是dm 骨架先搭好串口作为 dm 设备被探测然后才能正常输出。我实测过一个反例某次为了调试早期问题强行在 dm 之前初始化串口结果 log 是出来了但后面 dm 扫描时又初始化了一遍串口寄存器被重复配置波特率直接乱掉打印全是乱码。这个教训告诉我在 dm 框架下任何外设都不要绕过框架去抢跑。3. 绑定与探测dm 骨架的两根主梁3.1 绑定阶段设备与驱动的相亲登记绑定bind这个词用得很形象它干的事就是给设备和驱动牵线。具体流程是这样的dm 扫描到一个设备节点读取它的compatible字符串然后在一张全局的驱动表里查找哪个 driver 的of_match表里有这个字符串。找到了就创建一个struct udevice把dev-driver指向这个 driver同时把设备挂到父设备的子链表上。这里有个细节值得说struct udevice是动态分配的用的是 dm 自己的内存池。为什么不用普通 malloc因为 dm 设备数量在编译期其实是可以估算的用一个预分配的池子能避免内存碎片也能加快分配速度。这个设计思路在嵌入式环境里很常见毕竟资源紧张。绑定的顺序也有讲究。dm 会先绑定父节点再绑定子节点因为子节点需要知道自己的父是谁。这个自顶向下的顺序保证了树的完整性。如果你在调试时发现某个设备的dev-parent是空的那多半是绑定顺序出了问题或者设备树里这个节点的父节点没被正确识别。还有一个容易忽略的点绑定不等于驱动存在。如果某个设备的compatible在驱动表里找不到匹配dm 会怎么处理默认情况下它会报一个警告然后跳过这个设备但不会让整个启动失败。这个行为在移植新板子时特别有用你可以先让系统跑起来再逐个补驱动。3.2 探测阶段真正让硬件活过来探测probe是 dm 骨架里最重的一步。绑定只是登记探测才是入洞房。探测时 dm 会调用 driver 的probe回调驱动在这个回调里完成寄存器映射、时钟使能、复位释放等一系列硬件操作。探测的顺序由几个因素决定。首先是设备树的层级父设备通常先于子设备探测因为子设备可能依赖父设备提供的资源比如总线控制器。其次是u-boot,dm-pre-reloc标记带这个标记的设备会在重定位之前就被探测用于那些早期就要用的设备比如调试串口。再就是u-boot,dm-pre-proper这类设备在重定位后、正式扫描前探测。我踩过的一个坑是关于探测顺序的依赖问题。有一次 MMC 控制器和它的 PHY 是两个独立的 dm 设备PHY 必须先于控制器探测否则控制器初始化时访问 PHY 寄存器会失败。解决办法是在设备树里用depends或者调整节点顺序让 dm 知道谁先谁后。这个机制在较新版本的 dm 里支持得比较好老版本可能要靠手动控制。探测失败的处理也值得说。如果某个设备的 probe 返回错误dm 默认会把这个设备标记为探测失败后续再有人想用它会直接返回错误而不是重试。这个设计避免了反复初始化同一个坏设备。但在调试阶段这个缓存失败的行为有时候会误导你让你以为改了代码没生效其实是 dm 记住了上次的失败。遇到这种情况可以在 probe 里加打印或者临时清掉失败标记。3.3 用一张表看清绑定和探测的区别维度绑定bind探测probe核心动作创建 udevice匹配 driver调用 driver-probe操作硬件是否碰硬件否是失败影响跳过该设备启动继续设备标记失败后续不可用触发时机dm 扫描设备树时按层级和标记顺序触发典型耗时极短纯内存操作较长涉及寄存器和延时这张表我在调试时经常拿出来对照。当你发现某个设备存在但用不了多半是绑定成功但探测失败当你发现设备根本找不到那可能是绑定阶段就没匹配上驱动。4. 驱动骨架里那些不写文档的潜规则4.1 uclass 才是 dm 的真正组织者很多人学 dm 只盯着 driver 和 udevice忽略了 uclass。其实 uclass 才是 dm 骨架里最核心的组织单位。uclass 是一类设备的抽象比如所有串口都属于UCLASS_SERIAL所有网卡都属于UCLASS_ETH。当你调用uclass_get_device(UCLASS_ETH, 0, dev)时dm 是在 uclass 的链表里找第 0 个 eth 设备。uclass 的价值在于统一接口。不同厂商的网卡驱动实现千差万别但对上层来说它们都提供eth_ops里定义的那几个操作。这样上层代码就不用关心底层是哪家的芯片。这个设计思想其实就是面向对象里的多态只不过用 C 语言实现。在board_init_r的 dm 初始化过程中uclass 是在绑定设备时按需创建的。第一个属于某个 uclass 的设备被绑定时dm 会创建对应的 uclass 实例后续同类设备直接挂到这个 uclass 下。所以 uclass 的数量是动态的取决于实际用到了哪些类型的设备。这里有个实操技巧如果你想知道系统里到底有哪些 uclass、每个 uclass 下有几个设备可以在命令行里用dm tree和dm uclass命令前提是编译时开了CONFIG_CMD_DM。这两个命令在调试 dm 问题时简直是神器能直接看到整棵设备树和 uclass 的归属关系。4.2 设备树里的 status 属性是个隐形开关设备树里每个节点都有个status属性取值okay或disabled。这个属性看起来不起眼但它直接决定 dm 会不会去绑定和探测这个设备。disabled的设备在扫描阶段就被跳过了连 udevice 都不会创建。这个机制在板级配置里特别有用。比如一块板子有两个网口但某个产品型号只用其中一个那就可以在对应的设备树里把不用的那个网口标成disabled这样 dm 就不会去初始化它省时省电。我遇到过一个诡异的问题某个外设在设备树里明明写了驱动也编进去了但就是不出现在dm tree里。查了半天才发现这个节点的status被上游的 dtsi 文件设成了disabled而我在板级 dts 里没有覆盖它。这个坑提醒我看设备树一定要看最终展开的结果不能只看自己写的那一层。4.3 probe 的时机比你想的更灵活很多人以为 probe 只在board_init_r的 dm 初始化阶段发生一次其实不是。dm 支持懒探测lazy probe也就是说一个设备可以等到第一次被真正使用时才探测。这个机制在启动时间敏感的场景下非常有用能把不急着用的设备推迟初始化。懒探测的触发点是device_probe()被调用时。当你通过uclass_get_device拿到设备后如果这个设备还没探测dm 会自动触发探测。所以从使用者的角度看你不需要关心设备是什么时候探测的只要在用它之前确保 dm 已经初始化就行。但这个灵活性也带来一个坑如果某个设备在board_init_r早期被别的驱动依赖而它又是懒探测的那依赖它的驱动可能会拿到一个未探测的设备。解决办法是在设备树里给这个设备加u-boot,dm-pre-reloc或者显式在早期调用device_probe()。我在调试一个电源管理芯片时就遇到过这个问题最后是在它的消费者驱动里手动 probe 才解决。5. 移植新板子时 dm 骨架的排查链路5.1 从串口 log 定位 dm 卡在哪一步移植新板子最怕的就是卡在 dm 初始化阶段串口只打印一半就没动静了。这时候第一步是看 log 最后停在哪。如果停在initr_dm之前那问题在更早的基础设施如果停在 dm 扫描过程中那多半是某个设备的绑定或探测出了问题。我一般的做法是在dm_init_and_scan的关键节点加打印比如每绑定一个设备打一行每探测一个设备打一行。这样能精确定位到是哪个设备卡住的。虽然会拖慢启动但调试阶段这点开销完全值得。定位到具体设备后再去看它的驱动 probe 函数。常见的卡死原因有几个时钟没使能就去读寄存器总线挂死、复位没释放就访问读回全 0 或全 F、寄存器地址映射错误访问到非法地址触发异常。这些都要结合芯片手册逐个排查。5.2 驱动匹配不上的三种典型原因设备在设备树里驱动也编进去了但就是匹配不上这种情况我遇到过至少三种原因。第一种是compatible字符串拼写不一致。设备树里写vendor,device-a驱动里写vendor,device_a一个横杠一个下划线dm 就认不出来。这种错误特别隐蔽因为肉眼看过去几乎一样。第二种是驱动没有正确注册到 dm 的驱动表里。u-boot 用链接器段linker section来收集所有U_BOOT_DRIVER宏定义的驱动如果编译配置里把某个文件排除了或者链接脚本有问题驱动就不会出现在表里。可以用dm drivers命令查看当前系统里注册了哪些驱动。第三种是驱动依赖的 uclass 没被创建。比如一个驱动声明自己属于UCLASS_I2C但系统里没有任何 I2C 控制器被使能那这个 uclass 可能就不存在驱动也就无法正常绑定。这种情况通常伴随其他错误信息需要一起看。5.3 一个真实的排查案例有次移植一块新板子网卡死活起不来dm tree里能看到 eth 设备但ping就是不通。按排查链路走先确认绑定成功在 tree 里成功再确认探测成功加打印probe 返回 0成功那问题就在探测之后的配置上。继续查发现 probe 里读到的 PHY ID 是 0xffffffff说明 MDIO 总线读不到 PHY。回头查设备树发现 MDIO 节点的地址和实际硬件差了一位。改过来之后PHY ID 正常网卡也就通了。这个案例说明dm 骨架搭起来只是第一步骨架上的血肉具体硬件配置还得靠设备树和驱动配合。6. 把 dm 骨架用顺手之后的几点体会6.1 不要绕过框架直接操作硬件用惯了 dm 之后最大的体会就是任何外设操作都应该通过 dm 框架走。我见过不少代码为了图省事直接在板级文件里写死寄存器地址去操作硬件绕过了 dm。这种代码短期能跑但一旦换板子、换芯片就得全部重写而且和 dm 里的设备状态可能冲突。正确的做法是把硬件操作都封装进驱动的 ops 里上层通过 uclass 接口调用。这样代码的可移植性和可维护性都会好很多。虽然前期多写一点代码但后期省下的调试时间远超这点投入。6.2 设备树的组织要跟着 dm 的层级走设备树不是随便写的它的层级结构直接影响 dm 的绑定和探测顺序。父节点代表总线或控制器子节点代表挂在上面的设备这个层级要和硬件实际拓扑对应。我见过有人把所有设备都平铺在根节点下结果 dm 探测顺序完全乱套依赖关系全断。合理的做法是I2C 控制器作为一个节点它下面挂的 I2C 设备作为子节点SPI 同理GPIO 控制器和它管理的引脚也要有清晰的父子关系。这样 dm 在探测时自然就能保证先控制器后设备的顺序。6.3 善用 dm 提供的调试命令最后分享几个我常用的 dm 调试命令编译时打开CONFIG_CMD_DM就能用dm tree打印整棵设备树看层级和绑定状态dm uclass按 uclass 分组列出所有设备dm devres查看设备资源分配情况dm drivers列出所有已注册的驱动这几个命令在排查设备找不到驱动匹配不上探测顺序不对这类问题时能帮你快速缩小范围。我现在的习惯是每移植一块新板子第一件事就是dm tree看一眼心里对整个设备模型有个底。说到底board_init_r里的 dm 骨架搭建本质上就是先建根、再挂枝、后开花的过程。理解了这条主线再看那些零散的初始化调用就不会觉得是一团乱麻了。