news 2026/10/2 11:24:52

U-Boot board_init_r 中 DM 设备模型骨架搭建全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
U-Boot board_init_r 中 DM 设备模型骨架搭建全解析

1. 从 board_init_r 这个"总装车间"说起

很多人第一次翻 u-boot 源码,看到board_init_r这个函数,第一反应是"这不就是个初始化函数吗,挨个调用一遍就完事了"。我当初也是这么想的,直到有一次板子起不来,串口只打印到Board init就卡死,我才意识到这个函数远没有表面看起来那么简单。它其实是整个 u-boot 从"裸奔状态"切换到"设备模型驱动状态"的分水岭,dm(driver model)的骨架就是在这里被真正搭起来的。

先把结论摆出来:board_init_r是 u-boot 第二阶段(重定位之后)的主初始化入口,它承担的核心任务之一,就是把 dm 设备模型从"内存里的一堆描述结构"变成"可以真正被驱动绑定、探测、使用的活体"。你后面能调用uclass_get_device、能通过dev->ops拿到操作函数、能让网卡、MMC、串口各就各位,全靠这个函数里那几行看起来不起眼的调用。

这篇文章适合谁看?如果你已经能编译 u-boot、能烧录、能看串口 log,但对 dm 模型"什么时候初始化、谁先谁后、绑定和探测到底在哪一步发生"始终模模糊糊,那这篇就是写给你的。我会沿着board_init_r的执行顺序,把 dm 骨架的搭建过程一层层剥开,讲清楚每一步在干什么、为什么这么排、以及我踩过的那些坑。

需要提前说明的是,u-boot 版本差异比较大,我这里以主流的 dm 成熟版本(2018 年之后基本稳定)为基准,不同厂商的 BSP 会有裁剪和魔改,但主干逻辑是一致的。你对照自己手上的代码看,大方向不会错。

2. board_init_r 里 dm 初始化的真实调用链

2.1 先搞清楚 board_init_r 到底在哪个阶段跑

要理解 dm 骨架怎么搭,得先知道board_init_r运行在什么时间点。u-boot 的启动分两个大阶段:第一阶段是board_init_f,跑在重定位之前,此时代码还在只读的 flash 或者 ROM 里,内存布局还没最终确定;第二阶段就是board_init_r,跑在重定位之后,代码已经搬到 RAM 里,堆栈、全局数据都就位了。

这个区别非常关键。因为 dm 模型需要动态分配内存来存放struct udevice、struct uclass、struct driver的绑定关系,如果内存管理还没就绪,这些结构根本没法建。所以 dm 的正式初始化必须放在board_init_r里,而不是board_init_f。有些新手会问"为什么不在第一阶段就把驱动都初始化好",答案很简单:第一阶段连 malloc 都还不能正常用,你拿什么去建设备树。

board_init_r的典型结构大致是这样一条主线(不同版本函数名略有出入):

