U-Boot Device Model骨架搭建深度解析

发布时间:2026/10/31 1:04:28
U-Boot Device Model骨架搭建深度解析

1. 为什么说“屠龙刀在手”不是夸张——board_init_r 里那几行 dm 初始化代码,真能决定整台设备的生死

你有没有遇到过这样的情况:u-boot 编译顺利、烧写成功、串口有 log、U-Boot>提示符也出来了,但一执行bootm就卡死,或者fdt addr显示的设备树节点压根没被解析进驱动框架?更诡异的是,printenv能读,saveenv却失败;SPI Flash 探测到了,但sf probe报错“no device”;甚至 USB 主机控制器明明在设备树里写了 compatible,usb start却提示“no controllers found”。这些现象背后,90% 的根源不在驱动本身,而是在board_init_r函数里——那个短短几十行、常被跳过阅读的dm_init_and_scan()及其后续调用链。

标题里说的“屠龙刀”,指的就是 u-boot device model(DM)这套机制。它不是锦上添花的可选模块,而是现代 u-boot(v2015.04 之后强制启用)的底层骨架。没有它,u-boot 就像一栋没打地基的楼:表面光鲜,一碰就塌。board_init_r是 u-boot 第二阶段初始化的核心入口,从这里开始,系统才真正从“裸机状态”转向“可管理的设备世界”。而 DM 骨架的搭建,正是这个转折点上最关键的一击——它不负责具体功能,却决定了所有功能能否被调度、能否被发现、能否被正确绑定。

我第一次踩坑是在移植一款基于 Rockchip RK3399 的定制板卡时。当时设备树里已经完整描述了 eMMC、USB PHY、PMIC 和 I2C 总线,驱动源码也都存在,但mmc info命令始终返回 “no mmc devices”。花了三天时间逐行加printf,最后发现dm_init_and_scan()返回了-ENODEV,而错误源头竟然是ofnode_get_by_path()在解析/soc/i2c@ff150000节点时失败。根本原因?设备树里漏写了#address-cells和#size-cells这两个看似不起眼的属性。一个属性缺失,整个 I2C 子系统无法注册,进而导致依赖它的 PMIC 驱动无法 probe,最终 eMMC 控制器因缺少供电管理而无法初始化。你看,问题表现在 mmc,根子却扎在board_init_r里 DM 骨架搭设的第一步。

这把“屠龙刀”的锋利之处,在于它用一套统一的抽象层,把硬件差异、驱动实现、设备树描述、资源分配全部收束到一个可控的流程里。它不关心你是 ARM 还是 RISC-V,不关心你的 Flash 是 SPI 还是 NAND,只关心三件事:设备树节点是否合法、驱动是否注册、匹配规则是否满足。一旦骨架立住,后续所有设备的 probe、bind、init 都自动进入流水线;骨架歪了,再好的驱动也是废铁。所以,“速通”不是教你跳过原理,而是帮你精准定位骨架搭建过程中的每一个关节——从dm_init_and_scan()的入口,到device_bind_driver()的落点,再到driver_probe_device()的成败,每一步都值得拆开看透。

2. board_init_r:DM 骨架搭建的“黄金十五行”——逐行解剖 dm_init_and_scan() 的真实意图

board_init_r函数位于common/board_f.c(或部分平台在arch/xxx/lib/board.c),它是 u-boot 从第一阶段(SPL 或汇编初始化)移交控制权后的第一个 C 语言主干函数。整个函数逻辑清晰,但真正决定 DM 生死的,集中在如下这段代码(以 u-boot v2023.04 为基准):

