☰
U-Boot设备模型DM骨架:从board_init_r的initr_dm()看驱动树如何焊成
2026/11/6 3:04:10 网站建设 项目流程

先把结论放这儿:U-Boot 的设备模型(DM,Driver Model)真正成体系、成骨架,是从board_init_r里的initr_dm()开始的。你之前看到的 TPL/SPL 里那些 DM 操作,只是提前热身;到了board_init_r,整个驱动树、uclass 链表、设备绑定才正式像模像样搭起来。这篇文章不讲泛泛的概念,我直接带你沿调用链走一遍,把board_init_r里 DM 驱动骨架是怎么一步步“焊”出来的拆清楚。适合正在做板级移植、U-Boot 驱动开发、或者被dm tree打印搞到怀疑人生的朋友。

1. 先从 board_init_r 往上看,DM 到底被谁唤醒

1.1 两个初始化阶段的明确分工

U-Boot 从上电到运行,C 代码要经历两个大阶段:board_init_f(前初始化)和board_init_r(重定位后初始化)。区分这对兄弟特别关键,因为 DM 的完整骨架基本是在board_init_r里搭的。

board_init_f阶段,DDR 还没完全就绪,代码可能还在 Flash 里跑,全局变量虽然能用但堆空间非常局促。所以这个阶段 U-Boot 只做最基础的事:初始化串口、计算重定位地址、把栈和 GD(global data)准备好。有些板子在这个阶段也会提前用 DM 驱动串口,但是属于“够用就好”的尺度,不会把整棵设备树扫一遍。

到了board_init_r,代码已经重定位到内存里,堆够大,设备树也已经找到,这才是 DM 正式部署的时机。你去看common/board_r.c里的初始化序列,会看到一组函数指针数组,其中一个就是initr_dm。很多搞驱动的人第一次看板级代码会迷路,就是因为没抓住这个入口——那个传说中的“驱动骨架”,就是在这里被拉起来的。

1.2 一条实实在在的调用链

以常见 U-Boot 版本为例,board_init_r会按数组顺序执行初始化函数。核心片段大概是这样的:

static int initr_dm(void) { return dm_init_and_scan(); }

这函数在init_sequence_r[]数组里,排在比较靠前的位置。你顺着它往下挖,会发现真正做事的是dm_init_and_scan(),这个函数定义在drivers/core/root.c。它的动作逻辑很清晰:

int dm_init_and_scan(void) { int ret; ret = dm_init(); if (ret) return ret; ret = dm_scan(); if (ret) return ret; return 0; }

dm_init()负责把 DM 的根设备建好,把所有已注册的uclass登记到全局链表里;dm_scan()则负责拿着设备树(或板级配置)把所有需要用到的节点绑定成udevice。绑定完,骨架就有了。

这里有个关键认知:骨架是“绑定”出来的,不是“探测”出来的。绑定只是在内存里建立udevice对象和链表关系,探测才是真正调用驱动的probe去初始化硬件。board_init_r在initr_dm这个阶段,主要完成的是绑定层面的扫荡,真正的硬件操作是后续各子系统按需触发的。理解不了这层,后面看dm tree输出会觉得一团乱麻。

2. 骨架的钢筋:uclass、udevice、driver 三件套

2.1 三个核心数据结构的关系

DM 之所以叫“模型”,就是因为它把驱动抽象成了几个固定角色。核心有三个:struct driver(驱动程序)、struct uclass_driver(设备类驱动)、struct udevice(设备实例)。它们之间的关系有点像人和户口本:driver是“能力”,uclass_driver是“类别的管理规则”,udevice是“具体登记的个体”。

写驱动时,你一般用宏声明一个driver:

U_BOOT_DRIVER(sandbox_uart) = { .name = "sandbox_uart", .id = UCLASS_SERIAL, .probe = sandbox_uart_probe, .remove = sandbox_uart_remove, };

.id = UCLASS_SERIAL就是告诉 DM:这个驱动属于串口类。struct uclass_driver则定义了串口类的公共方法,比如.post_bind、.pre_probe、.post_probe,以及.per_device_plat_auto这类自动分配内存的大小。这些回调很多人不重视,实际上绑定时会调post_bind,探测前会调pre_probe,顺序错一点就可能出诡异问题。

2.2 链表到底怎么把设备串起来

DM 的骨架本质是几根链表穿起来的树。看struct uclass和struct udevice的关键字段:

