1. 从 board_init_r 切入,搞懂 u-boot 设备模型到底在搭什么
玩过 u-boot 移植的人都有一个共同的体感:板子能跑到board_init_r,基本就说明串口、时钟、DDR 这些最底层的东西已经活了,剩下的就是“把驱动一个个接上电”。但很多人卡就卡在这一步——明明board_init_f阶段串口已经能打印了,为什么到了board_init_r还要重新折腾一遍驱动?dm_init_and_scan到底扫了什么?gd->dm_root这棵设备树是怎么从无到有长出来的?
这篇东西就是把这层窗户纸捅破。我默认你手里已经有一份能编译、能跑起来的 u-boot 源码,也大概知道board_init_r是 C 运行环境的第二阶段入口,但对driver model(后面统一叫 dm)的内部骨架还是一团浆糊。读完你应该能做到三件事:第一,清楚board_init_r里 dm 初始化的完整调用链;第二,明白uclass、udevice、driver这三者是怎么绑到一起的;第三,自己动手往这棵树上挂一个新驱动时,知道该在哪一步、改哪个文件、填哪些结构体。
先把结论摆前面:u-boot 的 dm 不是“运行时动态发现设备”的那种模型,它更像是一套编译期登记、运行期绑定的静态骨架。设备节点在U_BOOT_DEVICE或者设备树里声明,驱动用U_BOOT_DRIVER注册,board_init_r阶段做的事情,就是把这堆散落的声明按uclass归类、按of_match匹配、按probe顺序激活。理解了这个“登记-匹配-激活”三段式,整个 dm 就没有神秘感了。
我见过太多人一上来就去啃drivers/core/device.c几千行代码,结果越看越晕。正确的姿势是先抓住board_init_r这个时间锚点,看它在什么时机、以什么顺序触发 dm 的初始化,再顺着调用栈往下钻。这样你看到的每一行代码都有明确的上下文,而不是孤立地读函数。
2. board_init_r 里 dm 初始化的完整调用链拆解
2.1 先定位 board_init_r 在启动流程中的位置
u-boot 的启动分两大阶段,board_init_f和board_init_r。前者跑在重定位之前,用的是临时栈和只读数据段,主要任务是初始化 DDR、串口、定时器这些“能让 C 代码跑起来”的基础设施,然后把 u-boot 自身重定位到 RAM 高端,最后跳转到board_init_r。
board_init_r的签名是void board_init_r(gd_t *new_gd, ulong dest_addr),它拿到的是重定位后的全局数据指针。这个阶段内存已经可用,堆(malloc)也能用了,所以 dm 这种需要动态分配udevice结构的框架才有条件展开。你在common/board_r.c里能找到它的定义,里面是一长串initr_xxx函数的调用序列,dm 相关的就在其中。
关键的一点:board_init_r里的初始化是有序的,顺序错了驱动就会 probe 失败。比如initr_dm必须排在需要 dm 的驱动之前,而initr_dm自己又依赖gd和堆已经就绪。这个顺序不是随便排的,是无数人踩坑之后定下来的。
2.2 initr_dm 到 dm_init_and_scan 的调用链
在common/board_r.c里,dm 的入口是initr_dm。它的核心就一行:
ret = dm_init_and_scan(false);这个false参数是pre_reloc_only,表示“不只扫描重定位前的设备,全部都要扫”。在board_init_f阶段其实也调过一次dm_init_and_scan(true),那次只初始化了极少数必须在重定位前就工作的设备(比如串口),真正的全量扫描留到了board_init_r。
dm_init_and_scan在drivers/core/root.c里,它干了两件大事:
dm_init():初始化 dm 的根节点gd->dm_root,创建根设备root和根驱动root_driver,并把uclass链表、udevice链表这些全局结构准备好。dm_scan():扫描所有已登记的设备和驱动,按uclass归类,触发匹配和 probe。
dm_init里最核心的是调用device_bind_by_name把根设备绑到根驱动上。根设备是个特殊存在,它的uclass是UCLASS_ROOT,是所有设备的祖先。你可以把它理解成设备树的“树根”,后面所有设备都是它的子孙。
2.3 dm_scan 到底扫了哪些来源
dm_scan不是只扫一个地方,它按优先级扫了多个来源,顺序如下:
dm_scan_platdata():扫描用U_BOOT_DEVICE宏静态声明的平台数据设备。这是最老式的方式,现在新板子基本不用了,但很多老代码还在。dm_scan_fdt():扫描设备树(Device Tree)。这是现代 u-boot 的主流方式,设备节点从.dts编译成.dtb,运行时解析。dm_scan_other():板级自定义扫描,弱符号函数,板子可以覆盖它来手动绑定一些特殊设备。
这三个来源扫完之后,所有udevice都已经创建并绑定了对应的driver,但还没有 probe。probe 是延迟的,等到真正有人调用uclass_get_device或者device_probe时才触发。这个“延迟 probe”设计很关键,它避免了启动时一次性把所有驱动都初始化,节省时间也避免依赖顺序问题。
2.4 为什么 dm 初始化要放在这个时间点
有人会问,为什么不在board_init_f里一次性把 dm 全初始化完?答案是内存和依赖。board_init_f阶段堆还没准备好,udevice结构需要动态分配,没堆就没法搞。而且很多驱动依赖 DDR 初始化完成、时钟树配置好,这些在board_init_f早期还没做。
放在board_init_r里,内存、堆、基础时钟都就绪了,驱动 probe 的成功率最高。但也不能太晚,因为后面initr_xxx里很多设备(比如 MMC、网络、USB)都要用 dm 接口去拿设备句柄,所以initr_dm必须排在它们前面。这个“不早不晚”的位置,就是board_init_r调用序列里精心安排的结果。
3. uclass、udevice、driver 三者的绑定关系与核心数据结构
3.1 三个结构体各自的职责
要理解 dm 骨架,必须先把这三个结构体的分工搞清楚。我用一个生活化的类比:driver是“工种说明书”,udevice是“具体某个工人”,uclass是“工种分类”。
struct driver:描述一类驱动怎么干活。里面有name、id、of_match(设备树匹配表)、probe、remove、ops(操作函数集)等。它是编译期就确定的,用U_BOOT_DRIVER宏注册到链接段里。struct udevice:描述一个具体的设备实例。里面有driver指针、uclass指针、parent指针、platdata(平台数据)、priv(私有数据)等。它是运行期动态分配的。struct uclass:描述一个设备类别,比如UCLASS_GPIO、UCLASS_MMC。它管理同一类设备,提供uclass_ops给上层调用。每个 uclass 有一个uclass_driver来描述它的行为。
三者的关系是:一个driver可以绑定多个udevice(比如两个相同的 GPIO 控制器),每个udevice属于一个uclass,uclass通过uclass_driver提供统一接口。上层代码通常只跟uclass打交道,不直接碰driver。
3.2 U_BOOT_DRIVER 宏展开后是什么样
U_BOOT_DRIVER这个宏是理解 dm 注册机制的钥匙。它展开后大致是这样:
#define U_BOOT_DRIVER(__name) \ ll_entry_declare(struct driver, __name, driver)ll_entry_declare会把驱动结构体放到一个特殊的链接段.u_boot_list_2_driver_2_xxx里。链接脚本u-boot.lds里有一个.u_boot_list段,专门收集这些登记项。运行时,dm 框架通过遍历这个段就能拿到所有已注册的驱动,不需要任何动态注册调用。
这就是“编译期登记”的含义。你写一个驱动,只要用了U_BOOT_DRIVER宏,它就会自动出现在驱动列表里,dm_scan时就能被找到。这个设计非常巧妙,避免了手动维护驱动列表的麻烦。
3.3 设备树节点如何匹配到 driver
设备树里的每个设备节点,通过compatible属性和驱动的of_match表匹配。of_match是一个struct udevice_id数组,每个元素有compatible字符串和data。匹配过程在driver_check_compatible里,就是字符串比较。
匹配成功后,device_bind_common会创建udevice,把driver指针填进去,然后调用uclass_bind_device把设备挂到对应 uclass 的链表上。如果这个 uclass 还没创建,会先创建 uclass 实例。
这里有个细节:uclass的创建是懒加载的。第一个属于某 uclass 的设备被绑定时,才创建这个 uclass。这样避免了启动时创建一堆用不到的 uclass。
3.4 probe 的触发时机与顺序
绑定完成不等于驱动工作。probe才是真正初始化硬件的地方。probe 的触发有两种:
- 主动 probe:上层调用
uclass_get_device、device_probe等接口时触发。 - 自动 probe:如果设备有
DM_FLAG_PRE_RELOC或者 uclass 有DM_UC_FLAG_SEQ_ALIAS等标志,会在扫描后自动 probe。
probe 顺序遵循“父设备先于子设备”的原则。因为子设备的 probe 往往依赖父设备已经初始化好(比如 I2C 从设备依赖 I2C 控制器)。dm 框架通过device_probe里的递归逻辑保证这个顺序:probe 一个设备前,先 probe 它的 parent。
注意:如果你自己写的驱动 probe 里访问了父设备的资源,但父设备还没 probe,就会拿到空指针。这是新手最常见的崩溃原因之一。
4. 动手搭一个 dm 驱动骨架的完整实操
4.1 确定驱动类型和 uclass
假设我们要加一个虚拟的“LED 控制器”驱动,它控制板子上几个 GPIO 灯。第一步是确定它属于哪个 uclass。如果只是简单 GPIO 操作,可以直接用UCLASS_GPIO;但如果我们想提供led_on、led_off这种语义化接口,就应该定义一个新的 uclass,比如UCLASS_LED_CTRL。
定义新 uclass 需要写一个uclass_driver:
UCLASS_DRIVER(led_ctrl) = { .id = UCLASS_LED_CTRL, .name = "led_ctrl", .post_bind = led_ctrl_post_bind, .per_device_auto = sizeof(struct led_ctrl_priv), };per_device_auto指定每个设备自动分配的私有数据大小,这样你不用手动 malloc。post_bind在设备绑定后调用,适合做一些初始化。
4.2 编写 driver 结构体和 ops
driver 结构体是核心:
static const struct led_ctrl_ops led_ctrl_ops = { .on = led_ctrl_on, .off = led_ctrl_off, }; U_BOOT_DRIVER(led_ctrl_gpio) = { .name = "led_ctrl_gpio", .id = UCLASS_LED_CTRL, .of_match = led_ctrl_ids, .ops = &led_ctrl_ops, .probe = led_ctrl_probe, .bind = led_ctrl_bind, .priv_auto = sizeof(struct led_ctrl_priv), };of_match表:
static const struct udevice_id led_ctrl_ids[] = { { .compatible = "myvendor,led-ctrl" }, { } };probe函数里做硬件初始化,比如申请 GPIO、配置方向:
static int led_ctrl_probe(struct udevice *dev) { struct led_ctrl_priv *priv = dev_get_priv(dev); int ret; ret = gpio_request_by_name(dev, "led-gpios", 0, &priv->gpio, GPIOD_IS_OUT); if (ret) return ret; return 0; }注意gpio_request_by_name这个调用,它内部会去设备树里找led-gpios属性,并触发对应 GPIO 控制器的 probe。这就是 dm 的依赖链自动解析。
4.3 设备树节点的写法
设备树里加节点:
led_ctrl: led-ctrl@0 { compatible = "myvendor,led-ctrl"; led-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; status = "okay"; };compatible必须和of_match里的字符串完全一致,大小写敏感。led-gpios属性会被gpio_request_by_name解析,&gpio0指向 GPIO 控制器节点。
4.4 编译配置的开关
别忘了在Kconfig里加配置项,并在Makefile里根据配置编译:
obj-$(CONFIG_LED_CTRL) += led_ctrl_gpio.oKconfig里:
config LED_CTRL bool "Enable LED controller driver" depends on DM_GPIO help Say Y here to enable the LED controller driver.depends on DM_GPIO很重要,因为驱动里用了 GPIO 接口,没有这个依赖编译会报错。
4.5 验证驱动是否被正确加载
编译烧录后,在 u-boot 命令行里用dm tree命令查看设备树。你应该能看到led_ctrl这个 uclass 和下面的设备节点。用dm uclass看 uclass 列表,用dm dev看设备详情。
如果设备没出现,先检查compatible是否匹配、Kconfig 是否开启、设备树节点status是否为okay。这三个是最常见的“设备不出现”原因。
5. 常见问题与排查技巧实录
5.1 设备绑定失败:compatible 不匹配
最常见的现象是dm tree里看不到你的设备。九成是compatible字符串对不上。设备树里的字符串和of_match里的必须逐字符一致,包括厂商前缀和连字符。我见过有人把myvendor,led-ctrl写成myvendor,led_ctrl,下划线和连字符混了,排查半天。
排查方法:在driver_check_compatible里加debug打印,或者用fdtgrep工具确认设备树里节点的compatible值。
5.2 probe 顺序导致的空指针
驱动 probe 里访问父设备资源,但父设备还没 probe,拿到空指针崩溃。典型场景是 I2C 从设备 probe 时访问 I2C 总线,但总线控制器还没初始化。
dm 框架本身保证父设备先 probe,但前提是你的设备在设备树里正确嵌套。如果从设备节点没有放在 I2C 控制器节点下面,而是放在根节点下,父子关系就断了,probe 顺序就乱了。
排查方法:用dm tree看设备的层级关系,确认嵌套正确。
5.3 私有数据分配失败
priv_auto和platdata_auto没设置,或者设置的大小不对,导致dev_get_priv返回的指针指向错误位置。这个错误很隐蔽,因为不一定立刻崩溃,可能只是数据错乱。
排查方法:确认priv_auto大小和struct xxx_priv的sizeof一致。用dev_get_priv拿到的指针,打印一下地址和内容,看是否符合预期。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 设备不出现在 dm tree | compatible 不匹配 | 对比设备树和 of_match 字符串 |
| probe 时崩溃 | 父设备未 probe | 检查设备树嵌套层级 |
| priv 数据错乱 | priv_auto 大小不对 | 核对 sizeof 和配置值 |
| 驱动没编译进去 | Kconfig 未开启 | 检查 .config 和 Makefile |
| uclass 找不到 | uclass_driver 未注册 | 确认 UCLASS_DRIVER 宏存在 |
| probe 返回 -ENODEV | 依赖的资源缺失 | 检查 gpio/clk/reset 属性 |
5.5 几个独家避坑技巧
第一,写新驱动时先用dm tree确认设备节点出现了,再写 probe 逻辑。很多人一上来就写一堆 probe 代码,结果设备根本没绑定,白忙活。
第二,of_match表最后一定要有一个空元素{ }作为结束标志,否则遍历会越界。这个空元素不是可选的,是必须的。
第三,probe 函数里尽量用dev_read_xxx系列接口读设备树属性,不要直接操作dev->platdata。前者会处理各种边界情况,后者容易踩坑。
第四,调试 dm 问题时打开CONFIG_DM_DEBUG和CONFIG_DEBUG_UART,能看到详细的绑定和 probe 日志。日志量大但值得。
第五,如果驱动 probe 依赖时钟或复位,用clk_get_by_index和reset_get_by_index,它们会自动触发对应控制器的 probe,比手动找设备靠谱。
6. 从骨架到实战:把 dm 用顺手的几个进阶思路
6.1 用 uclass 接口隔离上层和驱动
dm 最大的价值不是“能自动匹配驱动”,而是接口隔离。上层代码调用led_ctrl_on(dev),不关心底层是 GPIO 控制的还是 I2C 扩展芯片控制的。这种隔离让驱动替换变得容易,也让代码可测试性提升。
写驱动时,ops 里的函数应该只做“这件事怎么做”,不做“这件事什么时候做”。时机由上层决定,驱动只管执行。这个边界划清楚了,驱动就干净。
6.2 利用 post_bind 和 post_probe 做延迟初始化
post_bind在设备绑定后、probe 前调用,适合做一些不依赖硬件的准备工作,比如解析设备树属性存到 priv 里。post_probe在 probe 后调用,适合做一些依赖硬件状态的收尾工作。
这两个钩子用好了,能把 probe 函数拆得更清晰。我习惯把设备树解析放post_bind,硬件初始化放probe,状态检查放post_probe。
6.3 多设备实例的 seq 管理
同一个驱动绑定多个设备时,需要一个序号来区分。dm 提供dev->seq和uclass_get_device_by_seq接口。seq可以从设备树的reg属性或alias节点获取。
比如两个相同型号的 GPIO 控制器,用seq区分后,上层可以精确指定操作哪一个。alias节点里写gpio0 = &gpio@1000;,dm 会自动分配 seq。
6.4 驱动卸载与资源释放
u-boot 里驱动卸载用得少,但在一些热插拔场景(比如 USB)会用到。remove函数里要释放 probe 时申请的资源,比如gpio_free、clk_free。不释放会导致资源泄漏,下次 probe 时申请失败。
remove的调用顺序和 probe 相反,子设备先于父设备。dm 框架自动处理这个顺序,你只要保证remove里释放干净就行。
6.5 把 dm 调试信息用起来
dm tree、dm uclass、dm dev、dm drivers这几个命令是调试 dm 的利器。dm tree看层级,dm uclass看分类,dm dev看详情,dm drivers看所有已注册驱动。
我习惯在板子 bring-up 阶段,每次改完驱动就dm tree看一眼,确认设备出现、层级正确、probe 状态是active。这个习惯帮我省了大量调试时间。
7. 我个人在实际操作中的体会
u-boot 的 dm 框架刚接触时确实有点绕,但它的设计逻辑其实很朴素:编译期登记、运行期匹配、按需 probe。抓住这三句话,再看board_init_r里的调用链,就不会迷路。
我踩过最大的坑是早期不理解“绑定”和“probe”的区别,以为设备出现在dm tree里就代表驱动工作了。实际上绑定只是建立了udevice和driver的关联,probe 才是真正初始化硬件。很多“设备在但功能不正常”的问题,都是 probe 没触发或者 probe 失败被忽略了。
另一个体会是,设备树的嵌套关系比想象中重要。父子关系决定了 probe 顺序,probe 顺序决定了依赖能否满足。写设备树时多花五分钟确认嵌套,能省后面几小时的调试。
最后分享一个小技巧:如果你不确定某个驱动是否支持 dm,看它的U_BOOT_DRIVER宏里有没有.ops和.probe。有这两个基本就是 dm 驱动,没有的话可能是老式的非 dm 驱动,需要单独处理。这个判断方法在移植老代码时特别有用。