// common/board_f.c: board_init_r() int board_init_r(gd_t *gd, ulong dest_addr) { // ... 前置初始化:console、timer、env 等 ... /* Step 1: 初始化 DM 核心数据结构 */ if (IS_ENABLED(CONFIG_DM)) { ret = dm_init(); if (ret) { printf("ERROR: dm_init() failed: %d\n", ret); hang(); } } /* Step 2: 扫描设备树,创建设备对象并尝试绑定驱动 */ if (IS_ENABLED(CONFIG_DM)) { ret = dm_init_and_scan(true); if (ret) { printf("ERROR: dm_init_and_scan() failed: %d\n", ret); hang(); } } /* Step 3: 启动设备驱动 probe 流程 */ if (IS_ENABLED(CONFIG_DM)) { ret = device_probe_all(); if (ret) { printf("ERROR: device_probe_all() failed: %d\n", ret); hang(); } } // ... 后续初始化:net、cmd、boot 等 ... }

这三步,就是 DM 骨架搭建的“黄金十五行”。它们不是并列关系,而是严格的递进依赖链:dm_init()是筑基,dm_init_and_scan()是立柱,device_probe_all()是封顶。任何一步失败,后续全部停摆。下面逐行拆解其真实意图与隐藏陷阱。

2.1 dm_init():分配全局句柄,建立内存池——不是初始化驱动,而是初始化“管理员”

dm_init()看似简单,实则承担着最基础也最关键的职责:为整个 DM 框架分配和初始化核心数据结构。它的源码在drivers/core/root.c,核心动作只有三件:

  1. 分配gd->dm_root:这是一个struct udevice *类型的全局指针,指向 DM 设备树的根节点。它不是设备树本身的根(/),而是 u-boot 内部维护的、用于管理所有设备的逻辑根。malloc(sizeof(struct udevice))分配内存后,会调用device_bind_root()将其绑定到一个特殊的root_driver上。这个root_driver没有实际硬件操作,纯粹是管理容器。

  2. 初始化gd->uclass_root:这是一个struct list_head,用于挂载所有 uclass(设备类)的链表头。Uclass 是 DM 的核心抽象,比如UCLASS_I2C、UCLASS_MMC、UCLASS_ETH。每个 uclass 对应一个设备类型家族,内部维护该类型所有设备实例的列表和操作函数集。dm_init()会预先注册所有已编译进 u-boot 的 uclass(通过uclass_init()),确保后续扫描时能按需查找。

  3. 预分配设备 ID 池:dm_init()会初始化一个全局的struct device_id_pool,用于为新创建的设备分配唯一 ID。这个池子大小由CONFIG_DM_MAX_DEVICES决定(默认 1024)。如果设备总数超过此值,device_bind_driver()会直接返回-ENOSPC,且不会报错,只会静默失败——这是个极其隐蔽的坑,尤其在复杂板卡上容易触发。

提示:dm_init()的返回值ret绝大多数情况下是 0。它失败的常见原因只有两个:内存分配失败(malloc返回 NULL),或CONFIG_DM_MAX_DEVICES设置过小导致 ID 池耗尽。前者极少发生,后者则需要你检查include/configs/your_board.h中的配置项。

2.2 dm_init_and_scan(true):设备树解析与设备对象创建——“扫描”二字背后是深度遍历与严格校验

dm_init_and_scan()是骨架搭建的心脏环节。它的参数true表示“进行完整扫描”,即从设备树根节点/开始,递归遍历所有兼容节点,并为每个有效节点创建一个struct udevice实例。这个过程远非简单的“读取节点、创建对象”那么简单,它包含四重校验与决策:

  1. 节点有效性校验:首先检查节点是否具有compatible属性。没有compatible的节点(如/chosen、/aliases)会被直接跳过。接着,检查status属性,值为"disabled"的节点也会被忽略。这是最基础的过滤。

  2. 地址空间校验:对于需要内存/IO 地址的设备(如 CPU、总线控制器),dm_init_and_scan()会调用ofnode_get_reg()获取reg属性,并验证其格式是否符合#address-cells和#size-cells的定义。这就是我前面提到的 RK3399 坑的根源——#address-cells缺失,ofnode_get_reg()解析失败,整个节点被判定为无效,后续创建设备对象的流程直接终止。

  3. 驱动匹配校验:这是最关键的一步。dm_init_and_scan()会遍历所有已注册的驱动(通过driver_find()),将节点的compatible字符串与驱动的of_match表逐一比对。匹配成功后,还需检查该驱动是否支持当前 uclass(例如,一个compatible = "snps,dw-mshc"的节点,必须匹配到UCLASS_MMC下的驱动)。匹配失败,节点会被标记为DM_FLAG_PRE_RELOC并暂存,等待后续 probe。

  4. 设备对象创建与绑定:匹配成功后,调用device_bind_driver()创建struct udevice实例,并将其挂入对应 uclass 的设备链表中。此时设备处于UCMD_PROBE状态,尚未执行 probe 函数。device_bind_driver()还会处理父设备关系:如果节点有parent属性(如&i2c0),则自动将其 parent 指针指向父设备;否则,父设备默认为gd->dm_root。