struct uclass { struct list_head list_head; /* uclass 挂在全局链表 */ struct list_head dev_head; /* 该类下所有设备链表 */ struct uclass_driver *uc_drv; ... }; struct udevice { struct list_head uclass_node; /* 挂在 uclass->dev_head */ struct list_head child_head; /* 子设备链表头 */ struct list_head sibling_node; /* 兄弟节点链表 */ struct uclass *uclass; struct driver *driver; ... };

全局的入口是drivers/core/uclass.c里的一个静态链表头uclass_root,所有已登记的uclass都挂在这根链上。dm_init()做的事之一,就是让每个U_BOOT_UCLASS宏声明的 uclass 进入这条链表。每个 uclass 自己又有一根dev_head,管着所有属于这个类别的设备。设备之间则通过child_head和sibling_node组成一棵树,根就是gd->dm_root。

你可以把uclass_root想成“户籍总册”,每个 uclass 是一本“分类名册”,udevice就是名册上的一条条记录。board_init_r里dm_scan()做的核心工作,就是往这些名册里登记记录。

3. 绑定时骨架是怎么“焊”上去的

3.1 链接器 Section 的神秘力量

刚接触 DM 的人总会好奇:U_BOOT_DRIVER只是声明了一个结构体,没有调用构造函数,它怎么能被系统找到?

答案藏在链接脚本和宏里。U_BOOT_DRIVER本质上是用ll_entry_declare把一个struct driver放到特定的.u_boot_list_2_driver_2_<name>段。u_boot_list是 U-Boot 特有的一套链表式快捷方式,链接器会把所有驱动和 uclass 按顺序排布,运行时就能通过ll_entry_count和ll_entry_get遍历到。

所以你在代码里“写了一个驱动”,编译链接后它就会自动出现在系统的“能力清单”里。运行时扫描设备树时,每个设备树节点会根据compatible字符串去匹配这份清单里的驱动;匹配成功,就进行绑定。这也是为什么很多老工程师会说:驱动能不能被识别,先看链接段在不在、段序对不对。如果某次裁剪配置把驱动所在的目标文件排除掉,你会看到驱动明明在源码里,dm tree却怎么都找不到它。

3.2 device_bind_common 的关键动作

当dm_scan_fdt_node决定要给某个设备树节点创建udevice时,最终会落到device_bind_common()。它做的事很多,我挑重点讲:

  1. 根据驱动和 uclass 需要的私有数据大小,分配一块零初始化的内存,做udevice。
  2. 填充dev->driver、dev->uclass、dev->parent、dev->name。
  3. 把设备挂到父设备的child_head,并设置sibling_node。
  4. 把设备挂到对应 uclass 的dev_head链表上。
  5. 调用 uclass_driver 的post_bind回调,允许类别做额外初始化。
  6. 如果上述任何一步失败,释放内存并返回错误码。

这一步完成后,设备树里的一个节点就在内存里有了对应的“登记记录”,但它还没有真正碰硬件。整个过程可以概括为:bind 是礼成,probe 才是洞房。这个模型的好处是,U-Boot 可以在启动初期只花很少代价把设备树“物化”成设备链表,而不急着把所有外设点亮。

我在实际调试中,有相当多的问题都出在 bind 的post_bind回调里:某个 uclass 的post_bind会尝试给设备补一个“伴生设备”,如果设备树节点里缺少某个属性,post_bind返回错误,整个节点就绑不上。这种错误在启动日志里往往很不起眼,你只会在后续使用该设备时看到一个-ENODEV。

3.3 probe 与 bind 语义上的巨大差异

很多人搞混bind和probe,以为绑定了就会调probe。实际上,绑定只是建对象、挂链表,探测才会做硬件初始化。驱动开发中常见的流程是:

int device_probe(struct udevice *dev) { // 先探测父设备,保证层级依赖 // 调 uclass_driver->pre_probe // 调 driver->ofdata_to_platdata 解析设备树属性 // 调 driver->probe 做硬件初始化 // 调 uclass_driver->post_probe }

probe是有引用计数的,调一次后,再次调用不会重复执行。这个机制保证了设备可以被多个使用者共享,而不会反复 reset 硬件。我见过不少驱动,probe里做了 GPIO 申请,第二次使用时因为没走probe而以为引脚不存在,最后才发现问题不在驱动本身,而在设备在第一次使用时已经处于“已探测”状态。

4. 设备树扫描:骨架自动生长的过程

4.1 dm_scan 如何对设备树节点批量绑定

dm_scan()最终调用的是dm_scan_fdt_node(),这个函数会从根设备开始递归遍历设备树节点。每遇到一个节点,都会尝试:

  1. 读取节点compatible属性;
  2. 在驱动链表里找匹配的driver;
  3. 如果找到,则device_bind到当前父设备下;
  4. 递归进入该节点子节点,继续处理。

这个递归是深度优先的。所以树里父子设备的绑定顺序是:先绑父,再绑子。这样每个设备在绑定子设备时,父设备的链表已经存在,不至于子设备找不到挂靠点。

对应代码里会看到类似这样的逻辑:

static int dm_scan_fdt_node(struct udevice *parent, const void *blob, int offset, bool pre_reloc_only) { // 遍历节点 for (; depth >= 0; offset = fdt_next_node(blob, offset, &depth)) { // 判断是否需要 pre-reloc if (pre_reloc_only && !dm_fdt_pre_reloc(blob, offset)) continue; // 尝试匹配并绑定 ret = device_bind_common(...); // 递归扫描子节点 ... } }

4.2 pre_reloc 过滤器的作用

你不用在board_init_r里特别关心“所有节点必须全部绑定”这种压力,因为有些节点会被过滤。dm_fdt_pre_reloc函数会检查节点是否带u-boot,dm-pre-reloc属性,或者驱动是否声明了DM_FLAG_PRE_RELOC,又或者是 U-Boot 需要的早期设备类型(比如串口)。

在完整 U-Boot 阶段,dm_scan通常会去掉pre_reloc_only的限制,把普遍设备都扫进来。但在 TPL/SPL 阶段,这个筛选会非常严格,只绑定极少数关键设备,目的就是省内存、省启动时间。如果你发现 SPL 里某个设备明明在设备树中有,却始终没有udevice,先怀疑是不是没加u-boot,dm-pre-reloc属性。

4.3 board_init_r 中 DM 骨架的完整时间线

我习惯把board_init_r启动过程画成一条时间线:

  1. 重定位完成,GD、环境变量准备好;
  2. 执行initr_dm:dm_init()建根设备和 uclass 链表;
  3. 执行dm_scan():递归绑定设备树中的节点,形成设备树状链表;
  4. 后续初始化代码按需探测设备。比如网络子系统会在某个阶段遍历UCLASS_ETH,逐个probe找到的第一个可用网卡;
  5. 控制台、存储、命令等用到具体设备时,设备已经处于“已绑定未探测”状态,调用 API 才真正触发探测。

所以这个骨架的本质,不是把硬件全部初始化,而是把 CPU 眼中的“世界地图”提前画好。地图画好之后,谁要去哪个外设办业务,顺着表找过去再点亮设备即可。

5. 排障实战:如何看待一棵搭不起来或搭错位的驱动树

5.1 现场调试三板斧:dm tree、dm uclass、dm dump

真刀真枪调驱动,光看代码远远不够。U-Boot 命令行提供了几个 DM 调试命令,我几乎天天用:

  • dm tree:打印当前设备树状链表。它会把每个udevice的父子和状态列出来,能一眼看出哪些设备已经绑定、哪些已经探测过、哪些探测失败。
  • dm uclass:打印系统所有已登记的 uclass 和驱动数量。如果你注册的驱动没出现在这里,说明链接配置有问题。
  • dm dump:按驱动类型 dump 出驱动列表,适合快速确认一个驱动是否被链接进镜像。

dm tree输出长这样:

root 0 root_driver ├─ serial 0 serial ├─ gpio0 0 gpio@0 ├─ mmc 0 mmc@700 └─ usb 0 usb@100

看到某个节点有设备但没有探测状态,或者整个树缺了一根分支,就能顺着方向查是匹配失败、扫描被跳过,还是probe返回错误。我调网络驱动时,第一步永远是dm tree | grep eth,先看udecice建立没有,再看它有没有进入探测状态。这一步能省下一半的查错时间。

5.2 常见失败模式与排查速查表

我在多个板子上踩过的坑,总结成下面这个表,基本覆盖最常见的 DM 初始化问题:

现象常见原因排查思路
设备树节点有,但dm tree里没有compatible 没匹配上驱动;pre_reloc 过滤检查fdt nodecompat;查驱动是否链接进镜像
设备已绑定但 probe 失败父设备没探测;pinctrl 设置失败;属性解析出错开CONFIG_DEBUG_DEVICE,在 probe 入口加打印
系统启动时某个外设“没反应”驱动没被任何子系统触发探测;初始化顺序靠后业务代码里用uclass_get_device主动探测
dm uclass里没有某个类别U_BOOT_UCLASS宏对应的目标文件没链接看链接脚本或配置项,确认.u_boot_list_2_uclass
绑定时返回-ENOMEMuclass/post_bind 自动分配的私有数据太大;堆不够调小per_device_auto;检查 ram size 配置
同名驱动存在两个链接器段里同名驱动覆盖检查是否有重复定义或残留下游 patch

每个板子的失败方式都不同,但核心排查思路都是一致的:先看链表上有没有设备,再看设备状态有没有探测,最后再看硬件访问有没有异常。

5.3 调试日志与辅助手段

如果只靠dm tree不够,我会开启CONFIG_DM_DEBUG。这个配置能让 DM 的 bind/probe 过程打印大量调试信息,尤其是设备树匹配的细节。运行启动时串口输出会多出很多:

scan bus for devices... probe: gpio@0 bind: serial@5 Device 'serial@5' probed

有了这些日志,整个绑定扫描过程就像被点开了“上帝视角”。不过要注意,这个选项会显著增加编译体积和启动时间,只建议调试阶段开,合板量产前记得关掉。

经常有人问:为什么驱动里明明加了printf,启动时却一个字符都看不见?答案十有八九是串口驱动自身还没probe,或者串口设备在 DM 链表里根本没绑上。这个时候不要死磕驱动代码,先跑一个dm tree看串口节点状态,往往就真相大白了。

6. 我实际移植板卡时的几条硬经验

6.1 那次 GPIO 灯全不亮的教训

有一回,我在一块 ARM 板卡上调 GPIO。设备树没问题,GPIO 驱动看起来也没问题,但dm tree里 gpio 节点始终是“已绑定未探测”的状态。找了一上午,最后发现是 pin controller 设备没在链表里。GPIO 驱动probe里需要找pinctrl设备来设置引脚复用,而pinctrl节点因为 compatible 写成了foo,pinctrl,驱动里的名称匹配不上,导致 pin controller 没被绑定。GPIO 探测依赖它,自然跟着翻车。

这个经验让我养成了习惯:任何驱动要访问别的设备,先确认对方是不是已经在dm tree里“显灵”。特别是设备树里层层嵌套的父子依赖,光是面向层级的递归绑定就可能让某个中间节点被跳过。

6.2 使用“主动探测”而非赌初始化顺序

另一个经验是关于初始化顺序的。U-Boot 的probe是按需触发的,但不同子系统触发时机不同。我自己写板级初始化代码时,不喜欢赌“网络子系统一定在跑我的逻辑前把设备 probe 掉”。更稳的写法是显式调用uclass_get_device或uclass_first_device_err来确保目标设备已被探测:

struct udevice *dev; int ret; ret = uclass_get_device_by_name(UCLASS_ETH, "eth0", &dev); if (ret) return ret;

这样的好处是:即使系统启动序列调整,我的代码也不会因为设备还没探测而拿到一个空指针。尤其在board_init_r后续添加自己的板级逻辑时,这条原则能避免大量无意义的时序问题。

6.3 骨架搭好之后,如何继续“速通”改造

一旦你理解了board_init_r里 DM 骨架是由“全局 uclass 链表 + 根设备树 + 递归绑定设备树节点”三层构成,后面所有的驱动调试都会变得非常顺手。新增一个驱动的流程自然就清晰了:

  1. 写struct driver,用U_BOOT_DRIVER声明;
  2. 确认.id选对 uclass,必要时补U_BOOT_UCLASS或 uclass_driver;
  3. 确认设备树节点的 compatible 能匹配;
  4. 链接进镜像,用一个临时命令或dm tree验证绑定;
  5. 在probe里做硬件初始化,必要时安排post_bind做辅助设备。

这套流程基本覆盖了我这些年绝大多数驱动移植场景。DM 骨架这东西,第一眼看很玄乎,全是链表和宏;第二眼看就是一套挺朴素的“注册 + 匹配 + 挂链表 + 按需探测”机制。真正踩过一次 GPIO 灯全不亮的坑,再回头看board_init_r里的initr_dm,你会觉得手里这把屠龙刀其实也就是一把很好用的螺丝刀——关键是懂得从哪颗螺丝拧起。

最后多嘴一句:U-Boot 不同版本之间,initr_dm的名字和顺序可能略有调整,但骨架逻辑没变过。看代码时别死背函数名,把“根设备 -> uclass 链表 -> 递归绑定设备树 -> 按需探测”这条主线抓在手里,换成哪个版本你都能两小时摸透一个新板卡的驱动现状。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询