void board_init_r(gd_t *new_gd, ulong dest_addr) { // 各种早期基础设施初始化 ... initr_dm(); // dm 骨架搭建的关键入口 ... initr_serial(); // 串口在 dm 之后才能真正工作 ... initr_net(); // 网络依赖 dm 里的 eth 设备 ... main_loop(); // 进入命令行或启动内核 }

注意initr_dm()的位置,它排在很多外设初始化之前。这个顺序不是随便定的,而是因为后面的串口、网卡、存储设备全都要通过 dm 框架来获取,如果 dm 没先建好,后面全是空指针。

2.2 initr_dm 内部到底做了哪几件事

initr_dm()本身代码量不大,但它调用的dm_init_and_scan()才是真正干活的地方。我把这条链拆成几个关键动作:

第一件事是初始化 dm 的全局根节点。dm 模型是一棵树,树得有根。这个根就是root设备,它不对应任何真实硬件,纯粹是个逻辑容器。dm_init()会创建这个根设备,并把gd->dm_root指向它。你可以把它理解成整个设备树的"挂载点",后面所有设备都是它的子孙。

第二件事是扫描并绑定(bind)设备。这一步会遍历设备树(DTB)或者平台数据结构,把每一个有compatible属性的节点,找到对应的 driver,然后创建struct udevice并挂到父节点下面。注意,绑定阶段只是"建立关系",还没有真正去操作硬件。很多人误以为绑定就等于驱动跑起来了,其实不是,绑定只是把"设备"和"驱动"配对登记。

第三件事是按顺序探测(probe)设备。探测才是真正调用驱动的probe函数、去读写寄存器、初始化硬件的过程。dm 会按照设备树的层级和u-boot,dm-pre-reloc、u-boot,dm-pre-proper这些标记,决定哪些设备先探测、哪些延后。

这三件事的顺序不能乱:先有根,才能挂设备;先绑定,才能探测。这个逻辑链就是 dm 骨架的"钢筋结构"。

2.3 为什么串口初始化必须排在 dm 之后

这里有个特别典型的坑,我当年就栽过。串口在调试阶段太重要了,很多人想让它越早工作越好,于是把串口初始化往前提。但在 dm 框架下,串口设备本身就是一个 dm 设备,它需要先被绑定、再被探测,才能拿到struct udevice和对应的 ops。

如果你在initr_dm()之前就去调serial_init(),而此时 dm 还没建好,串口驱动里的dev指针是空的,轻则打印不出来,重则直接跑飞。所以正确的顺序永远是:dm 骨架先搭好,串口作为 dm 设备被探测,然后才能正常输出。

我实测过一个反例:某次为了调试早期问题,强行在 dm 之前初始化串口,结果 log 是出来了,但后面 dm 扫描时又初始化了一遍串口,寄存器被重复配置,波特率直接乱掉,打印全是乱码。这个教训告诉我,在 dm 框架下,任何外设都不要绕过框架去"抢跑"。

3. 绑定与探测:dm 骨架的两根主梁

3.1 绑定阶段:设备与驱动的"相亲登记"

绑定(bind)这个词用得很形象,它干的事就是给设备和驱动"牵线"。具体流程是这样的:dm 扫描到一个设备节点,读取它的compatible字符串,然后在一张全局的驱动表里查找哪个 driver 的of_match表里有这个字符串。找到了,就创建一个struct udevice,把dev->driver指向这个 driver,同时把设备挂到父设备的子链表上。

这里有个细节值得说:struct udevice是动态分配的,用的是 dm 自己的内存池。为什么不用普通 malloc?因为 dm 设备数量在编译期其实是可以估算的,用一个预分配的池子能避免内存碎片,也能加快分配速度。这个设计思路在嵌入式环境里很常见,毕竟资源紧张。

绑定的顺序也有讲究。dm 会先绑定父节点,再绑定子节点,因为子节点需要知道自己的父是谁。这个"自顶向下"的顺序保证了树的完整性。如果你在调试时发现某个设备的dev->parent是空的,那多半是绑定顺序出了问题,或者设备树里这个节点的父节点没被正确识别。

还有一个容易忽略的点:绑定不等于驱动存在。如果某个设备的compatible在驱动表里找不到匹配,dm 会怎么处理?默认情况下它会报一个警告,然后跳过这个设备,但不会让整个启动失败。这个行为在移植新板子时特别有用,你可以先让系统跑起来,再逐个补驱动。

3.2 探测阶段:真正让硬件"活过来"

探测(probe)是 dm 骨架里最"重"的一步。绑定只是登记,探测才是入洞房。探测时 dm 会调用 driver 的probe回调,驱动在这个回调里完成寄存器映射、时钟使能、复位释放等一系列硬件操作。

探测的顺序由几个因素决定。首先是设备树的层级,父设备通常先于子设备探测,因为子设备可能依赖父设备提供的资源(比如总线控制器)。其次是u-boot,dm-pre-reloc标记,带这个标记的设备会在重定位之前就被探测,用于那些早期就要用的设备(比如调试串口)。再就是u-boot,dm-pre-proper,这类设备在重定位后、正式扫描前探测。

我踩过的一个坑是关于探测顺序的依赖问题。有一次 MMC 控制器和它的 PHY 是两个独立的 dm 设备,PHY 必须先于控制器探测,否则控制器初始化时访问 PHY 寄存器会失败。解决办法是在设备树里用depends或者调整节点顺序,让 dm 知道谁先谁后。这个机制在较新版本的 dm 里支持得比较好,老版本可能要靠手动控制。

探测失败的处理也值得说。如果某个设备的 probe 返回错误,dm 默认会把这个设备标记为"探测失败",后续再有人想用它,会直接返回错误而不是重试。这个设计避免了反复初始化同一个坏设备。但在调试阶段,这个"缓存失败"的行为有时候会误导你,让你以为改了代码没生效,其实是 dm 记住了上次的失败。遇到这种情况,可以在 probe 里加打印,或者临时清掉失败标记。

3.3 用一张表看清绑定和探测的区别

维度绑定(bind)探测(probe)
核心动作创建 udevice,匹配 driver调用 driver->probe,操作硬件
是否碰硬件否是
失败影响跳过该设备,启动继续设备标记失败,后续不可用
触发时机dm 扫描设备树时按层级和标记顺序触发
典型耗时极短,纯内存操作较长,涉及寄存器和延时

这张表我在调试时经常拿出来对照。当你发现某个设备"存在但用不了",多半是绑定成功但探测失败;当你发现设备"根本找不到",那可能是绑定阶段就没匹配上驱动。

4. 驱动骨架里那些不写文档的潜规则

4.1 uclass 才是 dm 的真正组织者

很多人学 dm 只盯着 driver 和 udevice,忽略了 uclass。其实 uclass 才是 dm 骨架里最核心的组织单位。uclass 是一类设备的抽象,比如所有串口都属于UCLASS_SERIAL,所有网卡都属于UCLASS_ETH。当你调用uclass_get_device(UCLASS_ETH, 0, &dev)时,dm 是在 uclass 的链表里找第 0 个 eth 设备。

uclass 的价值在于统一接口。不同厂商的网卡驱动实现千差万别,但对上层来说,它们都提供eth_ops里定义的那几个操作。这样上层代码就不用关心底层是哪家的芯片。这个设计思想其实就是面向对象里的"多态",只不过用 C 语言实现。

在board_init_r的 dm 初始化过程中,uclass 是在绑定设备时按需创建的。第一个属于某个 uclass 的设备被绑定时,dm 会创建对应的 uclass 实例,后续同类设备直接挂到这个 uclass 下。所以 uclass 的数量是动态的,取决于实际用到了哪些类型的设备。

这里有个实操技巧:如果你想知道系统里到底有哪些 uclass、每个 uclass 下有几个设备,可以在命令行里用dm tree和dm uclass命令(前提是编译时开了CONFIG_CMD_DM)。这两个命令在调试 dm 问题时简直是神器,能直接看到整棵设备树和 uclass 的归属关系。

4.2 设备树里的 status 属性是个"隐形开关"

设备树里每个节点都有个status属性,取值okay或disabled。这个属性看起来不起眼,但它直接决定 dm 会不会去绑定和探测这个设备。disabled的设备在扫描阶段就被跳过了,连 udevice 都不会创建。

这个机制在板级配置里特别有用。比如一块板子有两个网口,但某个产品型号只用其中一个,那就可以在对应的设备树里把不用的那个网口标成disabled,这样 dm 就不会去初始化它,省时省电。

我遇到过一个诡异的问题:某个外设在设备树里明明写了,驱动也编进去了,但就是不出现在dm tree里。查了半天才发现,这个节点的status被上游的 dtsi 文件设成了disabled,而我在板级 dts 里没有覆盖它。这个坑提醒我,看设备树一定要看最终展开的结果,不能只看自己写的那一层。

4.3 probe 的时机比你想的更灵活

很多人以为 probe 只在board_init_r的 dm 初始化阶段发生一次,其实不是。dm 支持懒探测(lazy probe),也就是说,一个设备可以等到第一次被真正使用时才探测。这个机制在启动时间敏感的场景下非常有用,能把不急着用的设备推迟初始化。

懒探测的触发点是device_probe()被调用时。当你通过uclass_get_device拿到设备后,如果这个设备还没探测,dm 会自动触发探测。所以从使用者的角度看,你不需要关心设备是什么时候探测的,只要在用它之前确保 dm 已经初始化就行。

但这个灵活性也带来一个坑:如果某个设备在board_init_r早期被别的驱动依赖,而它又是懒探测的,那依赖它的驱动可能会拿到一个未探测的设备。解决办法是在设备树里给这个设备加u-boot,dm-pre-reloc或者显式在早期调用device_probe()。我在调试一个电源管理芯片时就遇到过这个问题,最后是在它的消费者驱动里手动 probe 才解决。

5. 移植新板子时 dm 骨架的排查链路

5.1 从串口 log 定位 dm 卡在哪一步

移植新板子,最怕的就是卡在 dm 初始化阶段,串口只打印一半就没动静了。这时候第一步是看 log 最后停在哪。如果停在initr_dm之前,那问题在更早的基础设施;如果停在 dm 扫描过程中,那多半是某个设备的绑定或探测出了问题。

我一般的做法是在dm_init_and_scan的关键节点加打印,比如每绑定一个设备打一行,每探测一个设备打一行。这样能精确定位到是哪个设备卡住的。虽然会拖慢启动,但调试阶段这点开销完全值得。

定位到具体设备后,再去看它的驱动 probe 函数。常见的卡死原因有几个:时钟没使能就去读寄存器(总线挂死)、复位没释放就访问(读回全 0 或全 F)、寄存器地址映射错误(访问到非法地址触发异常)。这些都要结合芯片手册逐个排查。

5.2 驱动匹配不上的三种典型原因

设备在设备树里,驱动也编进去了,但就是匹配不上,这种情况我遇到过至少三种原因。

第一种是compatible字符串拼写不一致。设备树里写vendor,device-a,驱动里写vendor,device_a,一个横杠一个下划线,dm 就认不出来。这种错误特别隐蔽,因为肉眼看过去几乎一样。

第二种是驱动没有正确注册到 dm 的驱动表里。u-boot 用链接器段(linker section)来收集所有U_BOOT_DRIVER宏定义的驱动,如果编译配置里把某个文件排除了,或者链接脚本有问题,驱动就不会出现在表里。可以用dm drivers命令查看当前系统里注册了哪些驱动。

第三种是驱动依赖的 uclass 没被创建。比如一个驱动声明自己属于UCLASS_I2C,但系统里没有任何 I2C 控制器被使能,那这个 uclass 可能就不存在,驱动也就无法正常绑定。这种情况通常伴随其他错误信息,需要一起看。

5.3 一个真实的排查案例

有次移植一块新板子,网卡死活起不来,dm tree里能看到 eth 设备,但ping就是不通。按排查链路走:先确认绑定成功(在 tree 里,成功),再确认探测成功(加打印,probe 返回 0,成功),那问题就在探测之后的配置上。

继续查,发现 probe 里读到的 PHY ID 是 0xffffffff,说明 MDIO 总线读不到 PHY。回头查设备树,发现 MDIO 节点的地址和实际硬件差了一位。改过来之后,PHY ID 正常,网卡也就通了。这个案例说明,dm 骨架搭起来只是第一步,骨架上的"血肉"(具体硬件配置)还得靠设备树和驱动配合。

6. 把 dm 骨架用顺手之后的几点体会

6.1 不要绕过框架直接操作硬件

用惯了 dm 之后,最大的体会就是:任何外设操作都应该通过 dm 框架走。我见过不少代码,为了图省事,直接在板级文件里写死寄存器地址去操作硬件,绕过了 dm。这种代码短期能跑,但一旦换板子、换芯片,就得全部重写,而且和 dm 里的设备状态可能冲突。

正确的做法是把硬件操作都封装进驱动的 ops 里,上层通过 uclass 接口调用。这样代码的可移植性和可维护性都会好很多。虽然前期多写一点代码,但后期省下的调试时间远超这点投入。

6.2 设备树的组织要跟着 dm 的层级走

设备树不是随便写的,它的层级结构直接影响 dm 的绑定和探测顺序。父节点代表总线或控制器,子节点代表挂在上面的设备,这个层级要和硬件实际拓扑对应。我见过有人把所有设备都平铺在根节点下,结果 dm 探测顺序完全乱套,依赖关系全断。

合理的做法是:I2C 控制器作为一个节点,它下面挂的 I2C 设备作为子节点;SPI 同理;GPIO 控制器和它管理的引脚也要有清晰的父子关系。这样 dm 在探测时自然就能保证"先控制器后设备"的顺序。

6.3 善用 dm 提供的调试命令

最后分享几个我常用的 dm 调试命令,编译时打开CONFIG_CMD_DM就能用:

  • dm tree:打印整棵设备树,看层级和绑定状态
  • dm uclass:按 uclass 分组列出所有设备
  • dm devres:查看设备资源分配情况
  • dm drivers:列出所有已注册的驱动

这几个命令在排查"设备找不到""驱动匹配不上""探测顺序不对"这类问题时,能帮你快速缩小范围。我现在的习惯是,每移植一块新板子,第一件事就是dm tree看一眼,心里对整个设备模型有个底。

说到底,board_init_r里的 dm 骨架搭建,本质上就是"先建根、再挂枝、后开花"的过程。理解了这条主线,再看那些零散的初始化调用,就不会觉得是一团乱麻了。

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

基于ESP32-S3的端侧AI语音助手架构与开发实战

1. 小智AI生态到底是什么:一套端侧语音助手的完整骨架小智AI(xiaozhi-esp32)这几年在开发者圈子里火得很快,核心原因其实很简单:它把一个原本需要手机、智能音箱或者高性能开发板才能跑起来的AI语音助手,压…

作者头像 李华
网站建设 2026/10/2 11:21:52

开源铝型材模拟驾驶舱DIY:从选型到组装避坑指南

差不多每个接触赛车模拟器的人,走到一定阶段都会遇到同一个坎:无论是整机入的成品驾驶舱,还是各种"性价比神架",用着用着总会在某个瞬间觉得哪里不对——要么方向盘柱在大力制动时明显发软,要么座椅角度怎么…

作者头像 李华
网站建设 2026/10/2 11:21:51

RISC-V AI CPU设计:面向边缘智能体的硬件-软件协同架构

1. 这家“黑马”公司招的到底是什么人?——从招聘标题拆解RISC-VAI Agent双赛道的真实能力图谱看到“自研RISC-V、高性能 CPU、AI Agent黑马企业”这个标题,很多工程师第一反应是:又一家蹭热点的PPT公司?但如果你真去扒过国内几家…

作者头像 李华
网站建设 2026/10/2 11:20:44

openrig:开源模块化硬件测试台架搭建指南

桌面上的设备堆到第三层以后,每次想动一个传感器都得先挪开两块开发板,线材缠在一起拉都拉不动——这种场景几乎每个搞硬件的人都不陌生。我折腾了三四年个人实验室,陆续攒下示波器、工控机、树莓派、CAN总线节点、RS485采集板这些零零碎碎的东西&#x…

作者头像 李华
网站建设 2026/10/2 11:20:20

嵌入式内存管理实战:从内存分布到内存池,根治内存泄露

嵌入式工程师有一半的 Bug 出在内存上,这话不是夸张。早年间带我入行的老师傅就这么说,我还不服气,直到自己做过车载控制器、调试过物联网网关、帮人排查过连续跑一个月才复现的死机问题,才明白他说的还是保守了。内存对嵌入式系统…

作者头像 李华
网站建设 2026/10/2 11:19:13

OpenClaw+Qwen:打造企业级智能代码审查机器人

代码审查这事,很多IT团队都卡在同一个地方:人不够、时间不够、标准不统一。提交一多,靠人来盯迟早漏。OpenClaw这种开源自动化平台出现以后,我把它们家的智能代理模型接进了团队的代码仓库,配合本地部署的Qwen模型&…

作者头像 李华