注意:dm_init_and_scan()的返回值ret通常反映的是最后一个被处理节点的状态,而非整体成功与否。因此,即使大部分节点成功,只要最后一个节点因compatible不匹配而失败,ret也会是-ENODEV。判断整体是否成功,不能只看ret,更要观察log中是否有大量No driver for node的警告。

2.3 device_probe_all():驱动 probe 的批量触发——不是“启动”,而是“排队+调度”

device_probe_all()的作用常被误解为“启动所有驱动”。实际上,它只是一个批量调度器。它遍历所有已绑定(device_bind_driver()成功)且状态为UCMD_PROBE的设备,依次调用device_probe()。而device_probe()的核心逻辑是:

  1. 状态检查:确认设备状态确实是UCMD_PROBE。
  2. 资源准备:调用device_ofdata_to_platdata(),将设备树节点的reg、interrupts、clocks等属性解析为驱动所需的platdata结构体。这一步失败(如ofnode_read_u32()读取clock-frequency失败),probe 直接返回错误。
  3. 驱动调用:最终调用驱动结构体中的.probe函数指针,将struct udevice *dev传入。这才是驱动真正开始干活的地方。

因此,device_probe_all()的成败,完全取决于前两步——dm_init()和dm_init_and_scan()是否成功构建了正确的设备对象链表。它本身不解决任何驱动逻辑问题,只负责把“待办事项”推给驱动去执行。

3. DM 骨架的“七寸”在哪里?——从设备树、驱动注册到 uclass 绑定的全链路断点排查

骨架搭建失败,症状千奇百怪,但根源往往集中在几个关键“七寸”位置。这些位置就像人体的命门,一处受阻,全身瘫痪。下面结合真实排错案例,带你走一遍完整的断点排查链路。

3.1 第一寸:设备树节点的“合法性”——ofnode_get_by_path()失败的三种典型场景

dm_init_and_scan()的起点是ofnode_get_by_path("/"),获取设备树根节点。如果这一步失败,整个流程直接 abort。但更常见的是,在遍历子节点时,某个ofnode_get_by_path()调用失败,导致该节点及其所有子节点被跳过。以下是三种高频场景:

场景一:#address-cells/#size-cells缺失或错误

  • 现象:I2C、SPI、PCI等总线下的子设备全部消失,i2c bus命令无输出。
  • 日志线索:ofnode_get_reg: node 'i2c@ff150000' has no #address-cells or #size-cells
  • 根因:设备树中,总线节点(如i2c@ff150000)必须显式声明#address-cells = <1>; #size-cells = <0>;(对于无 size 的地址)。子节点(如eeprom@50)的reg属性才能被正确解析。
  • 修复:在总线节点下补全这两个属性。注意#address-cells和#size-cells的值必须与reg属性的 cell 数量严格匹配。

场景二:status = "disabled"的误用

  • 现象:某个设备(如ethernet@ff3c0000)在fdt print中可见,但在dm tree中找不到。
  • 日志线索:Skipping node 'ethernet@ff3c0000' due to status 'disabled'
  • 根因:status属性值必须是字符串"okay"或"disabled"。如果写成status = <0>(数值 0),u-boot 会将其解释为NULL,从而跳过该节点。
  • 修复:统一使用字符串形式:status = "okay";或status = "disabled";。

