
1. 从 board_init_r 说起为什么 dm 驱动骨架值得单独拎出来讲搞 u-boot 移植的人迟早会在board_init_r这个函数里迷路。它不像board_init_f那样主要跟内存布局和早期硬件初始化打交道board_init_r是真正把系统从能跑推进到能用的枢纽——存储、网络、USB、文件系统、命令行全在这个阶段被拉起来。而 u-boot 从 2014 年前后开始全面推行的driver modeldm 驱动模型正是把这一大堆外设初始化从各写各的变成统一骨架的关键设计。我最早接触 dm 是在一块 ARMv7 的板子上当时board_init_r里一堆xxx_init()调用看得人头大改一个外设要动好几处。后来切到 dm 之后设备树里加个节点、驱动里声明个U_BOOT_DRIVER绑定和探测就自动完成了。这个转变的核心就是board_init_r里那几行看起来不起眼、实际上撑起整个驱动体系的 dm 初始化代码。这篇内容适合三类人正在做 u-boot 移植、被 dm 的 probe 顺序搞晕的嵌入式工程师想搞清楚 u-boot 设备模型底层机制、不想只停留在抄配置的驱动开发者以及需要给新板子写 dm 驱动、但不确定骨架该怎么搭的实践者。我会把board_init_r里 dm 骨架的搭建过程拆开从初始化入口、扫描绑定、探测顺序到实际写驱动时容易踩的坑尽量讲透。需要先说明一点u-boot 版本差异很大dm 相关 API 在 2016 到 2023 之间有过多次调整比如dm_init_and_scan的参数、uclass的注册方式、ofnode的引入等。我下面讲的是以较新版本2020 之后为主线的通用骨架遇到版本差异会单独点出来。你手上的代码如果对不上先确认版本别硬套。2. dm 驱动骨架的整体设计与选型逻辑2.1 为什么 u-boot 要引入 driver model在 dm 之前u-boot 的外设初始化基本是函数式的每个子系统自己维护一套全局变量和初始化函数比如drivers/mmc/mmc.c里自己管控制器列表drivers/net/里自己管网卡。问题很明显——设备多了之后初始化顺序靠人肉维护同一份驱动没法在多个板子上复用设备树信息也没法统一消费。dm 的核心思路借鉴了 Linux 的设备模型设备device、驱动driver、总线bus、类uclass四件套。设备描述有什么硬件驱动描述怎么操作这类硬件uclass 描述同一类设备的公共接口总线负责设备和驱动的匹配。这样一套下来设备树里的节点自动变成 device驱动通过compatible字符串匹配匹配成功就 probe。选这个模型的好处我在实际移植里体会最深的有三点。第一设备树驱动一切硬件描述和代码解耦换板子主要改 dts。第二probe 顺序可控通过uclass的post_probe和pre_probe钩子以及U_BOOT_DRIVER里的probe时机能精确控制谁先谁后。第三代码复用率高同一个 IP 核的驱动只要 compatible 对得上多个 SoC 都能用。2.2 board_init_r 在启动链里的位置要理解 dm 骨架得先知道board_init_r处在什么位置。u-boot 的启动分两大阶段board_init_f跑在重定位之前主要干内存布局、串口早期初始化、gd结构体准备这些事然后重定位代码到 RAM 顶部跳到board_init_r。board_init_r里干的事按顺序大致是initcall序列init_sequence_r、dm 初始化、各种子系统初始化mmc、net、usb、环境变量加载、控制台、最后进main_loop等命令。dm 初始化在init_sequence_r里被调用具体是initr_dm这个 initcall。它的位置很关键——必须在大多数外设驱动 probe 之前完成否则后面 mmc、net 这些依赖 dm 的子系统会找不到设备。我见过有人把initr_dm的顺序改乱了结果 mmc 初始化时uclass_get_device返回-ENODEV查了半天才发现是 dm 还没扫。所以记住一句话dm 骨架必须在所有依赖它的子系统之前搭好。2.3 骨架搭建的三个阶段整个 dm 骨架的搭建我习惯分成三个阶段来看准备阶段gd-dm_root建立根设备root driver绑定这是所有设备的祖先。扫描绑定阶段遍历设备树或平台数据为每个节点创建 device尝试匹配 driver匹配上就绑定。探测阶段按需或按序调用 driver 的probe真正初始化硬件。board_init_r里的 dm 初始化主要覆盖前两个阶段第三个阶段是懒加载的——设备在被第一次使用时才 probe这也是 dm 性能优化的关键设计。下面逐个拆。3. 核心细节解析dm 骨架里的关键数据结构和 API3.1 gd 里的 dm 根指针u-boot 的全局数据gdglobal_data里dm 相关的字段主要有dm_root、dm_root_f、uclass_root等。dm_root指向根设备它是设备树的根节点对应的 device。所有其他设备在逻辑上都是它的后代通过parent指针串起来。dm_root_f是重定位前的根设备重定位后dm_root接管。这个区分在早期版本里很重要因为 dm 初始化可能发生在重定位前后两个时机。较新版本里这个区分被弱化了但字段还在。我调试 dm 问题时第一件事往往是打印gd-dm_root和它的ofnode确认根设备绑对了。如果dm_root是 NULL后面所有uclass_get_device都会失败。3.2 uclass 的注册与查找uclass 是 dm 里最容易被低估的部分。它本质上是一类设备的公共接口 设备链表。比如UCLASS_MMC下挂着所有 mmc 控制器UCLASS_ETH下挂着所有网卡。uclass 的注册有两种方式一种是通过UCLASS_DRIVER宏静态声明链接时收集到uclass段另一种是运行时uclass_add。绝大多数情况用第一种。每个 uclass driver 里可以定义post_bind、pre_probe、post_probe、pre_remove等钩子这些钩子是控制 probe 顺序的利器。查找 uclass 用uclass_get查找 uclass 下的设备用uclass_get_device、uclass_get_device_by_name、uclass_get_device_by_ofnode等。我实际用下来uclass_get_device_by_ofnode在设备树场景下最稳因为它直接按节点匹配不依赖设备名。3.3 device 和 driver 的绑定过程绑定bind是 dm 骨架的核心动作。过程大致是扫描到一个设备树节点创建struct udevice然后遍历所有 driver用compatible匹配。匹配上就调用 driver 的bind方法把 device 和 driver 关联起来。这里有个细节很多人忽略bind 不等于 probe。bind 只是建立关联probe 才真正初始化硬件。dm 默认是懒加载设备第一次被device_probe时才 probe。但有些设备需要在启动早期就 probe这时候可以在 driver 里设置DM_FLAG_PRE_RELOC或者在board_init_r里显式调用uclass_get_device触发。U_BOOT_DRIVER宏展开后会生成一个struct driver里面有name、id、of_match、bind、probe、remove等字段。of_match就是 compatible 匹配表写驱动时这个表必须和 dts 里的 compatible 严格对应差一个字符都匹配不上。3.4 ofnode 与设备树的交互ofnode是 u-boot 对设备树节点的轻量封装比早期的fdtdec接口更直观。dm 里大量用 ofnode 来遍历节点、读属性。比如ofnode_first_subnode、ofnode_next_subnode遍历子节点ofnode_read_u32读属性。在board_init_r的 dm 初始化里扫描设备树就是从根 ofnode 开始递归遍历。每个节点尝试 bind。这里有个性能考量如果设备树很大全量扫描会很慢。所以 dm 支持OF_LIVE模式把设备树展开成树形结构遍历更快。开不开OF_LIVE对启动时间有影响我实测在中等规模 dts 上能差几十毫秒。4. 实操过程board_init_r 里 dm 骨架是怎么一步步搭起来的4.1 initr_dm 的调用链先看调用链。board_init_r里执行init_sequence_r数组其中一项是initr_dm。initr_dm的实现大致是static int initr_dm(void) { int ret; ret dm_init_and_scan(false); if (ret) return ret; return 0; }dm_init_and_scan是核心它内部又分几步dm_init建立根设备dm_scan_platdata扫描平台数据设备dm_scan_fdt扫描设备树dm_scan_other扫描其他来源。参数false表示不在重定位前调用重定位前是true。我建议你在initr_dm后面加一行调试打印确认gd-dm_root非空、uclass_root链表非空。这一步能挡掉一半的设备找不到问题。4.2 dm_init根设备的建立dm_init干的事是创建根设备。它调用device_bind_driver把root_driver绑到dm_root。root_driver是一个特殊的 driver定义在drivers/core/root.c里它的of_match匹配根节点。根设备建立后gd-dm_root指向它uclass_root也初始化好。这一步如果失败通常是root_driver没被链接进去检查CONFIG_DM和相关 Kconfig 是否开全。4.3 dm_scan_fdt设备树扫描与绑定这是骨架搭建里最重的一步。dm_scan_fdt从根 ofnode 开始递归遍历所有子节点对每个节点调用dm_scan_fdt_node。后者会检查节点是否有compatible属性、是否status okay、是否u-boot,dm-pre-reloc等然后尝试 bind。bind 的过程是lists_bind_fdt它遍历 driver 链表用of_match匹配。匹配上就device_bind_with_driver_data。这里有个坑如果多个 driver 的 compatible 相同先注册的赢。所以驱动命名和注册顺序要注意别让通用驱动抢了专用驱动的匹配。我遇到过一种情况一个 SoC 的 mmc 控制器通用驱动和专用驱动 compatible 都写了snps,dw-mshc结果通用驱动先注册专用驱动的特性没生效。解决办法是给专用驱动的 compatible 加更具体的字符串或者调整链接顺序。4.4 探测触发从懒加载到显式 probe骨架搭好后设备都处于 bound 状态没 probe。真正触发 probe 的时机有几类子系统初始化时显式调用比如 mmc 子系统mmc_init里会uclass_get_device拿控制器这时触发 probe。uclass 的 post_probe 钩子某些 uclass 会在自己的 probe 里触发子设备 probe。命令行手动触发dm tree、dm uclass命令可以查看状态dm probe可以手动 probe。我一般会在board_init_r末尾加一段调试代码遍历关键 uclass打印设备数量和 probe 状态。这样启动日志里就能看到哪些设备没起来。4.5 一个完整的驱动骨架示例假设要给一个虚构的my-i2c控制器写 dm 驱动骨架大致是这样#include dm.h #include i2c.h struct my_i2c_priv { void __iomem *base; u32 clock; }; static int my_i2c_probe(struct udevice *dev) { struct my_i2c_priv *priv dev_get_priv(dev); struct my_i2c_plat *plat dev_get_plat(dev); priv-base dev_read_addr_ptr(dev); priv-clock dev_read_u32_default(dev, clock-frequency, 100000); /* 硬件初始化 */ writel(priv-clock, priv-base CLK_REG); return 0; } static int my_i2c_of_to_plat(struct udevice *dev) { struct my_i2c_plat *plat dev_get_plat(dev); plat-reg dev_read_addr(dev); return 0; } static const struct udevice_id my_i2c_ids[] { { .compatible myvendor,my-i2c }, { } }; U_BOOT_DRIVER(my_i2c) { .name my_i2c, .id UCLASS_I2C, .of_match my_i2c_ids, .of_to_plat my_i2c_of_to_plat, .probe my_i2c_probe, .priv_auto sizeof(struct my_i2c_priv), .plat_auto sizeof(struct my_i2c_plat), };几个关键点of_to_plat负责从设备树读平台数据probe负责硬件初始化priv_auto和plat_auto自动分配私有数据和平台数据。dev_read_addr_ptr、dev_read_u32_default这些是 dm 提供的便捷接口比直接操作 ofnode 省事。5. 常见问题与排查技巧实录5.1 设备找不到-ENODEV 排查表-ENODEV是 dm 里最常见的错误。我整理了一张排查表现象可能原因排查方法uclass_get_device返回 -ENODEV设备没 binddm tree看设备是否在树里设备在树里但 probe 失败probe 函数返回错误加打印看 probe 哪一步失败compatible 匹配不上dts 和 driver 字符串不一致对比of_match和 dtsstatus 不是 okaydts 里节点被禁用检查status属性依赖的父设备没 probeprobe 顺序问题检查父设备 probe 状态5.2 probe 顺序错乱的处理probe 顺序错乱通常表现为设备 A 的 probe 里访问设备 B但 B 还没 probe。dm 的默认懒加载不保证顺序解决办法有几种在 driver 里用device_get_supply或dev_get_parent显式声明依赖dm 会按依赖顺序 probe。在 uclass 的pre_probe钩子里先 probe 依赖设备。在board_init_r里按顺序显式uclass_get_device。我一般优先用第一种依赖关系写在代码里比人肉维护顺序靠谱。5.3 设备树节点被忽略的几种情况设备树节点写了但没被 bind常见原因status disabled、没有compatible、u-boot,dm-pre-reloc没设但需要早期 probe、节点在chosen或aliases下被特殊处理。我踩过最坑的一次是节点写在__overrides__里u-boot 默认不扫改成正常节点就好了。5.4 内存分配失败与 priv/plat 大小priv_auto和plat_auto设太小probe 里访问越界表现可能是随机崩溃或数据错乱。我建议 priv 结构体里加个 magic 字段probe 时校验能快速定位越界。另外plat_auto在of_to_plat之前分配of_to_plat里写 plat 要确保大小够。5.5 版本差异导致的 API 变化前面提过dm API 跨版本有变化。几个典型dev_get_plat在旧版本叫dev_get_platdataof_to_plat是后来引入的早期直接在probe里读设备树ofnode替代了fdtdec的部分接口。移植旧代码时先查版本对应的头文件别照搬网上博客。6. 我个人在实际操作中的几点体会dm 骨架这东西看代码觉得简单真上手移植才知道坑都在细节里。我最大的体会是别急着写 probe先把 bind 和匹配搞对。很多设备起不来的问题根子在 compatible 没匹配上而不是 probe 逻辑错。启动日志里加一行 bind 成功的打印能省掉大量调试时间。另外dm tree和dm uclass这两个命令一定要用起来。它们能直接告诉你设备树被解析成什么样、哪些设备 bind 了、哪些 probe 了。我现在的习惯是新板子第一次启动先跑一遍dm tree把设备树和实际设备对一遍心里有底再往下调。最后分享一个小技巧如果怀疑某个设备没 probe可以在board_init_r里显式device_probe它看返回值。返回 0 说明 probe 成功返回负值看错误码比在日志里大海捞针快得多。这个内容后续还可以往uclass自定义接口、dm与ACPI的配合方向扩展等有空再写。