U-Boot 的board_init_r是很多嵌入式工程师眼里的黑盒——串口打印一刷而过设备该起来的都起来了但真让你说清楚dm驱动骨架是在哪一行、按什么顺序搭起来的多数人答不上来。我最早接触这块的时候也一样改个dts节点发现驱动没 probe加打印加到自己都晕最后才意识到问题出在board_init_r里那几段看似不起眼的initr_*调用顺序上。这篇就把board_init_r里设备模型driver model下文简称 dm的骨架搭建过程完整拆一遍从gd-flags的置位到dm_init_and_scan的两次扫描再到initr_dm_devices的收尾把每一步为什么放在这个位置讲透。适合已经能跑通 U-Boot、想搞明白 dm 初始化时序的嵌入式驱动开发者也适合正在往新板子上移植 dm 驱动、被 probe 顺序坑过的朋友。1. 先搞清楚 board_init_r 到底在什么时间点被调用1.1 从 _main 到 board_init_r 的交接U-Boot 的启动分两个大阶段board_init_f和board_init_r。前者跑在重定位之前主要干内存布局规划、gd结构体初始化这些活后者跑在重定位之后代码已经搬到 RAM 里可以放心调用各种复杂函数了。board_init_r的入口在common/board_r.c它的调用者是arch/*/lib/crt0.S里的_main_main在完成重定位、清 BSS、设置栈之后一个bl board_init_r就跳进来了。这个时间点很关键此时 DDR 已经可用gd-relocaddr指向新的 U-Boot 位置gd-malloc_base也准备好了堆内存可以正常分配。dm 框架里大量使用malloc来分配udevice、driver绑定结构所以 dm 的初始化必须放在堆可用之后。这也是为什么dm_init_and_scan不在board_init_f里调用的根本原因——那时候堆还没影呢。1.2 init_sequence_r 数组骨架的真正载体board_init_r函数体本身很短核心就一句initcall_run_list(init_sequence_r)。真正干活的是init_sequence_r[]这个函数指针数组它定义在common/board_r.c里按顺序排列了一堆initr_*函数。这个数组就是 dm 骨架的施工顺序表每一项都是一个初始化阶段。数组里和 dm 直接相关的项按出现顺序大致是这几个initr_dm早期 dm 初始化只做最基础的环境搭建initr_of_live如果开了 OF_LIVE把设备树展开成 live treeinitr_dm_devices扫描并 probe 所有驱动中间还夹着initr_serial、initr_console_record、initr_env等很多人以为 dm 是一次性初始化完的其实不是。U-Boot 故意把 dm 拆成先建骨架、后填血肉两段中间插入了串口、环境变量这些依赖 dm 但又必须先于完整扫描可用的东西。这个设计思路值得单独拎出来讲。1.3 为什么 dm 要分两阶段初始化设想一下如果 dm 一次性把所有驱动都 probe 完会有什么问题最直接的就是串口。串口驱动本身也是 dm 驱动但调试信息要靠串口输出如果串口 probe 之前 dm 扫描就崩了你连报错都看不到。所以 U-Boot 的做法是先用initr_dm建立一个最小的 dm 运行环境让serial这类基础驱动能提前 probe 出来等串口能打印了再走initr_dm_devices做全量扫描。这样即使后面某个驱动 probe 失败你至少能在串口上看到错误信息。这个先让日志通道可用再做重活的思路在嵌入式 bring-up 里非常常见我自己移植新板子时也一直沿用——先把串口打通再谈其他外设。2. initr_dmdm 骨架的第一根桩2.1 dm_init_and_scan 的第一次调用initr_dm的实现很简洁核心就是调用dm_init_and_scan(false)。注意这个false参数它对应的是dm_init_and_scan的pre_reloc_only形参。传false意味着不只处理重定位前的驱动所有驱动都可以纳入扫描范围。但这里有个细节虽然传了false这次调用并不会 probe 所有设备因为此时很多依赖还没就绪。dm_init_and_scan内部做了三件事dm_init、dm_scan_platdata如果没开 OF_CONTROL、dm_scan_fdt。dm_init负责初始化gd-dm_root、gd-uclass_root这两个根节点把 dm 的树根立起来。dm_scan_fdt则遍历设备树为每个有compatible属性的节点创建对应的udevice并尝试和U_BOOT_DRIVER注册的驱动做匹配。2.2 gd-dm_root 和 uclass_root 的建立dm_init里最关键的两行是创建dm_root和uclass_root。dm_root是整个设备树的根udevice所有设备节点最终都挂在它下面uclass_root是所有 uclass 的根每个 uclass比如UCLASS_SERIAL、UCLASS_GPIO都是它的子节点。这里有个容易踩的坑dm_root和uclass_root本身也是udevice它们的driver是root_driver和uclass_driver。如果你在dm_init之前就想访问某个 uclass比如uclass_get_device那必然失败因为uclass_root还没建。我见过有人在board_init_f阶段就想拿 GPIO结果一路返回-ENODEV查半天才发现是时序问题。2.3 第一次扫描的边界哪些设备会被 probeinitr_dm阶段的扫描重点是那些标记了DM_FLAG_PRE_RELOC或者被uclass声明为早期需要的设备。典型的就是串口。以ns16550为例它的驱动定义里有.flags DM_FLAG_PRE_RELOC所以能在重定位前就被识别。但真正让它 probe 的是initr_serial里显式调用的serial_init而不是dm_init_and_scan自动 probe 的。换句话说initr_dm主要完成的是建树 绑定probe 是后面按需触发的。这个区分很重要绑定bind只是把udevice和driver关联起来probe 才是真正调用驱动的.probe回调去初始化硬件。很多人把这两个概念混为一谈导致看日志时误以为驱动已经跑过了。3. initr_serial 与 initr_dm_devices 之间的时序玄机3.1 串口为什么必须夹在两次 dm 扫描中间init_sequence_r里initr_serial排在initr_dm_devices前面这不是随便排的。initr_serial会调用serial_init()而serial_init内部通过uclass_get_device_by_seq(UCLASS_SERIAL, 0, dev)拿到串口设备并 probe 它。此时uclass_root已经由initr_dm建好所以能成功拿到设备。如果顺序反过来先initr_dm_devices再initr_serial会怎样理论上也能跑但风险在于全量扫描时如果某个驱动 probe 失败并尝试打印错误而串口还没初始化这条错误信息就丢了。更糟的是某些平台的printf在串口未就绪时会走puts的 fallback 路径可能触发未定义行为。所以 U-Boot 把串口初始化提前本质是保证日志通道先于一切重活可用。3.2 initr_dm_devices 的全量扫描逻辑initr_dm_devices调用的是dm_init_and_scan(true)注意这次传的是true。这个参数会让扫描过程更彻底配合dm_scan_fdt的后续处理把所有还没 bind 的设备都补上并触发uclass的post_bind、post_probe等回调。具体流程可以拆成几步遍历设备树中所有节点跳过status disabled的对每个节点查找匹配的driver按compatible字符串匹配of_match表匹配成功则调用device_bind_with_driver_data创建udevice绑定完成后对需要立即 probe 的设备调用device_probe触发uclass的post_probe回调做 uclass 级别的收尾这里第 4 步的需要立即 probe是有条件的。默认情况下dm 采用惰性 probe——设备 bind 了但不一定马上 probe等第一次被uclass_get_device之类调用时才 probe。但有些设备标记了DM_FLAG_ACTIVE_DMA或者 uclass 有post_probe需求会在扫描阶段就 probe。3.3 一个真实的 probe 顺序踩坑案例我之前调一块新板子I2C 上的 PMIC 一直 probe 失败报-EPROBE_DEFER。查了半天发现是 I2C 控制器本身还没 probePMIC 作为 I2C 子设备自然拿不到总线。问题出在设备树里 I2C 控制器节点的status被写成了disabled而 PMIC 节点是okay。dm 扫描时跳过了 disabled 的 I2C 控制器但 PMIC 节点还在于是 PMIC 尝试 probe 时找不到父总线返回-EPROBE_DEFER。这个坑的教训是dm 的 probe 顺序强依赖设备树的层级和status属性。父节点 disabled子节点就算 okay 也没用。排查这类问题时我习惯先dm tree看一眼设备树在 dm 里的实际形态再dm uclass确认 uclass 绑定情况比盲猜高效得多。4. dm 骨架里的 uclass 机制驱动分类的底层逻辑4.1 uclass 是什么为什么需要它uclass 是 dm 框架里对一类设备的抽象。比如所有串口都属于UCLASS_SERIAL所有 GPIO 都属于UCLASS_GPIO。它的价值在于提供统一的操作接口你不需要知道具体是ns16550还是pl011只要拿到UCLASS_SERIAL的设备就能调serial_putc。从实现上看每个 uclass 对应一个uclass_driver里面定义了post_bind、post_probe、pre_remove等回调以及per_device_auto这样的自动分配大小。当设备 bind 到某个 uclass 时dm 会按per_device_auto给udevice-uclass_priv分配内存驱动可以直接用这块空间存私有数据不用自己 malloc。4.2 uclass 的注册时机uclass 的注册靠UCLASS_DRIVER宏展开后是一个linker_list项在链接阶段就被收集到.u_boot_list段里。dm_init里会遍历这个段把每个uclass_driver实例化成uclass节点挂到uclass_root下。所以 uclass 的注册是静态注册、运行时实例化不依赖设备树。这里有个细节UCLASS_DRIVER宏里的.id字段必须唯一且要和U_BOOT_DRIVER里的.id对应。如果两个驱动用了同一个 uclass id 但 uclass 本身没注册bind 时会报-ENODEV。我遇到过有人复制驱动代码时忘了改.id结果两个驱动抢同一个 uclass行为诡异。4.3 uclass 与 driver 的匹配规则匹配发生在device_bind阶段。dm 拿到一个设备树节点后先解析compatible然后在所有U_BOOT_DRIVER里找of_match表能匹配上的。匹配成功后驱动的.id字段决定它属于哪个 uclass。如果.id是UCLASS_SERIAL那这个设备就挂到 serial uclass 下。匹配失败的情况也常见设备树节点写了compatible vendor,foo但没有任何驱动的of_match里有这个字符串dm 会跳过这个节点不报错也不 bind。这种静默跳过很容易让人误以为驱动加载了实际根本没匹配上。排查时用dm tree看节点是否出现在树里是最直接的办法。5. 从 board_init_r 反推 dm 驱动的移植要点5.1 新板子移植 dm 驱动的检查清单基于上面拆解的骨架移植 dm 驱动时可以按这个顺序自查检查项位置常见问题驱动是否注册U_BOOT_DRIVER宏宏拼写错误导致没进 linker listcompatible 是否匹配of_match表设备树字符串和驱动不一致uclass 是否注册UCLASS_DRIVER宏uclass id 冲突或缺失设备树 status节点属性父节点 disabled 导致子节点失效probe 依赖顺序-EPROBE_DEFER处理依赖的总线未就绪私有数据分配per_device_auto大小不够导致越界这张表是我自己移植时总结的基本覆盖了 90% 的驱动不工作问题。按顺序查一遍比漫无目的地加打印快得多。5.2 用 dm 命令做运行时诊断U-Boot 编译时如果开了CONFIG_CMD_DM就能在命令行用dm系列命令。最常用的三个dm tree打印完整的 dm 设备树看层级和绑定情况dm uclass列出所有 uclass 及其下的设备dm devres查看设备资源分配排查内存问题我调 probe 顺序问题时习惯先dm tree确认设备在树里的位置再dm uclass看 uclass 绑定最后结合-EPROBE_DEFER的返回值定位依赖链。这套组合拳比单纯看代码高效太多。5.3 关于 initr_dm 和 initr_dm_devices 的取舍有人会问能不能把两次 dm 初始化合并成一次技术上可以但不建议。合并后你就失去了串口先于全量扫描可用这个保障一旦某个驱动 probe 崩溃调试信息可能丢失。U-Boot 官方把这个拆分保留至今是有充分工程理由的。如果你在做极度精简的 bootloader确实想省掉一次扫描那至少要保证串口在 dm 初始化之前能用。但说实话省这点开销意义不大dm 扫描本身很快真正的耗时在驱动 probe 的硬件操作上。6. 几个容易被忽略的 dm 初始化细节6.1 gd-flags 里的 DM 相关标志gd-flags里有几个和 dm 相关的位比如GD_FLG_DM_DEVRES表示设备资源管理已启用。这些标志在dm_init里被设置后续驱动可以通过gd-flags判断 dm 是否就绪。如果你在驱动里看到if (!(gd-flags GD_FLG_DM_DEVRES))这样的判断就是在做这个检查。6.2 of_live 对 dm 扫描的影响开了CONFIG_OF_LIVE后设备树会被展开成struct device_node的 live tree而不是每次解析 dtb。initr_of_live就负责这个展开。展开后dm_scan_fdt遍历的是 live tree速度更快但内存占用更高。嵌入式项目里要不要开取决于你的 RAM 预算和启动时间要求。我一般在小 RAM 平台上关掉大 RAM 平台开着省解析时间。6.3 dm 扫描失败时的错误处理dm_init_and_scan返回非零时initr_dm会直接panic。这意味着 dm 骨架搭建失败是致命错误不会继续往下走。这个设计是合理的——dm 是后续所有驱动的基础骨架塌了后面全完。但调试时要注意panic 信息可能因为串口还没完全就绪而打印不全必要时用 JTAG 抓。7. 把 dm 骨架理解透之后能做什么理解board_init_r里 dm 骨架的搭建过程最直接的好处是排查 probe 问题时不再靠猜。你知道设备是在哪一步 bind 的、哪一步 probe 的、uclass 是什么时候挂上去的就能精准定位问题出在哪个环节。再往深一层你可以基于这套机制做定制。比如实现一个自定义 uclass把一类特殊外设统一管理或者在post_probe回调里插入自己的初始化逻辑做板级定制而不改驱动源码。这些进阶玩法都建立在对骨架时序的清晰理解上。我自己在最近一个项目里就是靠重写某个 uclass 的post_probe把原本散落在各处的板级初始化代码收拢到一处维护成本降了不少。这种顺着框架的设计意图走的改法比硬改驱动内部逻辑要稳得多。最后分享一个我常用的调试技巧在dm_init_and_scan入口和出口各加一条debug打印配合CONFIG_DEBUG_UART的早期串口能把 dm 扫描的完整时间窗口框出来。这样即使全量扫描中途出问题你也能从日志里看出是扫描前还是扫描后崩的缩小排查范围。这个技巧在 bring-up 新板子时救过我好几次。
阅读完成 · 觉得有帮助?