场景三:compatible字符串拼写错误或版本不匹配

  • 现象:dm tree中能看到设备节点,但state显示PROBE_DEFER或PROBED,dm drivers列表里却没有对应驱动。
  • 日志线索:No driver for node 'ethernet@ff3c0000', compatible 'rockchip,rk3399-gmac'
  • 根因:驱动的of_match表中没有完全匹配的字符串。常见错误包括:多了一个空格("rockchip,rk3399-gmac ")、大小写错误("Rockchip,rk3399-gmac")、版本号不一致(设备树用"rockchip,rk3399-gmac-v2",驱动只支持"-v1")。
  • 修复:使用grep -r "rockchip,rk3399-gmac" drivers/net/确认驱动支持的 exact string,并修正设备树。

3.2 第二寸:驱动注册的“时机”与“完整性”——U_BOOT_DRIVER()宏的隐藏约束

驱动要被 DM 发现,必须通过U_BOOT_DRIVER()宏注册。这个宏看似简单,实则暗藏玄机:

U_BOOT_DRIVER(rockchip_gmac_drv) = { .name = "rockchip_gmac", .id = UCLASS_ETH, .of_match = rockchip_gmac_ids, .probe = rockchip_gmac_probe, .ops = &rockchip_gmac_ops, };

约束一:.id必须指向一个已注册的 uclass

  • 如果UCLASS_ETH在dm_init()时未被注册(例如,CONFIG_DM_ETH未启用),driver_find()就无法找到该驱动,匹配必然失败。
  • 验证方法:在dm tree输出中,查看uclass列是否包含eth。若无,则检查CONFIG_DM_ETH=y是否在 defconfig 中启用。

约束二:.of_match表必须以{ }结尾

  • of_match表是一个struct of_device_id数组,必须以{ }(全零结构体)作为结束标志。如果遗漏,driver_find()会越界读取内存,导致匹配逻辑混乱,甚至 crash。
  • 验证方法:检查驱动源码,确认rockchip_gmac_ids数组最后一行是{};。

约束三:驱动必须被链接进最终镜像

  • 即使驱动源码存在,如果其Makefile没有将其加入编译(如obj-$(CONFIG_ROCKCHIP_GMAC) += rockchip_gmac.o),U_BOOT_DRIVER()宏定义的符号就不会出现在u-boot.map中,driver_find()自然找不到它。
  • 验证方法:grep "rockchip_gmac_drv" u-boot.map,确认该符号存在且地址非零。

3.3 第三寸:uclass 的“桥梁”作用——UCLASS_ROOT与UCLASS_I2C的层级关系

Uclass 是 DM 的灵魂,它定义了设备的“家族身份”和“行为规范”。理解 uclass 的层级,是读懂dm tree输出的关键。

  • UCLASS_ROOT是顶层 uclass,gd->dm_root就属于它。所有设备,无论类型,最终都挂在UCLASS_ROOT下。
  • UCLASS_I2C是一个中间 uclass。它本身也是一个设备(i2c@ff150000),同时又是其他设备(如eeprom@50、rtc@68)的 parent。
  • UCLASS_MMC、UCLASS_ETH等是叶子 uclass,它们的设备通常是最终的功能设备。

dm tree的输出直观体现了这种关系:

[ 0] root | root_driver | root | | 0 | [ 1] i2c@ff150000 | rockchip_i2c | i2c | | 0 | | [ 2] eeprom@50 | at24 | i2c | | 0 | | [ 3] rtc@68 | m41t00 | i2c | | 0 | [ 4] ethernet@ff3c0000 | rockchip_gmac | eth | | 0

这里[ 1]是UCLASS_I2C,[ 2]和[ 3]是它的子设备,同属UCLASS_I2C;[ 4]是UCLASS_ETH,与[ 1]平级,都是UCLASS_ROOT的直接子节点。

