news 2026/9/30 1:53:04

U-Boot board_init_r 中设备模型骨架搭建与初始化时序详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
U-Boot board_init_r 中设备模型骨架搭建与初始化时序详解

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 tree)
  • initr_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_dm:dm 骨架的第一根桩

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 第一次扫描的边界:哪些设备会被 probe

initr_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等回调。

具体流程可以拆成几步:

  1. 遍历设备树中所有节点,跳过status = "disabled"的
  2. 对每个节点,查找匹配的driver(按compatible字符串匹配of_match表)
  3. 匹配成功则调用device_bind_with_driver_data创建udevice
  4. 绑定完成后,对需要立即 probe 的设备调用device_probe
  5. 触发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 控制器本身还没 probe,PMIC 作为 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 list
compatible 是否匹配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 新板子时救过我好几次。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:51:42

AI 替你打开网页并总结内容:BrowserSkill 5 分钟上手指南

AI 替你打开网页并总结内容:BrowserSkill 5 分钟上手指南 【免费下载链接】BrowserSkill Let AI agents use your real, logged-in browser without interrupting your work. CLI extension for browser automation across any shell-capable AI agent. 项目地址…

作者头像 李华
网站建设 2026/9/30 1:51:37

三步把 Switch 变成客厅 B 站大屏:wiliwili 客户端安装与上手

三步把 Switch 变成客厅 B 站大屏:wiliwili 客户端安装与上手 【免费下载链接】wiliwili 第三方B站客户端,目前可以运行在PC全平台、PSVita、PS4 、Xbox 和 Nintendo Switch上 项目地址: https://gitcode.com/GitHub_Trending/wi/wiliwili wiliwi…

作者头像 李华
网站建设 2026/9/30 1:49:58

存储系统可观测性终极实践:OpenTelemetry、eBPF 与微秒级指标全景透视

存储系统可观测性终极实践:OpenTelemetry、eBPF 与微秒级指标全景透视在现代微服务、海量分布式与高并发存储交织的复杂拓扑中,当线上出现一个“某用户支付接口耗时从 2ms 突发涨到 200ms”的性能抖动时,传统的监控手段往往只能两眼一抹黑&am…

作者头像 李华