1. 先说背景u-boot 重定位后dm 为什么要“推倒重来”搞过 u-boot 移植的人应该都有这种感觉board_init_r那个长长的 initcall 序列里前面是一堆 ddr、i2c、mux、timer 的低层初始化真正让你觉得“这块板子开始有自己的外设体系了”的瞬间其实是dm_init_and_scan()被调用的那一刻。从这往后u-boot 的设备模型driver model也就是 dm才在 RAM 里重新搭起了整套驱动骨架串口、GPIO、MMC、网络、I2C 全都挂在这棵树上。我也是当年被一块板子的串口问题折磨了将近两周才下决心把 dm 这块彻底啃透。后来再帮同事查问题发现绝大多数人卡住的点都差不多知道U_BOOT_DRIVER宏、知道设备树 compatible 要匹配但说不清楚board_init_r里initr_dm这一步到底干了什么以及为什么 relocation 之后整个 dm 体系要“重来一遍”。这篇就把这条主线彻底捋一遍源码级拆解不绕弯子。先声明一下这里的 dm 是 device model / driver model跟某些数据库和“dm 管理工具”没有半点关系别被搜索引擎误导了。我全篇讲的都是在common/board_f.c、common/board_r.c、drivers/core/这几个目录里跑的东西。1.1 从 board_init_f 到 board_init_r一次“搬家”u-boot 的启动流程宏观上分成两个大阶段board_init_f和board_init_r。前者是在只读存储器NOR flash、ROM、XIP 等里跑的那时候外部 SDRAM/DDR 刚刚被初始化出来u-boot 自己的代码体积和运行环境都还很“局促”只能做最基本的 CPU 级、板级初始化同时给后续代码腾出一块可用的 RAM 区域。等board_init_f把 RAM 初始化好、把堆栈和全局数据结构gd都布置妥当接下来就会执行真正的代码搬迁relocate_code。这一步把 u-boot 的代码段、数据段、BSS 段整体从 flash 复制到 RAM 的高地址区域之后才跳进board_init_r在一片完整、高速、可读写的内存里把整个系统真正跑起来。这个“搬家”和我们日常理解的进程迁移很像。搬家之后最大的问题就是原来所有指针、符号地址都还是 flash 里的老地址必须统一加一个偏移量gd-reloc_off去修正。但并不是所有东西都能靠“加偏移”救回来。有些东西是动态分配在临时堆里的搬家之前你可能用得很欢搬家之后那块内存就被新的堆管理区域接管甚至覆盖了。很不巧dm 框架早前绑定的那些设备对象恰恰就属于“不能靠加偏移硬扛”的那一类。这就是为什么 dm 要在board_init_f和board_init_r各初始化一次。board_init_f里有initf_dmboard_init_r里有initr_dm两次调的底层函数都是dm_init_and_scan()。区别在于前一次是为了在老环境里尽快把串口等极少数设备用起来好打印启动日志后一次才是真正建立长期稳定的设备模型骨架。注意很多新手为了省事把board_init_f里的initf_dm整个删掉结果发现重定位后串口根本起不来就是这个原因。早期 dm 框架不只是为了“提前用一下”它还决定了重定位后哪些设备会被提前绑好、以及 gd 里的若干 dm 相关指针有没有机会被修正。1.2 initcall 序列init_sequence_f 与 init_sequence_r如果你打开common/board_f.c和common/board_r.c会看到两张非常醒目的“表”——init_sequence_f和init_sequence_r。它们是函数指针数组每个元素对应一个初始化函数u-boot 启动时按顺序逐个调用。static init_fnc_t init_sequence_f[] { initf_malloc, initf_dm, initf_console_record, ... };static init_fnc_t init_sequence_r[] { initr_trap, ... initr_dm, initr_serial, initr_board_late, ... };initr_dm在init_sequence_r里的位置非常讲究它必须在initr_serial、initr_mmc、initr_net这些外设相关 initcall 之前。原因很简单——后面的函数几乎都要靠uclass_get_device()去拿设备而设备能不能被找到、能不能被 probe前提就是 dm 框架已经初始化完毕、root 设备已经建好、设备树已经扫描过一轮。所以说board_init_r里 dm 驱动骨架搭得好不好直接决定了后续一整套外设初始化能否顺利推进。你要是看到启动日志卡在某个initr_mmc或者initr_net上别一上来就怀疑对应驱动先回头确认initr_dm这一步是不是已经悄悄失败了。2. 先把骨架看清dm 框架的五个对象与两条链表在深入代码之前我强烈建议你把 dm 框架的几个核心概念装在脑子里。u-boot 的 dm 并不是一个特别精巧的设计但它的对象划分非常清晰driver驱动、uclass_driver类别驱动、uclass设备类别、udevice设备实例、以及一层一层的platdata/priv数据。如果用戏班子来类比uclass是一场戏的类型比如“武戏”“文戏”uclass_driver是这场戏的舞台监督负责安排上下场driver是剧本写的是这个角色怎么演udevice是台上正站着的那个角儿他手上有自己的道具priv还知道自己属于哪个剧种uclass而 root 设备就是那个戏台子本身所有东西都得从它上面搭起来。2.1 五个核心结构体结构体作用关键字段我的理解struct driver具体外设驱动由U_BOOT_DRIVER宏注册.name、.id、.of_match、.probe、.remove剧本描述了设备怎么初始化、怎么工作struct uclass_driver某一类设备的公共逻辑.id、.post_probe、.pre_probe、.per_device_auto舞台监督每个设备 probe 前后要做的公共动作struct uclass某类设备的“集合”运行期对象.uc_drv、.dev_head剧种队列挂同类的所有udevicestruct udevice绑定后的设备实例运行期对象.driver、.uclass、.parent、.priv、.platdata角色被 probe 之后才真正“活”起来struct device_private设备框架内部私有数据.driver_data、.flags、.deferred后台化妆间框架自己记账用注意区分uclass_driver和driver前者是某一类设备的公共行为后者是某一个具体外设的驱动。比如所有串口设备都归UCLASS_SERIAL管理但 pl011、8250、ns16550 各自有各自的struct driver。而uclass_driver只有一个挂在 UCLASS_SERIAL 上。2.2 两条链表串起全部设备dm 框架初始化完成后内存里会形成两个核心链表维度uclass 链表所有已经创建的struct uclass通过list_head挂在一个全局链表上。老版本里直接放在gd-uclass_root有的版本则是通过lists模块的全局头节点管理。扫描这个链表可以看到系统当前有哪些“类”。设备链表同一类下面的所有udevice挂在uclass-dev_head上同时每个设备又通过child_head挂在它的parent下面形成一棵和设备树节点对应的“设备树”。这两条维度缺一不可。uclass维度解决的是“我要一个串口去哪找”的问题parent/child维度解决的是“这个设备挂在哪个总线/哪个父设备下面”的问题。root 设备gd-dm_root是整棵设备的根。设备树里根节点下的所有节点最终都会成为 root 的孩子、孙子。所以dm_scan的入口就是从gd-dm_root开始递归扫描。2.3 U_BOOT_DRIVER 宏背后的“登记册”很多人第一次看U_BOOT_DRIVER宏会觉得神奇我没调用任何注册函数驱动怎么就被框架发现了其实这背后是 u-boot 的链接器段机制linker list。U_BOOT_DRIVER(name)展开后本质上是在目标文件里定义一个带有特殊 section 属性的全局变量#define U_BOOT_DRIVER(__name) \ ll_entry_declare(struct driver, __name, driver)ll_entry_declare会把变量放到.u_boot_list_2_driver_2_##name这样的段里。链接脚本在链接阶段会把这些分散在各目标文件里的段收集在一起并定义__u_boot_list_start、__u_boot_list_end之类的边界符号。这样运行时只要按段边界遍历一遍就能拿到当前镜像里所有通过宏注册的驱动。这也是为什么你加了新的U_BOOT_DRIVER之后不需要改任何“注册表”重新编译链接即可。#define ll_entry_start(_type, _list) \ ({ \ _type *start (_type *)__u_boot_list_##_list##_1; \ start; \ })同理UCLASS_DRIVER、U_BOOT_DRVINFOof-platdata 用这些宏也都是往各自专属段里塞条目。理解了这一层再看lists_bind_fdt里的“遍历驱动找匹配”就顺理成章了。3. 核心函数拆解dm_init_and_scan 到底做了什么board_init_r里的initr_dm核心就一句话调用dm_init_and_scan(true)。绝大部分人死磕的也就是这个函数的内部逻辑。我把这段代码拆成两半来看前半段是dm_init()负责“造根”后半段是dm_scan()负责“扫描设备树并绑定驱动”。int dm_init_and_scan(bool pre_reloc_only) { int ret; ret dm_init(); if (ret) return ret; if (CONFIG_IS_ENABLED(OF_CONTROL)) { ret dm_scan(pre_reloc_only); if (ret) return ret; } return 0; }3.1 dm_init造根绑定 root_driverdm_init()的逻辑非常简单核心只有两件事绑定 root 设备初始化 uclass 链表头。int dm_init(void) { int ret; if (gd-dm_root) return -EINVAL; ret device_bind_by_name(NULL, false, root_info, DM_ROOT_NON_CONST); if (ret) return ret; INIT_LIST_HEAD((struct list_head *)gd-uclass_root); return 0; }先看gd-dm_root是否已经被设置过。如果已经存在直接返回错误。这就解释了为什么dm_init_and_scan不能被重复调用——第二次进来dm_init()就失败了。所以别指望“再调一次全量扫描把新设备补上”。再看device_bind_by_name。这里绑定的不是设备树里的某个节点而是用了一个静态的root_info去创建 root 设备。root 驱动叫root_driver它的uclass_id是UCLASS_ROOT。绑定成功后gd-dm_root指向这个新创建的 rootudevice。INIT_LIST_HEAD((struct list_head *)gd-uclass_root)则是把 uclass 链表头初始化成空链表。注意gd-uclass_root本身不是struct list_head而是用两个unsigned long存的链表头这里强转一下做初始化是个很有意思的细节。dm_init做完之后的状态是有一棵只有根节点的设备树有一个空的 uclass 链表。整个骨架的“地基”有了接下来才是填充内容。提示如果你自己写代码想在运行期手动创建设备通常也应该走device_bind_common或device_bind_driver这类函数而不是直接 malloc 一个struct udevice硬塞。device_bind_by_name只是早期绑定 root 用的特殊通道。3.2 dm_scan从根开始递归扫描设备树dm_init之后gd-dm_root已经有了接下来dm_scan要做的就是遍历设备树把所有“能匹配到驱动”的节点一个个变成udevice。int dm_scan(bool pre_reloc_only) { int ret; ret dm_scan_fdt_node(gd-dm_root, gd-fdt_blob, 0, pre_reloc_only); if (ret) return ret; return 0; }dm_scan_fdt_node的输入参数很有意思parent 是gd-dm_rootfdt_blob 是设备树二进制源offset 是 0也就是从设备树根节点开始。int dm_scan_fdt_node(struct udevice *parent, const void *blob, int offset, bool pre_reloc_only) { ... for (; !ret (depth 0); depth fdt_next_node(blob, offset, next)) { ... ret lists_bind_fdt(parent, blob, offset, child, pre_reloc_only); ... if (depth 0) { /* 递归进入子树 */ ... } } }这里有个容易忽略的点lists_bind_fdt每次绑定的是当前节点到parent设备下绑定成功后会得到一个子设备。如果这个节点还有子节点下一轮循环就会用这个新子设备作为父设备继续扫。这样设备树的层级结构就在 dm 的parent/child链表里被完整复刻出来了。pre_reloc_only参数控制的是“过滤规则”如果为真那么只有那些在设备树里显式标记了u-boot,dm-pre-reloc以及一些版本里的bootph-pre-ram的节点才会被绑定其余节点跳过留到后续按需绑定。如果为假则全量绑定。不同版本之间这个过滤逻辑细节有差异但意图是一致的早期阶段能省则省只处理必需设备。3.3 lists_bind_fdtcompatible 匹配与 device_bind_commonlists_bind_fdt是真正干活的地方。它的核心逻辑是拿设备树节点上的compatible字符串逐个去匹配驱动段里所有struct driver的.of_match[]表。int lists_bind_fdt(struct udevice *parent, const void *blob, int offset, struct udevice **devp, bool pre_reloc_only) { ... drv ll_entry_start(struct driver, driver); ... for (entry drv; entry ! drvend; entry) { ... ret device_bind_common(parent, entry, ...); if (ret 0) { /* 绑定成功 */ } } }一个设备树节点可以写多个 compatible 字符串u-boot 会按顺序去驱动段里找匹配项。找到第一个能绑定的驱动就算成功。匹配规则简单粗暴of_match表里的.compatible字符串和设备树节点的compatible属性完全一致即可。device_bind_common是整个绑定过程的中枢。它负责分配struct udevice和struct device_private内存在 uclass 链表中查找或创建对应的struct uclass把新设备挂到父设备的child_head和所属 uclass 的dev_head根据.of_match[]表里的.data字段初始化dev-driver_data保存设备树节点信息dev-node_/of_offset分配平台数据platdata_auto_alloc_size指定的内存注意device_bind_common完之后并不代表设备已经能用了。它只是完成了“登记”动作。真正的初始化动作在device_probe()里。3.4 初始化后的内存拓扑如果你用dm tree命令观察一个正常启动的 u-boot会看到类似这样的层级输出root ├── serialf0000000 ├── gpiof0010000 │ ├── i2cf0020000 │ └── mmcf0030000 ├── ethf0040000 └── ...这棵树的本质就是udevice之间的 parent/child 关系而每个设备又同时挂在某个 uclass 的dev_head链表中。root 设备是唯一的顶层入口gd-dm_root指向它。知道了这个拓扑后面调试时思路就会特别清晰先看设备有没有出现在dm tree里没有就是绑定/扫描问题出现了但 probe 失败那就是驱动回调本身的问题。4. probe 激活骨架只是“绑定”驱动运行靠 probe绑定了不等于能用。很多人在自己写驱动的时候只写了.probe却没注意device_probe这个框架级函数到底做了多少事。我把它拆开来细说。4.1 device_probe 完整流程device_probe的大致流程如下int device_probe(struct udevice *dev) { ... /* 1. 递归 probe 父设备 */ if (dev-parent !(dev-parent-flags DM_FLAG_ACTIVATED)) device_probe(dev-parent); /* 2. 分配私有数据 */ if (dev-driver-priv_auto_alloc_size) dev-priv alloc_priv(dev, ...); /* 3. 分配平台数据 */ if (dev-driver-platdata_auto_alloc_size) dev-platdata alloc_plat(dev, ...); /* 4. uclass 预处理钩子 */ if (dev-uclass-uc_drv-pre_probe) dev-uclass-uc_drv-pre_probe(dev); /* 5. 调用驱动自身的 probe */ ret dev-driver-probe(dev); /* 6. uclass 后处理钩子 */ if (dev-uclass-uc_drv-post_probe) dev-uclass-uc_drv-post_probe(dev); /* 7. 标记设备已激活 */ dev-flags | DM_FLAG_ACTIVATED; ... }第 1 步非常关键如果一个设备的 parent 还没 probeu-boot 会先递归 probe 父设备。这就是为什么很多 I2C 从设备的驱动代码里根本不用关心 I2C 控制器是否初始化——框架在 probe 你之前会先保证你的“上层”已经活了。这也是 dm 框架最有价值的地方之一依赖顺序不用你手工维护。第 2、3 步是自动内存分配。priv_auto_alloc_size和platdata_auto_alloc_size这两个字段决定了驱动私有数据和平台数据的大小框架会在 probe 时统一 malloc。我见过不少人在probe回调里自己 malloc 私有结构体这就破坏了框架的“生命周期管理”语义remove 的时候很容易漏释放。正确做法是声明priv_auto_alloc_size然后在 probe 里直接用dev-priv。第 4、6 步是 uclass 一级的钩子。比如UCLASS_I2C的pre_probe可能会做一些总线地址检查UCLASS_NET的post_probe可能会注册 MAC 地址。这些钩子让整个类别的设备能在统一位置做统一处理也是 uclass 存在的意义。4.2 谁在 board_init_r 里触发了第一次 probe回到board_init_r。initr_dm执行完后设备树设备已经被绑定成udevice但绝大多数设备只是“登记”状态没被 probe。真正让系统开始“唤醒”设备的是后面的initr_serialstatic int initr_serial(void) { if (CONFIG_IS_ENABLED(DM_SERIAL)) { struct udevice *dev; /* 触发串口设备 probe */ uclass_get_device(UCLASS_SERIAL, 0, dev); ... } ... }uclass_get_device会先尝试找这个 uclass 下标 0 的设备如果找到了就调用device_probe。串口设备一旦 probe 成功printf输出就有地方去了。你会在启动日志里看到重定位前的输出和重定位后的输出都正常靠的就是这一下。后面initr_mmc、initr_net、initr_i2c等 initcall 也是类似套路通过uclass_get_device(UCLASS_MMC, ...)之类的调用触发对应类别的设备 probe。所以整个board_init_r的过程可以理解成“搭好骨架 → 按需唤醒”的顺序推进。4.3 自动 probe 标志DM_FLAG_PROBE_ON_SCAN 等有些设备需要“一绑定就 probe”比如 root 设备、某些必须在扫描阶段就初始化好时钟/电源的节点。driver结构体里有个.flags字段其中DM_FLAG_PROBE_ON_SCAN就是这个作用。绑定阶段如果看到设备树属性或驱动标志要求立即激活dm_scan_fdt_node会顺手调一下device_probe。这种“立即 probe”和“延迟 probe”的辩证关系是整个 dm 设计里最精髓的地方扫描阶段只登记、不初始化启动速度快而等真正用到这个设备时再由uclass_get_device触发初始化内存和电都花在刀刃上。5. 实际移植给新外设挂进 dm 骨架理解了上面的机制给一块新板子加外设驱动就变成了“机械操作”。我以挂一个假的 GPIO 控制器为例说明完整的三步走。5.1 第一步注册 UCLASS_DRIVER如果这个外设属于一个全新的类别需要在include/dm/uclass-id.h里加一个UCLASS_FOO枚举值然后定义一个对应的uclass_driverUCLASS_DRIVER(foo_uclass) { .id UCLASS_FOO, .name foo, .post_probe foo_post_probe, };如果外设属于已有类别UCLASS_GPIO、UCLASS_SERIAL、UCLASS_MMC这步直接跳过因为框架已经帮你定义好了。5.2 第二步注册 U_BOOT_DRIVER接着写具体驱动并声明of_match表#include dm.h #include dm/ofnode.h static const struct udevice_id foo_ids[] { { .compatible vendor,foo-gpio, .data 0x1234 }, { } }; static int foo_probe(struct udevice *dev) { /* 此时 dev-priv 已分配好 */ printf(foo probe, driver_data%lu\n, dev-driver_data); return 0; } U_BOOT_DRIVER(foo_gpio_drv) { .name foo_gpio, .id UCLASS_FOO, .of_match foo_ids, .probe foo_probe, .priv_auto_alloc_size sizeof(struct foo_priv), };注意driver_data的值会在绑定时从 of_match 表中写入dev-driver_data所以 probe 里可以用它区分同一驱动对应不同硬件版本的情况。5.3 第三步设备树节点在 dts 文件里添加对应节点foo: foo40000000 { compatible vendor,foo-gpio; reg 0x40000000 0x1000; status okay; };重新编译把新的u-boot.dtb烧进去。启动后在 u-boot 命令行执行 dm tree如果一切正常新设备应该出现在 root 下。再执行 dm uclass dump可以看到所有 uclass 及其设备列表。注意dm tree里能看到的设备不一定 probe 成功了。要确认 probe 结果得看设备那个ofnode后缀是否带了活动标记或者直接在 probe 函数里打印日志。很多时候设备在dm tree里“虚挂”着因为还没被任何消费者触发。6. 实操中踩过的坑排查思路与技巧说句实话dm 框架本身并不难难的是当它出问题时你手上有没有一套清晰的排查路径。我把自己和同事踩过的坑整理成一个速查表再附一个真实调试案例。6.1 常见错误速查表报错或异常现象根本原因排查方向No UCLASS_XXX对应的 uclass 没被编译进去或uclass-id.h里没有该枚举检查CONFIG_DM_XXX是否打开确认 uclass ID 宏路径Unable to bind ... (-19)compatible 没匹配到任何of_match[]对比 dts 的 compatible 字符串与驱动of_match是否一字不差设备在dm tree里出现但 probe 没执行没有消费者触发uclass_get_device或没有加DM_FLAG_PROBE_ON_SCAN找到使用该设备的代码路径确认是否有 get 操作probe 回调返回错误导致启动卡住probe 内部资源获取失败、寄存器访问异常在 probe 开头加打印逐步定位返回错误的具体位置reloc 后串口突然不打印initr_dm失败或串口驱动没有DM_SERIAL相关配置打开DEBUG_UART确认 dm 初始化在重定位后重新执行成功设备重复绑定dm_scan被执行了两次可能来自OF_PLATDATA和 FDT 同时启用检查CONFIG_IS_ENABLED(OF_PLATDATA)避免两套机制同时生效6.2 一个实例为什么重定位后串口突然哑了之前调试一块 FPGA 板卡flash 里启动日志一切正常一旦进入board_init_r串口输出就断。日志停在 relocation 之前后段全无。我当时怀疑是串口驱动问题反复看驱动代码没找到毛病。后来把initr_dm的返回值和每一步调用都用debug()打出来才发现dm_init_and_scan挂了。再深入一看设备树里串口节点的 compatible 写错了板级 dtsi 覆盖时少写了一个字符。扫描阶段匹配不上驱动串口设备根本没绑定initr_serial里uclass_get_device(UCLASS_SERIAL, 0, dev)永远返回-ENODEV后续打印全部白费。这个案例的通用价值在于重定位后串口失效大方向应该先检查 dm 是否重建成功、串口设备是否重新绑定成功而不是一上来就怀疑硬件配置。毕竟 reloc 前同一套串口硬件还是好的。6.3 保留调试手段事前埋点和事后 dump我给自己的板子长期保留了三样调试手段强烈建议你也养成这个习惯第一在drivers/core/root.c的dm_init_and_scan、dm_scan_fdt_node等关键函数里加debug()或log_debug()打印。开启CONFIG_LOG后能看清扫描到哪个节点、绑定哪个驱动、返回什么错误码。这是定位绑定失败的“第一现场”。第二板子能进命令行时用dm tree、dm drivers、dm uclass dump三条命令交叉验证。dm drivers能列出当前镜像里所有注册过的驱动可以快速确认你的U_BOOT_DRIVER到底有没有编进来。第三在initr_dm前后分别检查gd-dm_root和gd-uclass_root的状态。一个正常的裸指针应该非空链表头应该处于“已初始化”状态。如果gd-dm_root在 relocate 之后还是老地址说明 gd 指针修正出了问题这时候要回头查relocate_code和 gd 布局而不是继续盯驱动代码。还有一个小技巧CONFIG_DEBUG_UART是救命神器。它独立于 dm 存在能在 dm 完全没起来之前输出字符。碰到 dm 相关问题先让 DEBUG_UART 把关键地址和状态打出来往往几分钟就能定位到问题区域。我个人这几年的体会是u-boot 的 dm 骨架真正难的不是某个函数怎么写而是理解“先绑定、后激活”这两阶段分离的设计理念。board_init_r里的initr_dm只是把骨架搭起来真正的血肉是后面无数个uclass_get_device和device_probe一步步填进去的。把这个主线条理顺了以后移植新板子、加新驱动、查启动异常你都会有一种“屠龙刀在手”的底气。