提示:dm tree中的uclass列显示的是设备所属的 uclass 名称(如i2c,eth),而driver列显示的是具体驱动名(如rockchip_i2c,rockchip_gmac)。两者不可混淆。一个 uclass 可以有多个驱动(如UCLASS_I2C下有rockchip_i2c、designware_i2c),一个驱动只能属于一个 uclass。

4. “速通”的实战工具箱——从dm tree到dm drivers,五种命令直击骨架状态

掌握了理论,必须配上趁手的工具。u-boot 提供了一套强大的 DM 调试命令,它们不是摆设,而是你诊断骨架状态的“X 光机”。下面介绍五种最常用、最有效的命令及其解读方法。

4.1dm tree:设备树的“实时快照”——看骨架是否立住,看层级是否正确

dm tree是诊断的起点。它输出当前 DM 框架中所有已创建的设备对象及其父子关系。一个健康的输出应该具备以下特征:

  • 根节点存在:第一行[ 0] root必须存在,且state为PROBED。
  • 关键总线可见:i2c、spi、pci等总线节点必须出现,且state为PROBED。
  • 功能设备挂载正确:如eeprom@50应该挂在i2c@ff150000下,而不是直接挂在root下。
  • 状态标识清晰:state列显示设备当前状态:PROBED(已 probe 成功)、PROBE_DEFER(probe 被推迟,通常因依赖未就绪)、BIND(仅绑定,未 probe)、FAIL(probe 失败)。

实战解读:如果dm tree输出为空,说明dm_init_and_scan()根本没执行,或执行过程中因严重错误(如malloc失败)而提前退出。如果只有root节点,说明设备树解析失败,应重点检查#address-cells和compatible。

4.2dm drivers:驱动注册的“花名册”——看驱动是否上线,看匹配是否可能

dm drivers列出所有已注册的驱动及其支持的compatible字符串。这是验证驱动是否被正确编译进镜像、是否具备匹配资格的直接证据。

输出格式为:

Driver Uclass Compatible rockchip_i2c i2c rockchip,rk3399-i2c designware_i2c i2c snps,designware-i2c rockchip_gmac eth rockchip,rk3399-gmac

实战解读:如果你在dm tree中看到ethernet@ff3c0000,但在dm drivers中找不到rockchip,rk3399-gmac,那问题 100% 出在驱动编译或注册环节。反之,如果dm drivers里有该驱动,dm tree里却没有对应设备,问题一定出在设备树节点的合法性上。

4.3dm uclass:uclass 的“组织架构图”——看家族是否健全,看设备是否归位

dm uclass显示所有 uclass 的统计信息,包括该 uclass 下有多少设备、最大设备数、当前设备数等。

输出示例:

Uclass Devices Max Cur root 1 1024 1 i2c 1 1024 1 eth 0 1024 0 mmc 0 1024 0

实战解读:Cur列显示当前该 uclass 下实际存在的设备数量。如果eth的Cur为 0,但设备树里有ethernet节点,说明该节点未能成功绑定到UCLASS_ETH。此时应结合dm tree和dm drivers,确认是节点问题还是驱动问题。

4.4dm devres:资源分配的“审计报告”——看内存、中断、时钟是否到位

dm devres显示指定设备(可通过dm tree中的序号或路径指定)所申请的所有资源,包括reg(地址空间)、interrupts(中断号)、clocks(时钟源)等。

命令:dm devres 4(查看序号为 4 的设备,即ethernet@ff3c0000)

输出示例:

Device: ethernet@ff3c0000 reg: 0xff3c0000 0x10000 interrupts: 0x0000002a clocks: 0x00000001

实战解读:如果dm devres 4输出为空,说明device_ofdata_to_platdata()在device_probe()阶段失败,设备树属性未被正确解析。此时应检查设备树中ethernet@ff3c0000节点的reg、interrupts、clocks属性是否存在且格式正确。

4.5dm test:驱动 probe 的“压力测试”——手动触发,绕过 defer 机制

dm test命令允许你手动对指定设备执行 probe 操作,这对于诊断PROBE_DEFER状态非常有用。

命令:dm test 4(对序号为 4 的设备执行 probe)

