1. 从 board_init_r 这个总装车间说起玩过 u-boot 的人都知道板级初始化走到board_init_r这个函数的时候整个系统已经完成了最底层的搬运——代码从 Flash 搬到了 RAM栈已经搭好C 运行环境完全就绪。接下来要干的事情就是把整个系统的外设家当一件件盘清楚、初始化好、挂上号最后交给内核或者进入命令行。这个过程你可以把它想象成一家工厂的总装车间零件各种外设控制器已经堆在仓库里了现在要按图纸把它们装到流水线上接好线通上电让整条产线跑起来。而 u-boot 的driver model设备模型简称 dm就是这套总装流程的图纸 工装夹具。它规定了每个设备怎么描述、怎么绑定驱动、怎么按顺序初始化、怎么在运行时被找到。很多人第一次看board_init_r里的 dm 相关代码会觉得云里雾里dm_init_and_scan、dm_scan_platdata、dm_scan_fdt、dm_init这些函数一个接一个到底谁先谁后、各自干了什么、驱动骨架是在哪一步搭起来的这篇文章就带你把这套骨架的搭建过程彻底拆开看。这篇文章适合谁如果你已经能编译 u-boot、能看懂基本的板级配置文件但对 dm 的初始化流程只有模糊印象那这篇就是写给你的。如果你正在给自己的板子移植 u-boot发现设备死活 probe 不起来或者 probe 顺序不对导致依赖报错那这篇更值得你逐段对照。我会尽量用总装车间这个类比贯穿全文把抽象的代码流程讲成一条能看得见的生产线。先给一个全局的结论让你心里有张地图board_init_r里的 dm 初始化本质上分两大阶段——第一阶段是建骨架把设备树或平台数据里的设备节点解析出来创建udevice结构体绑定对应的驱动形成一个树状的设备链表第二阶段是通电试车按依赖顺序逐个 probe 设备让驱动真正去操作硬件。骨架没搭好试车必然出问题骨架搭好了但顺序错了试车一样会炸。下面我们一层层剥。2. 骨架搭建前的准备工作gd 里的 dm 根节点从哪来2.1 为什么需要一个根设备在讲board_init_r之前必须先搞清楚一个前提dm 模型里的所有设备最终都挂在一棵树上而这棵树必须有一个根。这个根就是gd-dm_root。gd是 u-boot 的全局数据指针每个架构都有自己的global_data结构体dm 相关的字段就藏在里面。为什么非要有个根因为 dm 的设备查找、驱动绑定、probe 顺序全都依赖父子关系。比如一个 I2C 控制器下面挂着 EEPROMEEPROM 的 probe 必须等 I2C 控制器 probe 完成。这种依赖关系如果不用树来表达就得靠人工维护一张顺序表板子一多、外设一杂维护成本直接爆炸。用树结构父节点天然就是子节点的依赖probe 时先父后子逻辑自洽。根设备本身通常不对应任何真实硬件它是一个虚拟设备driver 是root_driver在drivers/core/root.c里定义。它的uclass是UCLASS_ROOT。你可以把它理解成总装车间里那根总电源母线本身不是一台机器但所有机器都得从它这里接电。2.2 gd-dm_root 的诞生时机关键点来了gd-dm_root并不是在board_init_r里才创建的。它更早在dm_init这个函数里就已经被建立。而dm_init的调用时机取决于你的配置。在典型的 ARM 板子上board_init_f阶段末尾会调用dm_init如果开启了CONFIG_DM或者在某些流程里由board_init_r开头的initr_dm来触发。这里有个非常容易踩的坑不同 u-boot 版本、不同架构dm_init的调用位置不一样。我见过有人在board_init_r里死找dm_root的赋值找了半天没找到因为在他那个版本里根节点早在board_init_f就建好了。所以你在读代码时第一件事是确认你手上这版 u-boot 的dm_init到底在哪调用。用grep -rn dm_init(搜一下比对着文档猜靠谱得多。dm_init干的事情其实很朴素分配一个udevice给根设备把gd-dm_root指向它初始化gd-uclass_root链表然后调用device_bind_driver把root_driver绑上去。注意这里只是绑定还没有 probe。绑定和 probe 是两码事这是理解整个 dm 流程的分水岭。提示绑定bind是建立设备-驱动的关联关系创建数据结构probe 是真正调用驱动的 probe 函数去初始化硬件。骨架搭建阶段主要在做 bind试车阶段才做 probe。2.3 uclass 链表设备的分类货架除了设备树dm 还维护了一套uclass设备类机制。每个设备都属于某个 uclass比如所有 I2C 控制器都属于UCLASS_I2C所有 GPIO 都属于UCLASS_GPIO。gd-uclass_root是一条链表头所有 uclass 按需创建并挂上去。为什么要多这一层因为很多时候你拿到一个设备并不关心它是哪块板子上的第几个控制器你只关心给我一个 I2C 总线。uclass 就是提供这种按类型查找能力的货架。总装车间里零件不是按来自哪个供应商摆放的而是按螺丝归螺丝、轴承归轴承分类上架装配工人伸手就能拿到对的类型。uclass 就是这个分类货架。在骨架搭建阶段每 bind 一个设备如果它对应的 uclass 还不存在dm 就会自动创建这个 uclass 并挂到uclass_root上。这个过程是惰性的用到才建避免了一上来就创建一堆空货架。3. board_init_r 里 dm 扫描的三条路径3.1 dm_init_and_scan总入口board_init_r里跟 dm 骨架搭建最直接相关的就是dm_init_and_scan这个函数有些版本叫initr_dm或直接内联展开。它是整个扫描流程的总入口内部会依次走三条路径去发现设备平台数据扫描dm_scan_platdata从静态定义的U_BOOT_DEVICE宏或者struct driver_info数组里发现设备。这是给那些不支持设备树、或者需要在设备树之前就初始化的设备准备的。设备树扫描dm_scan_fdt解析设备树把每个有compatible属性的节点变成udevice。这是现代 u-boot 的主力路径。预重定位扫描dm_scan_pre_reloc处理那些需要在重定位之前就绑定好的设备通常跟DM_FLAG_PRE_RELOC标志配合。这三条路径不是随便排的顺序有讲究。平台数据先扫是因为有些极早期设备比如串口调试口必须在设备树解析之前就能用设备树扫描是主体预重定位扫描处理特殊标志的设备。你如果自己加了设备但死活扫不到先确认它走的是哪条路径再确认对应的宏或设备树节点写对了没有。3.2 dm_scan_fdt 的递归下降设备树扫描是重头戏。dm_scan_fdt会从根节点开始递归遍历每一个子节点。对每个节点它做几件事检查节点是否有compatible属性没有就跳过比如纯容器节点。检查节点的status如果是disabled就跳过。调用lists_bind_fdt在已注册的驱动列表里找匹配的驱动。找到驱动后调用device_bind_with_driver_data创建udevice并挂到父设备下。这里的关键是匹配。u-boot 维护了一张驱动表每个驱动用U_BOOT_DRIVER宏注册里面带着of_match数组列出自己能匹配哪些compatible字符串。扫描时拿节点的compatible去逐个比对匹配上就绑定。我踩过的一个坑设备树里compatible写了个拼写错误的字符串比如把vendor,uart写成vendor,urart结果驱动匹配不上设备静默消失没有任何报错。u-boot 在匹配失败时通常只是跳过不会大喊大叫。所以设备不见了的时候第一件事就是核对compatible字符串和驱动of_match表是否一字不差。3.3 绑定过程中的父子关系建立绑定不是简单地把设备塞进一个全局数组而是要建立父子关系。device_bind_with_driver_data会做这几步分配udevice结构体。设置dev-parent指向父设备扫描时传入的 parent。把dev挂到父设备的child_head链表上。查找或创建对应的 uclass把dev挂到 uclass 的dev_head链表上。调用驱动的bind回调如果有。第 5 步很多人忽略。驱动的bind回调在绑定阶段就会被调用而不是等到 probe。有些驱动会在bind里做资源分配、读取设备树属性、设置私有数据。如果你在bind里访问了还没初始化的硬件就会出问题。记住bind 阶段硬件可能还没上电别在这里碰寄存器。父子关系建立好之后整棵树就成型了。这时候你如果打印gd-dm_root的子节点能看到一个层次分明的设备树镜像。骨架到这一步才算真正搭起来。4. 驱动是怎么挂到骨架上的U_BOOT_DRIVER 与 of_match4.1 U_BOOT_DRIVER 宏展开后到底是什么很多人写驱动就是照抄一个模板把U_BOOT_DRIVER(xxx)一填就完事但从没想过这个宏展开后是什么。它本质上定义了一个struct driver结构体并把它放进一个特殊的链接段section里。链接器会把所有驱动结构体收集到连续的内存区域u-boot 启动时通过遍历这个段就能拿到所有已注册驱动。这个设计很巧妙不需要手动维护驱动列表加一个驱动文件、写一个U_BOOT_DRIVER链接进去就自动上架了。总装车间的零件供应商目录是自动生成的新供应商只要把货送到目录里自动多一条。struct driver里几个关键字段字段作用常见坑name驱动名用于日志和查找重名会导致行为不确定iduclass 的 id写错会挂到错误的货架of_match设备树匹配表拼写错误导致静默不匹配bind绑定回调别在这里碰硬件probe探测回调真正初始化硬件的地方ops操作函数集通过 uclass 层调用4.2 of_match 表的匹配逻辑of_match是一个struct udevice_id数组每项包含compatible字符串和一个可选的data。匹配时dm 拿设备树节点的compatible属性可能多个去和每个驱动的of_match逐项比对第一个匹配上的胜出。这里有个细节匹配是有优先级的取决于驱动注册的顺序。如果两个驱动都声明能匹配同一个compatible谁先被遍历到谁赢。这在实际项目中会导致诡异问题你新加了一个驱动想覆盖旧的结果旧驱动先匹配上了。解决办法是确保compatible字符串唯一或者用更具体的字符串。另外of_match里的data字段可以传一个整数给驱动驱动在probe里通过dev_get_driver_data拿到。这个技巧常用于同一个驱动支持多个变体芯片用data区分不同型号的寄存器偏移。4.3 uclass 的自动创建与 ops 转发当第一个属于某 uclass 的设备被绑定时dm 会检查这个 uclass 是否已存在不存在就创建。uclass 创建时会调用UCLASS_DRIVER注册的init回调如果有并初始化ops。uclass 层的ops是 dm 的一大精髓。以 I2C 为例UCLASS_I2C定义了一套标准操作i2c_xfer、i2c_set_bus_speed等。具体某个 I2C 控制器驱动实现自己的i2c_opsuclass 层负责把通用调用转发到具体设备。这样上层代码调用i2c_read时根本不需要知道底下是哪个控制器uclass 自动路由。这个转发机制在骨架搭建阶段就已经准备好了设备绑定时它的ops被挂到 uclass 的ops链表上调用时按设备查找对应的ops。所以骨架搭得好不好直接决定了后续所有外设操作能不能顺畅路由。5. probe 顺序骨架搭好后怎么通电试车5.1 惰性 probe 与主动 probe骨架搭好之后设备并不会自动全部 probe。u-boot 采用惰性 probe策略只有当你真正要用某个设备时才去 probe 它。比如你调用i2c_get_busdm 发现这个 I2C 控制器还没 probe就触发 probeprobe 完成后再返回。但有些设备必须在启动早期就 probe比如串口、定时器、看门狗。这些设备通过DM_FLAG_PRE_RELOC标志或者uclass的pre_probe机制在board_init_r的特定阶段被主动 probe。惰性 probe 的好处是启动快用不到的设备不浪费时间。坏处是问题暴露得晚一个设备可能到系统跑了一半才 probe一 probe 就崩这时候排查范围很大。所以移植阶段我建议临时打开CONFIG_DM_DEBUG把 probe 过程打出来心里有数。5.2 依赖顺序是怎么保证的probe 顺序靠两样东西保证父子关系和uclass 的post_probe/pre_probe钩子。父子关系是天然的依赖probe 一个设备前dm 会先确保它的父设备已经 probe。比如 EEPROM 挂在 I2C 控制器下probe EEPROM 时会先 probe I2C 控制器。这个递归逻辑在device_probe里实现它会沿着parent链往上找逐个 probe。但父子关系不够用。有些依赖是平级的比如一个 GPIO 控制器需要先有一个时钟控制器给它供时钟但它们在设备树里可能是兄弟节点。这时候就要靠uclass的钩子或者clk子系统的显式依赖声明。u-boot 的clk、reset、power domain这些子系统都实现了自己的依赖处理。我遇到过一个典型问题GPIO 控制器 probe 时去申请时钟但时钟驱动还没 probe导致申请失败。解决办法是在 GPIO 驱动的probe里显式调用clk_get_by_index这个调用会触发时钟设备的惰性 probe从而保证顺序。关键是要用 dm 提供的 API 去获取资源而不是直接访问寄存器API 内部会处理依赖。5.3 probe 失败的传播与排查probe 失败会返回负的错误码dm 会把这个设备标记为未 probe并记录错误。如果这个设备是别的设备的依赖错误会向上传播最终可能导致整个子系统不可用。排查 probe 失败我的经验是分三步看返回值-ENODEV通常是设备没找到或驱动没匹配-EINVAL通常是参数或设备树属性有问题-EPROBE_DEFER是依赖还没就绪dm 会稍后重试。看设备树确认节点status是okaycompatible拼写正确必需的属性如reg、clocks都在。加日志在驱动的probe开头加debug打印确认到底进没进 probe进了之后卡在哪一步。-EPROBE_DEFER是个好东西它让驱动可以声明我现在缺依赖等会儿再来。dm 会把这个设备放回待 probe 队列等依赖就绪后重试。但要注意如果依赖永远不就绪就会陷入无限重试或者最终超时。所以看到-EPROBE_DEFER不要高兴太早要确认依赖链最终能闭环。6. 移植实战给一块新板子搭 dm 骨架的完整流程6.1 从设备树开始而不是从代码开始新手移植 u-boot最容易犯的错是先写驱动代码再补设备树。正确的顺序反过来先把设备树写对让 dm 能扫出设备再让驱动去匹配。具体做法从参考板子的设备树拷一份删掉不需要的外设保留最基本的 SoC 节点CPU、内存、时钟、中断控制器、串口。然后逐个添加你要用的外设节点每加一个就编译一次用dm tree命令如果开了CONFIG_CMD_DM看设备有没有被扫出来。dm tree是移植阶段的神器它把当前 dm 树打印出来包括每个设备的层级、uclass、驱动名、probe 状态。骨架搭得对不对一眼就能看出来。我习惯每加一个外设就跑一次dm tree确认它出现在正确的位置、绑定了正确的驱动。6.2 驱动匹配不上的五种常见原因设备扫出来了但驱动没绑上或者绑上了但 probe 失败原因通常在这五类里compatible 不匹配设备树字符串和of_match表对不上包括大小写、连字符、厂商前缀。驱动没编译进去Makefile里没加或者Kconfig没选导致驱动根本没注册。uclass id 写错驱动挂到了错误的货架查找时找不到。设备树节点 status 是 disableddm 直接跳过。依赖的父设备没就绪父设备 probe 失败子设备跟着失败。这五类我按出现频率排了序compatible 不匹配占了一半以上。所以遇到问题先查字符串别急着怀疑代码逻辑。6.3 用 dm tree 和 dm uclass 验证骨架dm tree看层级dm uclass看分类。两个命令配合使用能快速定位问题。比如你发现某个 I2C 控制器在dm tree里存在但dm uclass里UCLASS_I2C下没有它那说明 uclass 绑定出了问题多半是驱动的id字段写错了。还有一个技巧dm tree输出里每个设备前面有个标记表示 probe 状态。未 probe 的设备标记不同probe 失败的会有额外提示。移植阶段盯着这个标记能省下大量调试时间。提示dm tree和dm uclass需要CONFIG_CMD_DM正式发布时可以关掉省空间但移植阶段强烈建议打开。7. 几个让我印象深刻的坑与经验7.1 设备树里 reg 属性缺失导致的静默失败有一次移植一个 SPI Flash设备树节点写了compatible和spi-max-frequency唯独漏了reg。dm 扫描时节点被扫出来了驱动也匹配上了但 probe 时读reg拿到默认值 0结果访问了错误的片选。这种问题不会报错只会表现为读写数据不对排查起来非常费劲。教训设备树节点的必需属性一个都不能少。移植时对照驱动代码里dev_read_*的调用把需要的属性列个清单逐个核对。7.2 probe 顺序依赖导致的间歇性失败有个项目里网络 PHY 的复位 GPIO 由某个 GPIO 扩展芯片控制而那个扩展芯片挂在 I2C 上。启动时偶尔网络起不来偶尔又正常。查了很久才发现是 probe 顺序问题PHY 驱动 probe 时去拉复位 GPIO但 GPIO 扩展芯片还没 probe拉了个寂寞。解决办法是在 PHY 驱动里用gpio_request_by_name获取 GPIO这个 API 会触发 GPIO 设备的惰性 probe保证顺序。永远不要直接操作 GPIO 寄存器用 dm 的 GPIO API这是血的教训。7.3 别在 bind 里做 probe 的事前面提过但值得再强调。有个同事在驱动的bind回调里初始化了硬件寄存器结果在某些启动路径下bind发生在时钟还没使能的时候直接挂死。bind只应该做数据结构的准备硬件操作全部放到probe。7.4 关于 CONFIG_DM 的渐进式迁移老代码迁移到 dm 是个渐进过程。u-boot 支持部分设备用 dm、部分用老式初始化。迁移时不要想着一次全换先把新加的外设用 dm 写老外设保持不动等稳定了再逐个迁移。board_init_r里 dm 初始化和老式初始化是可以共存的顺序上 dm 先跑老式后跑。8. 把骨架思维用到极致回过头看board_init_r里的 dm 初始化核心就三件事建根、扫设备、绑驱动。根在更早的阶段就建好了board_init_r主要在做扫描和绑定probe 则是按需触发。理解了这三件事的先后和依赖再看那些函数调用就不会晕。我个人的体会是dm 这套东西初看复杂但一旦你把设备树是图纸、udevice 是零件、driver 是工装、uclass 是货架、probe 是通电这个类比刻进脑子里读代码就变成了看总装流程每一步都能对上号。移植新板子时我现在的习惯是先花半天把设备树理清楚用dm tree反复验证骨架骨架对了后面写驱动就是填probe函数的事效率比一上来就啃代码高得多。最后分享一个我常用的小技巧在board_init_r里 dm 扫描完成后临时加一行打印gd-dm_root的子设备数量和你的设备树节点数量对一下。数量对不上说明有节点被跳过顺着dm tree找基本十分钟内能定位。这个土办法在移植初期帮我省了无数时间。