
1. 从 board_init_r 到 dm 驱动骨架为什么这块骨头最难啃搞嵌入式的人都有一个共识u-boot 的启动流程里board_init_r是真正意义上的分水岭。在这之前CPU 刚上电DDR 可能还没初始化完你连 printf 都用不了在这之后串口通了、内存能用了、驱动模型开始接管外设整个系统才算真正“活”过来。而 dmdriver model驱动骨架就是在这个阶段被一层层搭起来的。很多人第一次看 u-boot 源码翻到board_init_r就懵了。函数体里一堆initr_xxx调用什么initr_dm、initr_dm_devices、initr_serial、initr_mmc看着像流水账但每个调用背后都牵扯到设备模型的初始化顺序、驱动绑定、probe 时机。你要是没搞清楚这套骨架是怎么搭的调试的时候就会遇到各种诡异问题串口突然不输出了、MMC 设备找不到、网卡 probe 失败但没有任何报错。我见过太多人在这块栽跟头。有人改了 dts 里的节点结果设备就是不 probe有人在board_init_r里手动调了device_probe结果系统直接挂死还有人把initr_dm的顺序调了整个启动流程全乱套。这些问题的根源都是没理解 dm 驱动骨架在board_init_r里的搭建逻辑。这篇文章就是要把这块骨头啃碎。我会从board_init_r的整体流程讲起拆解 dm 驱动骨架的搭建步骤分析每个关键环节的设计意图然后给出可直接复现的实操方法和排查技巧。不管你是刚接触 u-boot 的新手还是已经调过几个板子的老手都能从里面找到有用的东西。尤其是那些正在移植新板子、调试 dm 驱动的人这篇文章基本可以当手册用。提示本文基于 u-boot 2020.04 到 2023.07 之间的主流版本编写不同版本在函数命名和调用顺序上可能有细微差异但核心逻辑一致。阅读时建议对照自己手上的源码。2. board_init_r 的整体流程与 dm 骨架的搭建时机2.1 board_init_r 到底做了什么board_init_r是 u-boot 第二阶段也就是重定位之后的主入口。它接收两个参数gdglobal data 指针和dest_addr重定位目标地址。函数一开始会做一些基础设置比如重新初始化gd、设置malloc区域、清 BSS 段然后进入一连串的initr_xxx调用。这些initr_xxx调用不是随便排的它们的顺序有严格的依赖关系。比如initr_dm必须在initr_serial之前因为串口驱动需要 dm 框架来绑定initr_dm_devices又必须在initr_dm之后因为设备 probe 需要 dm 根节点已经就绪。整个调用链就像搭积木底层的先放上层的后放顺序错了就会塌。在 u-boot 的common/board_r.c里这些initr_xxx函数被放在一个init_fnc_t数组里通过initcall机制依次执行。这个数组的顺序就是启动流程的顺序也是 dm 驱动骨架搭建的顺序。2.2 dm 骨架搭建的三个关键阶段dm 驱动骨架在board_init_r里的搭建可以分成三个阶段第一阶段dm 根节点初始化initr_dm这个阶段做的是 dm 框架的基础准备。initr_dm会调用dm_init_and_scan它会创建 dm 的根设备root扫描 dts 里的设备节点把每个节点转换成udevice结构然后挂到 dm 的设备树上。这时候设备还没有 probe只是“注册”了。第二阶段设备 probeinitr_dm_devices这个阶段会遍历 dm 设备树对每个标记了DM_FLAG_PRE_RELOC或者需要早期初始化的设备调用device_probe。probe 的过程就是驱动和硬件真正打交道的过程读寄存器、配置时钟、初始化外设。串口、定时器、中断控制器这些关键设备通常在这个阶段 probe。第三阶段按需 probe后续initr_xxx后面的initr_serial、initr_mmc、initr_net等函数会通过uclass_get_device或者device_probe按需触发特定设备的 probe。这时候 dm 骨架已经搭好了这些调用只是往骨架上挂具体的“肉”。这三个阶段的关系可以用一个比喻第一阶段是搭骨架第二阶段是给骨架装上关键器官心脏、肺第三阶段是给骨架穿上衣服、拿上工具。骨架没搭好后面全白搭。2.3 为什么 dm 骨架要在 board_init_r 里搭有人可能会问dm 框架为什么不在board_init_f里就初始化好答案很简单board_init_f运行的时候DDR 可能还没初始化堆内存不可用而 dm 框架需要动态分配内存来创建设备结构。没有内存dm 根本跑不起来。另外board_init_f阶段的主要任务是初始化 DDR、串口早期调试串口、重定位 u-boot 自身。这些任务比较“硬核”不需要 dm 框架也能完成。等到board_init_r阶段内存可用了系统环境稳定了才是 dm 框架登场的最佳时机。还有一个原因是灵活性。dm 框架支持从 dts 动态扫描设备而 dts 的解析需要完整的内存管理支持。在board_init_f阶段强行初始化 dm会让代码复杂度大幅增加得不偿失。3. dm 驱动骨架的核心组件与数据结构3.1 udevice、driver、uclass 三者的关系dm 框架的核心是三个数据结构udevice、driver、uclass。理解它们的关系是理解 dm 骨架的关键。udevice代表一个设备实例。每个 dts 里的设备节点在 dm 框架里都会对应一个udevice。它包含了设备的名称、所在总线、父设备、子设备列表、对应的驱动指针、uclass 指针等信息。driver代表一个驱动。它包含了驱动的名称、对应的udevice匹配方式比如 compatible 字符串、probe 函数、remove 函数、以及各种操作函数如ops。uclass代表一类设备。比如串口是一个 uclassMMC 是一个 uclassGPIO 是一个 uclass。每个 uclass 有自己的 ID、名称、以及一组通用的操作接口。三者的关系是udevice通过driver来驱动硬件而driver属于某个uclass。当你调用uclass_get_device时dm 框架会先找到对应的 uclass然后从 uclass 的设备列表里找到匹配的udevice最后调用它的driver-probe。这个设计的好处是解耦。设备驱动不需要知道具体硬件的细节只需要实现 uclass 定义的接口uclass 不需要知道具体有哪些设备只需要管理设备列表设备实例不需要知道驱动怎么实现只需要绑定正确的驱动。3.2 设备树的扫描与 udevice 的创建dm 骨架搭建的第一步是扫描设备树。dm_init_and_scan会调用dm_scan_fdt它会遍历 dts 里的所有节点对每个节点调用device_bind。device_bind做的事情是根据节点的 compatible 字符串找到匹配的driver然后分配一个udevice把driver和udevice绑定起来最后把udevice挂到父设备的子设备列表里。这个过程有一个关键点device_bind只是绑定不 probe。也就是说这时候设备只是“注册”了还没有真正初始化硬件。probe 的时机由后面的initr_dm_devices或者按需调用来决定。为什么要分开绑定和 probe因为有些设备需要在特定时机 probe。比如 I2C 控制器必须在它的子设备I2C 从设备之前 probe否则子设备找不到总线。如果绑定的时候就 probe顺序就无法控制了。3.3 uclass 的注册与设备的归类在device_bind的过程中dm 框架还会把udevice归类到对应的uclass。每个uclass有一个设备列表device_bind会把新创建的udevice加到列表里。uclass 的注册有两种方式一种是通过UCLASS_DRIVER宏静态注册另一种是通过uclass_create动态创建。大多数 uclass 都是静态注册的比如UCLASS_SERIAL、UCLASS_MMC、UCLASS_GPIO。uclass 的作用是提供统一的操作接口。比如UCLASS_SERIAL定义了serial_putc、serial_getc、serial_setbrg等接口所有串口驱动都实现这些接口。上层代码调用serial_putc时不需要知道具体是哪个串口驱动只需要通过 uclass 找到对应的设备然后调用它的ops-putc。这种设计让 u-boot 的驱动代码非常干净。你换一个串口芯片只需要换驱动上层代码一行都不用改。3.4 驱动匹配的优先级与 compatible 字符串驱动匹配是 dm 框架的核心机制之一。当device_bind扫描到一个设备节点时它会读取节点的compatible属性然后遍历所有注册的driver找到匹配的驱动。匹配的规则是driver-of_match数组里有一个或多个 compatible 字符串如果设备节点的 compatible 和其中任何一个匹配就算匹配成功。匹配的优先级是先匹配of_match里靠前的条目再匹配靠后的。如果多个驱动都匹配同一个 compatible先注册的驱动优先。这里有一个常见的坑如果你在 dts 里写了一个 compatible但没有任何驱动匹配它dm 框架会报一个警告但不会阻止启动。这个设备会处于“未绑定”状态后续调用uclass_get_device时会失败。所以调试的时候如果发现某个设备找不到第一件事就是检查 compatible 是否拼写正确以及对应的驱动是否已经注册。4. board_init_r 中 dm 骨架搭建的实操流程4.1 initr_dmdm 框架的入口initr_dm是 dm 骨架搭建的起点。它的代码很短但做的事情很关键static int initr_dm(void) { int ret; ret dm_init_and_scan(false); if (ret) { debug(DM init failed: %d\n, ret); return ret; } return 0; }dm_init_and_scan的参数false表示不进行“早期扫描”。早期扫描是在board_init_f阶段用的那时候内存还没准备好只能扫描一部分设备。在board_init_r阶段我们用完整扫描。dm_init_and_scan内部会做几件事调用dm_init初始化 dm 框架的全局数据创建根设备root。调用dm_scan_fdt扫描设备树创建所有udevice。调用dm_scan_platdata扫描平台数据如果有的话。调用dm_scan_other扫描其他设备比如通过U_BOOT_DEVICE宏静态定义的设备。这一步完成后dm 设备树已经建好了所有设备都处于“已绑定、未 probe”状态。注意initr_dm必须在initr_serial之前执行。如果你在board_init_r里调整了顺序把initr_serial放到initr_dm前面串口驱动会因为找不到 dm 设备而 probe 失败系统会没有任何输出。4.2 initr_dm_devices批量 probe 关键设备initr_dm_devices的代码也很短static int initr_dm_devices(void) { return dm_probe_devices(); }dm_probe_devices会遍历 dm 设备树对每个设备调用device_probe。但不是所有设备都会 probe只有满足以下条件之一的才会设备标记了DM_FLAG_PRE_RELOC。设备标记了DM_FLAG_ACTIVE。设备是某个 uclass 的“关键设备”比如串口、定时器。probe 的顺序是按照设备树的层级来的父设备先 probe子设备后 probe。这是为了保证子设备 probe 时父设备已经就绪。比如 I2C 从设备 probe 时I2C 控制器必须已经 probe 完成。这个阶段 probe 的设备通常包括串口UCLASS_SERIAL定时器UCLASS_TIMER中断控制器UCLASS_IRQ时钟控制器UCLASS_CLK复位控制器UCLASS_RESET电源域UCLASS_POWER_DOMAIN这些设备是系统运行的基础必须在早期 probe。4.3 initr_serial串口设备的按需 probeinitr_serial是串口初始化的入口。它的代码大致如下static int initr_serial(void) { int ret; ret serial_init(); if (ret) { return ret; } return 0; }serial_init会调用uclass_get_device_by_seq或者uclass_get_device来获取串口设备然后调用device_probe触发 probe。这里有一个细节如果串口设备在initr_dm_devices阶段已经 probe 了serial_init会直接返回不会重复 probe。dm 框架会检查设备的flags如果已经 probe 过就跳过。串口 probe 的过程包括配置波特率、初始化 FIFO、设置数据位和停止位、使能收发。这些操作通过driver-probe和driver-ops里的函数完成。实操心得如果你在initr_serial之后发现串口没有输出先检查initr_dm是否成功再检查串口的 compatible 是否匹配到了驱动最后检查串口的时钟和引脚配置是否正确。这三个地方是最容易出问题的。4.4 initr_mmc存储设备的按需 probeinitr_mmc是 MMC 设备初始化的入口。它的代码大致如下static int initr_mmc(void) { int ret; ret mmc_initialize(gd-bd); if (ret) { return ret; } return 0; }mmc_initialize会遍历所有 MMC 控制器对每个控制器调用device_probe然后扫描总线上的 MMC 设备eMMC、SD 卡等。MMC 控制器的 probe 过程包括配置时钟、初始化控制器寄存器、设置总线宽度、扫描卡。如果卡检测成功还会读取卡的 CID、CSD 等信息。这里有一个常见的坑如果 MMC 控制器的时钟配置不对probe 会失败但错误信息可能不明显。你需要在mmc_initialize里加调试打印或者用dm_dump命令查看设备状态。4.5 initr_net网络设备的按需 probeinitr_net是网络设备初始化的入口。它的代码大致如下static int initr_net(void) { int ret; ret eth_initialize(); if (ret) { return ret; } return 0; }eth_initialize会遍历所有网络设备对每个设备调用device_probe然后注册到网络子系统。网络设备的 probe 过程包括配置 MAC 地址、初始化 PHY、设置链路速度、使能收发。如果 PHY 初始化失败网络设备会 probe 失败但系统可能不会报错只是网络不可用。注意网络设备的 probe 通常比较慢因为要等 PHY 链路协商完成。如果你在initr_net之后立即使用网络可能会因为链路还没就绪而失败。建议在initr_net之后加一个延时或者检查链路状态后再使用。5. 常见问题与排查技巧实录5.1 设备不 probe 的几种原因设备不 probe 是最常见的问题。根据我的经验原因通常有以下几种compatible 不匹配dts 里的 compatible 字符串和驱动里的of_match不一致。这是最常见的原因尤其是手写 dts 的时候很容易拼错。驱动未注册驱动没有编译进 u-boot或者没有用U_BOOT_DRIVER宏注册。检查drivers目录下的 Makefile 和 Kconfig确保驱动被编译。uclass 未注册uclass 没有用UCLASS_DRIVER宏注册或者注册的 ID 和驱动里的uclass_id不一致。设备被禁用dts 里的节点有status disabled或者u-boot,dm-pre-reloc没有设置。probe 顺序问题父设备没有 probe子设备无法 probe。检查设备树的层级关系确保父设备先 probe。排查方法用dm_dump命令查看设备树找到目标设备看它的flags和driver是否正确。如果driver是空的说明没有匹配到驱动如果flags里没有DM_FLAG_ACTIVE说明设备被禁用了。5.2 probe 失败但无报错的排查思路有时候设备 probe 失败了但串口没有任何输出。这种情况通常是因为 probe 函数里的错误没有被打印出来。排查思路在device_probe里加调试打印输出设备名称和返回值。检查driver-probe函数的返回值确保错误被正确传递。用dm_dump查看设备状态如果flags里有DM_FLAG_PROBED说明 probe 成功了如果没有说明 probe 失败了。检查 probe 函数里的硬件操作比如时钟使能、引脚配置、寄存器读写这些操作失败时可能不会打印错误。实操心得我习惯在device_probe的入口和出口加debug打印这样能清楚地看到哪些设备 probe 了哪些失败了。虽然会增加日志量但调试的时候非常有用。5.3 dm 骨架搭建顺序错误的典型表现dm 骨架搭建顺序错误的表现通常很隐蔽但有一些典型症状串口无输出initr_serial在initr_dm之前执行串口驱动找不到 dm 设备probe 失败。MMC 设备找不到initr_mmc在initr_dm_devices之前执行MMC 控制器没有 probe总线扫描失败。网络不可用initr_net在initr_dm之前执行网络设备没有绑定eth_initialize 找不到设备。系统挂死在board_init_r里手动调用了device_probe但设备依赖的父设备还没有 probe导致空指针访问。排查方法检查board_init_r里的initr_xxx调用顺序确保initr_dm在最前面initr_dm_devices紧随其后其他initr_xxx在后面。5.4 常见问题速查表问题现象可能原因排查方法解决方案串口无输出initr_dm 未执行或失败检查 initr_dm 返回值确保 initr_dm 在 initr_serial 之前设备不 probecompatible 不匹配用 dm_dump 查看 driver修正 dts 或驱动里的 compatibleprobe 失败无报错错误未打印在 probe 函数里加 debug检查硬件操作返回值MMC 找不到控制器未 probe检查 initr_mmc 顺序确保 initr_dm_devices 在 initr_mmc 之前网络不可用PHY 未就绪检查链路状态加延时或检查 PHY 配置系统挂死probe 顺序错误检查设备树层级确保父设备先 probe5.5 独家避坑技巧技巧一用 dm_dump 命令快速定位问题dm_dump是 u-boot 里最实用的调试命令之一。它会打印出完整的 dm 设备树包括每个设备的名称、uclass、driver、flags。你可以在board_init_r之后调用dm_dump看看目标设备是否在列表里driver 是否匹配flags 是否正确。技巧二在 dts 里加 debug 属性你可以在 dts 的设备节点里加一个u-boot,debug属性然后在驱动里读取这个属性如果存在就打印更多调试信息。这样可以在不修改代码的情况下针对特定设备开启调试。技巧三用 gpio 命令验证引脚配置如果设备 probe 失败怀疑是引脚配置问题可以用gpio命令手动设置引脚状态然后用万用表测量。这能快速确认引脚是否配置正确。技巧四检查时钟树很多设备 probe 失败是因为时钟没有使能。用clk命令查看时钟树确认目标设备的时钟是否已经开启。如果没有检查时钟控制器的驱动和 dts 配置。技巧五分阶段调试如果问题比较复杂可以把board_init_r里的initr_xxx调用逐个注释掉只保留initr_dm和initr_dm_devices然后逐步加回其他调用观察问题出现在哪个阶段。这种方法虽然笨但非常有效。6. 从骨架到血肉dm 驱动骨架的扩展与优化6.1 添加自定义 uclass 和驱动在实际项目中你经常需要添加自定义的 uclass 和驱动。比如你有一个特殊的传感器u-boot 里没有对应的 uclass你需要自己定义一个。添加自定义 uclass 的步骤在include/dm/uclass-id.h里添加新的 uclass ID。创建 uclass 驱动文件用UCLASS_DRIVER宏注册。在 uclass 驱动里定义post_bind、pre_probe、post_probe等回调可选。创建设备驱动文件用U_BOOT_DRIVER宏注册指定uclass_id。在 dts 里添加设备节点设置 compatible。添加自定义驱动的步骤创建驱动文件定义struct udevice_id数组设置 compatible。定义struct driver设置 name、id、of_match、probe、ops 等。用U_BOOT_DRIVER宏注册驱动。在 Makefile 和 Kconfig 里添加编译选项。注意自定义 uclass 的 ID 不要和已有的冲突。建议在uclass-id.h里找一个空闲的 ID或者使用UCLASS_LAST之后的 ID。6.2 优化 probe 顺序与依赖管理dm 框架支持通过u-boot,dm-pre-reloc和u-boot,dm-spl等属性来控制 probe 时机。但在复杂项目里这些属性可能不够用。你可以通过以下方式优化 probe 顺序使用dm_pre_probe回调在 uclass 驱动里实现pre_probe回调在设备 probe 之前做一些准备工作比如使能时钟、配置引脚。使用device_probe的返回值如果设备 probe 失败返回错误码dm 框架会记录这个错误后续调用uclass_get_device时会返回错误。你可以根据错误码判断问题原因。使用DM_FLAG_PRE_RELOC如果设备需要在重定位之前 probe设置这个标志。但要注意这个标志只对board_init_f阶段的早期扫描有效。使用DM_FLAG_ACTIVE如果设备需要立即 probe设置这个标志。dm 框架会在initr_dm_devices阶段 probe 它。6.3 性能优化减少 probe 时间在大型项目里dm 设备树可能包含几百个设备probe 时间可能达到几百毫秒。如果启动时间有要求需要优化 probe 流程。优化方法延迟 probe不是所有设备都需要在启动时 probe。对于不关键的设备可以延迟到使用时再 probe。dm 框架支持按需 probe你只需要在用到设备时调用uclass_get_device即可。并行 probe如果硬件支持可以并行 probe 多个设备。但 dm 框架本身是单线程的需要自己实现并行逻辑。减少设备树节点删除不需要的设备节点减少扫描和绑定时间。使用dm_scan_platdata如果设备信息是静态的可以用U_BOOT_DEVICE宏定义平台数据避免解析 dts。实操心得我在一个项目里把 probe 时间从 300ms 优化到 80ms主要靠延迟 probe 和减少设备树节点。延迟 probe 的效果最明显因为很多设备在启动阶段根本用不到。6.4 调试工具与技巧汇总除了dm_dumpu-boot 还提供了其他调试工具dm_tree打印 dm 设备树的层级结构比dm_dump更直观。dm_uclass打印所有 uclass 及其设备列表。dm_dev打印指定设备的详细信息。dm_drivers打印所有注册的驱动。这些命令在调试 dm 问题时非常有用。你可以在board_init_r之后调用它们查看 dm 框架的状态。另外你还可以在代码里调用dm_dump_all函数它会打印完整的 dm 设备树。这个函数在drivers/core/dump.c里定义可以直接调用。注意这些调试命令会增加代码大小正式发布时建议关闭。可以通过CONFIG_DM_DEBUG和CONFIG_CMD_DM来控制。7. 我个人在实际操作中的体会调 u-boot 的 dm 驱动骨架最深的体会就是顺序决定一切。board_init_r里的initr_xxx调用顺序不是随便排的每一个位置都有它的道理。你改一个顺序可能整个系统就起不来了。另一个体会是dm_dump 是你的好朋友。遇到设备不 probe、probe 失败、找不到设备这些问题第一件事就是dm_dump看看设备树的状态。很多时候问题一眼就能看出来。还有一点不要怕加调试打印。u-boot 的debug宏很好用你可以在关键路径上加打印观察执行流程。虽然会增加日志量但调试的时候非常有用。正式发布时把DEBUG关掉就行。最后分享一个小技巧如果你在移植新板子建议先把board_init_r里的initr_xxx调用精简到最少只保留initr_dm、initr_dm_devices、initr_serial确保系统能启动、串口能输出。然后再逐步加回其他调用每加一个就测试一次。这样能快速定位问题避免一次性引入太多变量。这个内容后续还可以这样扩展比如深入分析dm_init_and_scan的内部实现或者讲解如何用U_BOOT_DRIVER宏编写一个完整的 dm 驱动再或者分析 dm 框架在 SPL 阶段的裁剪和优化。这些都是值得单独写一篇的内容。