
1. 从 board_init_r 说起为什么 dm 驱动骨架值得单独拎出来讲玩过 u-boot 移植的人都有一个共同的体感板子能不能起来串口能不能出字很多时候卡点不在 kernel而在 u-boot 自己。而 u-boot 里最容易被忽略、又最容易出问题的环节就是board_init_r这个函数。它处在重定位之后的 C 运行环境里是真正意义上的“第二阶段入口”从内存布局、外设初始化到驱动模型挂载全都在这里完成。很多人第一次看board_init_r的源码会觉得它像一条流水线init_sequence_r数组里挂了一堆函数指针挨个执行看起来平平无奇。但真正把 dmdriver model设备模型驱动骨架搭起来的关键动作恰恰藏在这条流水线里。我最早接触 u-boot 设备模型的时候踩过一个很典型的坑明明 dts 里写了节点驱动也编译进去了但board_init_r跑完之后设备就是没绑定上。后来把dm_init_and_scan的调用时机、gd-dm_root的初始化顺序、以及uclass的注册流程捋了一遍才发现问题出在骨架搭建的先后顺序上。u-boot 的 dm 不是 kernel 那种“设备树一解析驱动自动匹配”的完整体系它更像是一套轻量级的、按需初始化的框架board_init_r就是这套框架的“总装车间”。所以这篇内容我想干一件事把board_init_r里 dm 驱动骨架到底是怎么一步步搭起来的从代码路径、数据结构、初始化顺序到实际调试手段完整拆一遍。适合正在做 u-boot 移植、调试 dm 驱动、或者被probe顺序搞得头大的嵌入式工程师。不管你是刚接手一块新板子的新手还是已经能改 dts 但说不清 dm 内部流转的老手这篇都能帮你把这条链路串起来。核心关键词就几个u-boot、board_init_r、dm 驱动、设备模型、driver后面所有内容都围绕它们展开。2. board_init_r 在 u-boot 启动链路里的真实位置2.1 从 _start 到 board_init_r 的完整跳转路径要理解 dm 骨架为什么在board_init_r里搭得先知道这个函数是怎么被调到的。u-boot 的启动链路大致是这样的上电后先跑汇编入口_start设置栈指针、关中断、做一些 CPU 级别的初始化然后跳到board_init_f。board_init_f是在重定位之前运行的此时还在只读的初始内存里它的主要任务是规划内存布局、初始化gdglobal data结构体、准备重定位目标地址。等board_init_f跑完会调用relocate_code把整个 u-boot 镜像搬到 DRAM 的高端地址然后跳到重定位后的board_init_r。这个“重定位”动作非常关键。因为board_init_f阶段能做的事情很有限很多全局变量、堆内存、驱动数据结构都还没法正常使用。只有到了board_init_ru-boot 才真正拥有了可读写的完整运行环境。dm 设备模型需要动态分配内存来存放udevice、uclass、driver这些结构体所以它必须放在重定位之后。这也是为什么dm_init_and_scan不在board_init_f里调用的根本原因。从代码路径上看board_init_r位于common/board_r.c它的主体逻辑就是遍历init_sequence_r这个函数指针数组。这个数组里每一项都是一个init_fnc_t类型的函数返回值非零表示成功零表示失败并会触发 hang。数组的顺序是经过精心设计的因为很多初始化动作之间存在依赖关系。比如串口要先于控制台控制台要先于大部分调试输出而 dm 的初始化又必须早于那些依赖设备模型的驱动。2.2 init_sequence_r 数组里和 dm 相关的关键节点init_sequence_r里和 dm 骨架搭建直接相关的节点主要有这么几个我按执行顺序列一下initr_trace初始化 trace 功能和 dm 关系不大但会影响后续调试手段。initr_reloc标记重定位完成设置gd-flags里的相关标志位。initr_caches使能 cache保证后续内存访问性能正常。initr_malloc初始化 malloc 池这是 dm 动态分配内存的前提。initr_dm核心节点调用dm_init_and_scan搭建 dm 骨架。initr_bootstage记录启动阶段信息。initr_console_record控制台记录初始化。initr_serial串口初始化此时会通过 dm 来 probe 串口设备。可以看到initr_dm的位置非常靠前它排在initr_malloc之后、initr_serial之前。这个顺序不是随便定的没有 mallocdm 没法分配udevice没有 dm串口驱动就没法通过设备模型来绑定。所以initr_dm必须卡在这个位置。我在实际移植时遇到过有人为了调串口把initr_serial往前挪结果 dm 还没初始化串口 probe 直接失败板子连日志都出不来。这种顺序依赖是 u-boot 启动链路里最需要尊重的规则之一。2.3 为什么 dm 骨架不能更早或更晚搭建有人可能会问既然 dm 这么重要为什么不放到board_init_f里答案前面提过重定位之前没有可用的堆内存udevice和uclass这些结构体没法动态分配。而且board_init_f阶段gd结构体本身还在初始化中gd-dm_root这个字段还没有稳定的存储位置。那能不能更晚一点比如放到initr_serial之后也不行。因为 u-boot 现在越来越多的驱动都迁移到了 dm 框架下串口、GPIO、MMC、网口甚至一些时钟和复位控制器都依赖 dm 来管理和 probe。如果 dm 骨架没搭好这些驱动的probe函数根本不会被调用。更晚搭建意味着大量外设无法初始化系统基本就废了。所以initr_dm的位置是一个“刚刚好”的平衡点早于所有依赖 dm 的驱动晚于内存分配机制。理解这个位置背后的约束比死记代码顺序更有价值。3. dm 驱动骨架的核心数据结构拆解3.1 udevice、driver、uclass 三者的关系u-boot 的 dm 模型里最核心的三个结构体是udevice、driver和uclass。很多人一开始会被这三个概念绕晕我用一个生活化的类比来解释把 dm 想象成一个公司。driver是岗位说明书规定了“这个岗位能干什么活”uclass是部门比如“串口部”“GPIO 部”每个部门有自己的统一管理规则udevice是具体员工每个员工属于某个部门按照某份岗位说明书来工作。具体到代码里struct driver描述一个驱动的能力包含name、id、probe函数、remove函数、of_match匹配表、以及它所属的uclass。struct uclass描述一类设备的共性包含uclass_id、uclass_driver、设备链表等。struct udevice描述一个具体的设备实例包含它对应的driver指针、所属uclass指针、父设备指针、platdata 私有数据等。这三者的绑定关系是在 dm 扫描阶段建立的。driver通过U_BOOT_DRIVER宏注册到链接段里uclass通过UCLASS_DRIVER宏注册udevice则在解析设备树或板级文件时动态创建。board_init_r里的dm_init_and_scan就是把这些静态注册的 driver 和动态创建的 device 关联起来并触发probe。3.2 gd-dm_root 与根设备的作用gd结构体里有一个字段叫dm_root它指向 dm 树的根设备。这个根设备是一个特殊的udevice它不对应任何真实硬件而是作为整棵设备树的起点。所有其他设备都是它的子节点或者孙节点。dm_init_and_scan的第一步就是创建这个根设备并把它挂到gd-dm_root上。为什么需要一个根设备因为 dm 的设备树是有层级关系的。比如一个 I2C 控制器下面挂着 EEPROM一个 SPI 控制器下面挂着 Flash这些从属关系需要通过parent指针来维护。根设备作为顶层容器让所有设备都能找到自己的位置。在probe顺序上dm 会先 probe 父设备再 probe 子设备这个规则保证了依赖关系正确的初始化顺序。我在调试一个 I2C 温度传感器的时候发现传感器一直 probe 失败最后查出来是 I2C 控制器的probe没被调用。原因就是设备树里传感器的parent没有正确指向 I2C 控制器导致 dm 在扫描时把它挂到了根设备下面probe 顺序错乱。后来在 dts 里把层级关系写对问题立刻解决。这个经历让我意识到dm_root和父子关系不是摆设它直接决定了 probe 的先后顺序。3.3 driver 的注册机制与链接段布局u-boot 的 driver 注册用的是链接段linker list机制。U_BOOT_DRIVER宏展开后会把一个struct driver实例放到.u_boot_list_2_driver_1和.u_boot_list_2_driver_3之间的段里。链接脚本会把这些段收集起来形成一个连续的数组。dm 初始化时通过遍历这个数组就能拿到所有注册的 driver。这个机制的好处是不需要手动维护驱动列表新增驱动只要用宏注册就行。但缺点也很明显如果链接脚本没有正确收集这些段driver 就会“丢失”。我在一次移植中遇到过驱动明明编译进去了但 dm 扫描时找不到的情况最后发现是自定义的链接脚本里漏掉了.u_boot_list_2_driver_*段。这种问题很隐蔽因为编译不报错运行时才出问题。uclass的注册机制类似用的是.u_boot_list_2_uclass_*段。udevice则不是静态注册的它要么从设备树解析生成要么通过device_bind系列函数动态创建。理解这个“静态注册 动态创建”的组合是看懂 dm 骨架的关键。4. dm_init_and_scan 内部到底做了什么4.1 dm_init 的初始化动作dm_init_and_scan是initr_dm实际调用的函数它内部又分成dm_init和dm_scan两个阶段。dm_init负责搭建 dm 的基础设施主要做这几件事第一初始化gd-dm_root为 NULL确保后续创建根设备时状态干净。第二调用dm_init_priv初始化 dm 的私有数据区这块内存用来存放udevice和uclass的动态数据。第三创建根设备通过device_bind把一个特殊的根 driver 绑定到根设备上并设置gd-dm_root。第四初始化uclass链表为后续扫描做准备。这里有个细节值得注意根设备的创建是在dm_init阶段完成的而不是在dm_scan阶段。这意味着即使设备树解析失败根设备依然存在dm 的基本框架不会崩。这种设计提高了鲁棒性但也意味着如果根设备创建失败后面所有扫描都没有意义。我在调试时会在dm_init之后加一句打印gd-dm_root的日志确认根设备是否正常创建这个习惯帮我省了很多排查时间。4.2 dm_scan 的扫描流程与设备树解析dm_scan阶段负责把设备树里的节点转换成udevice并绑定到对应的 driver 上。它的流程大致是先扫描u-boot,dm-pre-reloc标记的节点这些是需要在重定位前就准备好的设备然后扫描普通节点创建udevice并调用device_bind绑定 driver最后扫描uclass相关的节点确保每个 uclass 都有对应的设备。设备树解析用的是ofnode系列 API。u-boot 有自己的扁平设备树FDT解析器和 kernel 的 OF 框架类似但不完全相同。dm_scan_fdt会遍历设备树节点对每个有compatible属性的节点尝试匹配 driver。匹配规则是看 driver 的of_match表里是否有对应的compatible字符串。匹配成功后创建udevice设置driver指针和uclass指针然后递归处理子节点。这里有个容易踩的坑u-boot 的设备树里节点需要显式标记u-boot,dm-pre-reloc或者u-boot,dm-spl之类的属性才能被特定阶段扫描到。如果忘了加这些标记设备在board_init_r阶段可能扫不到。我在移植一块新板子时I2C 控制器一直不工作最后发现是 dts 里漏了u-boot,dm-pre-reloc导致它在 pre-reloc 阶段没被初始化后续依赖它的设备全部失败。4.3 probe 的触发时机与顺序控制dm_scan完成后设备已经绑定好了但还没有probe。probe的触发有两种方式一种是显式调用device_probe另一种是通过uclass的post_probe或者init回调自动触发。在board_init_r阶段大部分设备的probe是在后续的initr_serial、initr_mmc等节点里按需触发的。probe 顺序遵循“父先子后”的原则。dm 在device_probe时会先检查父设备是否已经 probe如果没有会递归 probe 父设备。这个机制保证了依赖关系。但要注意如果设备树里的父子关系写错了probe 顺序就会乱。比如把一个 GPIO 设备挂到了错误的父节点下可能导致它在依赖的时钟控制器之前 probe从而失败。我个人的经验是在调试 probe 顺序问题时可以在device_probe函数里加打印输出设备名称和父设备名称这样能直观看到实际的 probe 顺序。u-boot 也提供了dm tree命令可以在运行时查看设备树结构和 probe 状态这个命令非常实用建议每个做 dm 调试的人都熟练掌握。5. 实操从零搭建一个 dm 驱动并验证骨架5.1 编写一个最小 dm 驱动光看代码不够得动手写一个才能真理解。下面我以一个虚拟的“demo”设备为例展示如何写一个最小 dm 驱动并让它被board_init_r的 dm 骨架扫描到。首先定义 driver#include common.h #include dm.h struct demo_priv { int value; }; static int demo_probe(struct udevice *dev) { struct demo_priv *priv dev_get_priv(dev); priv-value 42; printf(demo probe: %s, value%d\n, dev-name, priv-value); return 0; } static int demo_ofdata_to_platdata(struct udevice *dev) { printf(demo ofdata_to_platdata: %s\n, dev-name); return 0; } static const struct udevice_id demo_ids[] { { .compatible demo,device }, { } }; U_BOOT_DRIVER(demo) { .name demo, .id UCLASS_DEMO, .of_match demo_ids, .probe demo_probe, .ofdata_to_platdata demo_ofdata_to_platdata, .priv_auto_alloc_size sizeof(struct demo_priv), };这里有几个关键点U_BOOT_DRIVER宏把 driver 注册到链接段of_match表里的compatible要和设备树节点对应priv_auto_alloc_size告诉 dm 自动分配私有数据内存。ofdata_to_platdata是可选的用于从设备树读取配置。5.2 定义 uclass 并注册driver 需要一个 uclass 来归属。定义 uclass 的代码如下UCLASS_DRIVER(demo) { .id UCLASS_DEMO, .name demo, .post_probe demo_post_probe, };UCLASS_DRIVER宏把 uclass 注册到对应的链接段。post_probe是可选回调在所有设备 probe 完成后调用。uclass 的id需要在uclass-id.h里定义比如UCLASS_DEMO。这个 id 是 dm 内部识别 uclass 的唯一标识不能重复。5.3 在设备树里添加节点在板级 dts 文件里添加节点demo: demo10000000 { compatible demo,device; reg 0x10000000 0x1000; u-boot,dm-pre-reloc; };compatible要和 driver 的of_match对应reg是寄存器地址范围u-boot,dm-pre-reloc表示这个设备在重定位前就需要准备好。如果不需要 pre-reloc可以去掉这个属性。5.4 验证 dm 骨架是否正常工作编译烧录后在 u-boot 命令行里执行dm tree应该能看到 demo 设备出现在树里。执行dm uclass可以看到 demo uclass 的信息。如果设备没有出现检查这几个点driver 是否编译进镜像、链接脚本是否包含 driver 段、dts 节点是否被正确编译、compatible是否匹配。我在第一次写 dm 驱动时忘了在uclass-id.h里定义UCLASS_DEMO编译报错但错误信息不直观找了半天才发现。后来养成习惯新增 uclass 时先定义 id再写 driver 和 uclass 结构体最后加 dts 节点按这个顺序基本不会漏。6. 常见问题与排查技巧实录6.1 设备扫不到、probe 不执行的排查思路这是 dm 调试里最高频的问题。设备扫不到通常有以下几个原因现象可能原因排查方法dm tree 里没有设备dts 节点未编译进 dtb用 fdtdump 查看 dtb 内容dm tree 里有设备但无 driverdriver 未注册或 compatible 不匹配检查 U_BOOT_DRIVER 宏和 of_match 表设备有 driver 但 probe 未调用父设备未 probe 或 probe 顺序问题用 dm tree 查看父子关系加打印probe 返回错误驱动内部初始化失败检查 probe 函数返回值和硬件状态排查时我一般按“先看树、再看绑定、最后看 probe”的顺序来。dm tree命令能看到设备层级和 probe 状态dm uclass能看到 uclass 和 driver 的绑定情况。如果树里没有设备问题在 dts 或编译环节如果树里有设备但没 driver问题在 driver 注册或匹配如果都有但 probe 没跑问题在 probe 触发时机或父设备依赖。6.2 probe 顺序错乱导致的依赖失败probe 顺序错乱是另一个常见坑。比如一个 GPIO 驱动依赖时钟控制器但时钟控制器还没 probeGPIO 就先去 probe 了结果读寄存器失败。这种问题的根源通常是设备树里的父子关系不对或者u-boot,dm-pre-reloc标记缺失。解决方法是确保依赖关系在设备树里正确表达。如果 A 依赖 BA 应该是 B 的子节点或者 A 的 driver 在 probe 时显式调用device_probe(B)。u-boot 的 dm 不会自动分析依赖关系它只认父子层级。所以设计 dts 时要把层级关系想清楚。我遇到过一个典型案例MMC 控制器依赖一个固定的 GPIO 来做电源控制但 GPIO 节点和 MMC 节点是平级的导致 MMC probe 时 GPIO 还没准备好。后来把 GPIO 节点移到 MMC 节点下面作为子节点问题解决。这个经验说明dts 的层级不只是逻辑组织它直接影响运行时行为。6.3 内存分配失败与 priv_auto_alloc_size 配置dm 在创建udevice时会根据priv_auto_alloc_size自动分配私有数据内存。如果这个值设得太大或者 malloc 池太小就会分配失败。表现是 probe 时dev_get_priv返回 NULL或者直接 hang。排查方法是检查CONFIG_SYS_MALLOC_F_LEN的值确保 malloc 池足够大。在board_init_r阶段malloc 池已经初始化完成但如果设备数量多、私有数据大还是可能不够。我一般会在dm_init后打印 malloc 池的使用情况确认余量。另外priv_auto_alloc_size要和实际结构体大小一致。如果结构体里有指针还要考虑指针指向的内存是否需要额外分配。u-boot 的 dm 只负责分配结构体本身不负责结构体内部指针指向的内存。这个细节容易忽略导致 probe 时访问空指针。6.4 用 dm 命令快速定位问题u-boot 提供了一组 dm 相关命令熟练使用能大幅提升调试效率dm tree查看设备树结构和 probe 状态。dm uclass查看 uclass 列表和绑定情况。dm dev查看指定设备的详细信息。dm drivers查看已注册的 driver 列表。这些命令在CONFIG_CMD_DM开启后可用。我建议在调试阶段把这个配置打开正式发布时再根据空间决定是否保留。dm tree的输出里probe 成功的设备会标记[]未 probe 的标记[-]一眼就能看出哪些设备没起来。7. 一些移植现场的实战体会做 u-boot 移植这些年dm 骨架这块给我最大的感受是它看起来复杂但一旦理解了“静态注册 动态扫描 按需 probe”这个核心逻辑后面都是顺藤摸瓜。很多问题不是代码写错了而是顺序错了、层级错了、标记漏了。board_init_r里的initr_dm就像是一个总开关它把前面board_init_f准备好的内存和后面各个驱动的初始化连接起来。我个人的习惯是拿到一块新板子先把dm tree跑通确认根设备和主要控制器都在树里再去调具体驱动。这样能把问题分层是 dm 骨架没搭好还是某个驱动自身的问题。分层之后排查效率会高很多。另外设备树的u-boot,dm-pre-reloc标记一定要根据实际需求加。加多了会增大 pre-reloc 阶段的内存压力加少了会导致设备扫不到。我的做法是只给真正需要在重定位前使用的设备加这个标记比如串口、时钟、复位控制器其他设备放到普通扫描阶段。最后分享一个小技巧如果怀疑某个设备没被扫描到可以在dm_scan_fdt里加打印输出每个被扫描节点的compatible和扫描结果。这个打印量比较大但定位问题时非常有效。定位完记得去掉避免影响启动速度。