拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

u-boot设备模型解析:从board_init_r到驱动绑定与probe实战

u-boot设备模型解析:从board_init_r到驱动绑定与probe实战

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里,它干了两件大事:

  1. dm_init():初始化 dm 的根节点gd->dm_root,创建根设备root和根驱动root_driver,并把uclass链表、udevice链表这些全局结构准备好。
  2. 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.o

Kconfig里:

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 treecompatible 不匹配对比设备树和 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 驱动,需要单独处理。这个判断方法在移植老代码时特别有用。

返回列表