实战解读:如果设备状态是PROBE_DEFER,运行dm test 4后,如果 probe 成功,说明之前的 defer 是因为依赖设备(如时钟、电源)尚未就绪;如果 probe 依然失败,则说明是驱动自身逻辑问题或资源解析失败,需要深入probe函数调试。

注意:dm test是一个危险命令,它会强行执行 probe,可能破坏系统状态。仅在调试环境使用,切勿在生产环境中运行。

5. 从“屠龙刀”到“倚天剑”——DM 骨架之上,如何构建可扩展的设备驱动体系

搭好骨架,只是万里长征第一步。真正的价值在于,如何在这个稳固的骨架上,构建出灵活、可复用、易维护的设备驱动体系。这需要超越board_init_r的视野,理解 DM 框架的设计哲学。

5.1 驱动分层:platform driver 与 uclass driver 的协同

DM 驱动并非铁板一块,而是分为两层:

  • Uclass Driver(uclass 驱动):定义设备的通用行为接口。例如,UCLASS_I2C的 uclass driver 定义了i2c_xfer()、i2c_set_bus_num()等函数,所有 I2C 设备都必须实现这些接口。它位于drivers/i2c/i2c-uclass.c。
  • Platform Driver(平台驱动):实现具体硬件的操作。例如,rockchip_i2c驱动实现了i2c_xfer()的具体逻辑,即如何向 RK3399 的 I2C 寄存器写入数据。它位于drivers/i2c/rockchip_i2c.c。

这种分层带来了巨大好处:当你更换 SoC,只需编写新的 platform driver(如mediatek_i2c.c),而上层的 uclass driver 和所有 I2C 设备(如at24、m41t00)的代码完全无需改动。dm tree中的driver列显示的是 platform driver,而uclass列显示的是 uclass driver。

5.2 设备树覆盖:动态修改设备树,避免硬编码

在board_init_r之后,你还可以通过fdt命令动态修改设备树,这为现场调试和配置提供了极大便利。例如,临时禁用某个设备:

=> fdt addr $fdtcontroladdr => fdt setprop /soc/i2c@ff150000 status "disabled" => fdt resize => reset

这条命令序列将i2c@ff150000节点的status改为"disabled",下次启动时,dm_init_and_scan()就会跳过它。这比重新编译 u-boot 快得多,是现场快速验证的利器。

5.3 自定义 uclass:为私有硬件打造专属管理框架

对于高度定制化的硬件(如专用的 FPGA 配置接口),你可以创建自己的 uclass。步骤如下:

  1. 在include/dm/uclass-id.h中添加UCLASS_FPGA_CONFIG枚举值。
  2. 在drivers/core/uclass.c的uclass_init_list[]中添加对应的uclass_id和初始化函数。
  3. 编写 uclass driver,定义其操作函数集(如fpga_config_load())。
  4. 编写 platform driver,id字段指向UCLASS_FPGA_CONFIG。

这样,你的 FPGA 配置设备就能像标准 I2C 设备一样,被dm tree管理,被dm test调试,完美融入 DM 体系。

我在一个军工项目中就实践过这套方案。客户要求 u-boot 启动时自动加载 FPGA bitstream,并校验 CRC。我们创建了UCLASS_FPGA_CONFIG,将加载、校验、重载等逻辑封装在 uclass driver 中。最终,用户只需在设备树中添加一个fpga@0节点,指定compatible = "ourcompany,fpga-loader",一切就自动完成了。这比在board_init_r里硬编码几十行 FPGA 操作要优雅、安全、可维护得多。

这把“屠龙刀”的终极意义,不在于它能砍断多少 bug,而在于它赋予了你一种结构化思维:把硬件看作可管理的对象,把驱动看作可插拔的模块,把配置看作可编程的数据。当你真正理解了board_init_r里那几行dm_init_and_scan()的重量,你就不再是一个在寄存器手册里苦苦挣扎的工程师,而是一个运筹帷幄、指挥若定的系统架构师。