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

资讯详情

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

u-boot设备模型初始化:board_init_r中的dm骨架搭建与驱动probe实战

u-boot设备模型初始化:board_init_r中的dm骨架搭建与驱动probe实战

1. 从 board_init_r 这个"总装车间"说起

很多人第一次翻 u-boot 源码,翻到board_init_r的时候都会有点懵。前面board_init_f阶段还在忙着搬内存、初始化串口、点亮第一行 log,怎么一进board_init_r,画风突然就变了——满屏的dm_前缀函数,dm_init_and_scan、dm_scan_platdata、dm_scan_fdt_devices,一个接一个往外冒。如果你只是想把板子跑起来,照着现成的配置抄一抄也能过;但如果你想搞清楚 u-boot 的设备模型(driver model,后面统一简称 dm)到底是怎么在启动流程里"搭骨架"的,那board_init_r就是那个绕不开的总装车间。

我先把结论摆在前面:u-boot 的 dm 驱动骨架不是在某一个函数里一次性建好的,而是在board_init_r里分阶段、按顺序、层层递进地"扫描—绑定—探测"出来的。board_init_r本身更像一个调度中心,它不负责具体某个驱动的初始化,而是负责在正确的时机,把 dm 的各个扫描入口依次触发,让设备树、平台数据、驱动列表这三路信息汇聚成一张完整的设备-驱动关系网。

这篇文章适合三类人看:第一类是做 BSP 移植、经常要改board_init_r附近代码的嵌入式工程师;第二类是想深入理解 u-boot dm 机制、但被各种uclass、udevice、driver绕晕的驱动开发者;第三类是准备面试、需要把 u-boot 启动流程讲清楚的同学。我会尽量用"总装车间"这个类比贯穿全文,把抽象的 dm 概念落到具体的代码路径上,让你看完之后能自己画出这张骨架图。

在展开之前,先明确一个前提:不同版本的 u-boot,board_init_r的具体实现差异不小。我下面讲的内容以较新的 dm 完整版本(大致 2018 年之后的主流版本)为基准,老版本可能没有dm_init_and_scan这种统一入口,而是散落在各个init_sequence_r里。你对照自己手上的代码时,先确认版本,别硬套。

2. board_init_r 里 dm 骨架的三次关键扫描

2.1 为什么 dm 初始化要放在 board_init_r 而不是 board_init_f

这个问题我当年也纠结过。board_init_f阶段连 DDR 都还没初始化完,代码还在只读的 SRAM 或者临时栈上跑,这时候去搞 dm 扫描,内存分配、设备树解析全都没法正常进行。dm 的核心操作——分配udevice、解析ofnode、绑定driver——都需要可写的堆内存和完整的 malloc 机制,而这些恰恰是board_init_f阶段给不了的。

所以 u-boot 的设计是:board_init_f只做最基础的、必须在重定位前完成的事情(比如串口、时钟、部分 pinmux),把 dm 的完整初始化推迟到board_init_r。到了board_init_r,内存已经重定位完毕,gd->malloc_base可用,设备树也已经从存储介质里读进了内存,这时候再搭 dm 骨架,时机刚刚好。

提示:如果你在board_init_f阶段就想用某个 dm 设备,通常会失败或者拿到一个未 probe 的空壳。正确做法是用DM_FLAG_PRE_RELOC标记那些确实需要提前初始化的驱动,让它们在重定位前被特殊处理。

2.2 dm_init_and_scan:骨架搭建的总入口

在board_init_r里,dm 骨架搭建的核心入口就是dm_init_and_scan。这个函数名字起得很直白——init 加 scan。它内部大致做了这么几件事:

第一,调用dm_init,初始化 dm 的全局根设备gd->dm_root,建立最顶层的uclass根节点。你可以理解为先把总装车间的"总电闸"合上,让整个 dm 框架有一个可以挂载的根。

第二,调用dm_scan_platdata,扫描平台数据(platdata)。这是给那些没有设备树、或者设备树信息不全的板子准备的兜底路径。很多老平台或者简单外设,驱动信息是硬编码在U_BOOT_DEVICE宏里的,这一步就是把这些静态定义的设备注册进 dm。

第三,调用dm_scan_fdt_devices(在支持设备树的配置下),扫描设备树里的设备节点。这是现代 u-boot 的主路径,绝大多数设备信息都来自.dts文件。它会遍历设备树,把每个有compatible属性的节点转换成对应的udevice,并尝试匹配driver。

第四,调用dm_scan_other,给板级代码留一个自定义扫描的钩子。有些板子有特殊的设备来源(比如从 EEPROM 读出来的配置),可以在这里补充。

这四步走完,dm 的"设备清单"和"驱动清单"基本就建立起来了,但注意——这时候大部分设备只是被"绑定"(bind),还没有被"探测"(probe)。绑定只是建立了 udevice 和 driver 的关联,真正的硬件初始化(比如配置寄存器、申请 GPIO)要等到 probe 阶段。

2.3 从 bind 到 probe:骨架上的设备什么时候真正"活"过来

这是很多人容易混淆的地方。dm_init_and_scan跑完之后,你用dm_dump_all去看,会发现一大堆设备状态是"not probed"。这不是 bug,而是 dm 的惰性初始化设计。

probe 的触发有两种典型方式:

一种是按需 probe。当某个驱动调用uclass_get_device或者device_get_by_ofnode去获取一个设备时,dm 会检查这个设备是否已经 probe,如果没有,就现场触发它的probe回调。这种方式的好处是启动快,用不到的设备不初始化。

另一种是主动 probe。在board_init_r的后半段,会有一个init_sequence_r数组,里面有一项initr_dm_devices(或者类似名字,视版本而定),它会遍历所有标记了DM_FLAG_ACTIVE_DMA或者需要在启动阶段就绪的设备,主动 probe 它们。串口、网口、MMC 这些启动关键路径上的设备,通常就是在这里被拉起来的。

我个人的经验是:调试 dm 问题时,先分清是 bind 阶段出错还是 probe 阶段出错。bind 出错通常是compatible字符串对不上、driver 没注册;probe 出错则多半是硬件资源(时钟、GPIO、电源域)没准备好,或者 probe 顺序有依赖。这两类问题的排查思路完全不同,混在一起查会浪费大量时间。

3. udevice、uclass、driver 三者的绑定关系是怎么建立的

3.1 用"车间—工位—工人"类比理解 dm 三件套

dm 里最核心的三个结构体是udevice、uclass、driver。官方文档讲得很抽象,我用总装车间的类比给你翻译一下:

  • driver是"工人",它知道怎么干活(probe、remove、ops),但它本身不知道自己要装在哪条产线上。
  • udevice是"工位",它代表一个具体的设备实例,有地址、有资源、有状态。
  • uclass是"车间",它把干同类活的工位组织在一起,提供统一的调度接口(比如所有 GPIO 工位归 GPIO 车间管)。

绑定关系是这样的:一个udevice在 bind 时,会通过compatible或者driver name找到对应的driver,同时根据 driver 声明的uclass id挂到对应的uclass下面。这样,当你通过 uclass 接口去操作设备时,uclass 就能找到挂在自己名下的 udevice,再通过 udevice 找到 driver 的 ops 去执行。

3.2 bind 阶段:compatible 匹配与 driver 查找的完整链路

bind 的核心逻辑在device_bind_common里。当你从设备树扫描到一个节点时,dm 会做这么几件事:

首先,读取节点的compatible属性。这个属性可能是一个字符串列表,dm 会逐个尝试匹配已注册的 driver。匹配的依据是 driver 的of_match表,里面列出了这个 driver 支持的所有 compatible 字符串。

其次,如果 compatible 匹配成功,dm 会分配一个udevice结构体,把 driver 指针、设备树节点(ofnode)、父设备等信息填进去。如果这个 driver 属于某个 uclass,还会把 udevice 挂到该 uclass 的设备链表上。

然后,如果 driver 定义了bind回调,会在这里调用。bind 回调通常用来做一些轻量的初始化,比如解析设备树里的私有属性、分配私有数据结构。注意 bind 阶段不应该去操作硬件寄存器,因为这时候设备可能还没上电,或者时钟还没开。

最后,如果设备有子节点,dm 会递归地为子节点也执行 bind。这就是为什么设备树里的层级结构能直接映射成 dm 里的父子设备关系。

注意:compatible 匹配是大小写敏感的,而且必须完全一致。我见过有人把"vendor,device"写成"vendor,Device",结果 bind 一直失败,查了半天才发现是大小写问题。这种坑很隐蔽,建议用dm_dump_all或者打开DEBUG级别的 dm 日志来确认匹配过程。

3.3 uclass 的自动创建与 driver 的注册时机

uclass 不是凭空出现的,它也需要被创建。uclass 的创建有两种途径:

一种是自动创建。当第一个属于某个 uclass id 的设备被 bind 时,如果这个 uclass 还不存在,dm 会自动调用uclass_add创建一个。这依赖UCLASS_DRIVER宏注册的 uclass driver,里面定义了 uclass 的名字、id、以及可选的post_bind、pre_probe等回调。

另一种是显式创建。某些核心 uclass(比如 root、sysreset)会在dm_init阶段就被提前创建好,因为它们是整个 dm 框架的基础。

driver 的注册则依赖U_BOOT_DRIVER宏。这个宏会把 driver 结构体放到一个特殊的链接段(.u_boot_list)里,启动时 dm 通过遍历这个段就能拿到所有已注册的 driver。这种"链接段收集"的手法在 u-boot 里很常见,好处是不需要手动维护一张 driver 注册表,新增驱动只要写宏就行。

我踩过的一个坑是:driver 的name字段必须全局唯一。如果你两个 driver 起了同一个名字,链接时不会报错,但运行时 dm 查找 driver 可能会拿到错误的那个,导致 probe 行为诡异。这个问题的排查成本很高,因为现象往往不是直接崩溃,而是某个设备行为不对。建议在新增 driver 时,用grep全局搜一下名字有没有重复。

4. 设备树扫描在 dm 骨架里的具体作用路径

4.1 dm_scan_fdt_devices 如何遍历设备树节点

dm_scan_fdt_devices是设备树路径的核心。它的逻辑其实不复杂:从设备树的根节点开始,递归遍历所有子节点,对每个节点调用dm_scan_fdt_node。

dm_scan_fdt_node会做几个判断:

第一,检查节点是否有compatible属性。没有 compatible 的节点通常只是容器节点(比如soc { }这种纯层级节点),不需要绑定设备,但它的子节点还是要继续遍历。

第二,检查节点的status属性。如果是"disabled"或者"fail",就跳过这个节点及其子节点。这是设备树的标准机制,用来在板级配置里裁剪不需要的设备。

第三,检查节点是否已经被绑定过。dm 会维护一个已绑定节点的记录,避免重复绑定。这在有多个扫描入口(platdata 和 fdt 同时存在)时很重要。

第四,调用lists_bind_fdt尝试匹配并绑定。如果匹配到 driver,就创建 udevice;如果没匹配到,默认情况下只是打印一条 debug 日志,不会报错。这一点很关键——设备树里有节点但没驱动,u-boot 不会因此启动失败,只是那个设备用不了。

4.2 ofnode 与 udevice 的映射关系维护

设备树节点在 dm 里用ofnode表示,它本质上是一个指向设备树节点的句柄。每个从设备树绑定来的udevice都会保存自己的ofnode,这样驱动在 probe 时就能通过dev_ofnode(dev)拿到节点,去读取自己需要的属性。

这个映射关系是双向的:从 udevice 可以拿到 ofnode,从 ofnode 也可以通过ofnode_to_dev之类的接口反查 udevice。后者在中断控制器、时钟树这种需要"从设备树节点找设备"的场景里特别有用。

我遇到过一个典型问题:某个驱动在 probe 时用ofnode去读属性,结果读出来是空。排查后发现,是因为这个设备在设备树里被放在了错误的父节点下,导致ofnode的路径不对。设备树的层级结构不是随便写的,它会影响 dm 的父子关系和 probe 顺序。父设备先于子设备 probe是 dm 的一条基本规则,如果你把有依赖关系的设备放错了层级,probe 顺序就会乱。

4.3 设备树里 status 属性对骨架裁剪的影响

status属性是设备树裁剪的开关。在 dm 扫描时,status = "disabled"的节点会被整个跳过,包括它的子节点。这个机制在多板共用一个 dtsi 的场景下非常有用——你可以把 SoC 通用的外设都写在 dtsi 里,然后在具体板子的 dts 里用status开关来裁剪。

但这里有个容易忽略的点:u-boot 和 Linux 对 status 的处理基本一致,但 u-boot 的 dm 扫描发生在很早的阶段。如果你的设备树里某个节点依赖另一个节点的初始化结果,而那个节点被 disabled 了,就会导致 probe 失败。我建议在调试阶段,先用dm_dump_all确认实际被绑定的设备列表,再对照设备树看哪些节点被裁掉了,这样能快速定位"设备树里明明有,但 dm 里找不到"的问题。

另外,u-boot,dm-pre-reloc这个属性值得单独提一下。它标记的设备会在重定位前就被绑定和 probe,用于那些board_init_f阶段就要用的设备(比如调试串口)。这个属性用错了会导致启动早期崩溃,用少了又会导致早期 log 出不来,需要根据实际需求权衡。

5. 驱动 probe 顺序与依赖处理的实战经验

5.1 probe 顺序由什么决定

probe 顺序是 dm 调试里最让人头疼的问题之一。它主要由三个因素决定:

第一,设备树的层级。父设备总是先于子设备 probe。这是 dm 的硬性规则,因为子设备可能需要访问父设备的资源。

第二,uclass 的pre_probe和post_probe回调。某些 uclass 会定义这些回调,用来在设备 probe 前后做一些统一处理。比如 clock uclass 可能会在 pre_probe 里确保时钟源已经就绪。

第三,u-boot,dm-pre-reloc标记。带这个标记的设备会在早期阶段优先 probe,不受常规顺序约束。

除此之外,dm 本身不保证同一层级、同一 uclass 下设备的 probe 顺序。如果你有两个设备有依赖关系,不能指望它们按设备树里的书写顺序 probe。正确做法是用depends或者显式地在驱动里调用device_get_by_ofnode来触发依赖设备的 probe。

5.2 用 depends 和 uclass 回调解决依赖

depends是 u-boot dm 提供的一个依赖声明机制。在 driver 结构体里,你可以用DM_DRIVER_ALIAS或者相关的宏来声明"我这个驱动依赖某个 uclass 或某个具体设备"。dm 在 probe 时会先确保依赖项已经 probe 完成。

不过说实话,depends在实际项目里用得不算多,因为它的表达能力有限,而且容易和 probe 顺序的其他规则冲突。更常见的做法是在 probe 函数里显式获取依赖设备:

static int my_driver_probe(struct udevice *dev) { struct udevice *clk; int ret; ret = clk_get_by_index(dev, 0, &clk); if (ret) { dev_err(dev, "failed to get clock: %d\n", ret); return ret; } /* 此时 clk 已经被 probe 完成,可以安全使用 */ ret = clk_enable(clk); ... }

这种写法的好处是依赖关系明确、可读性强,而且clk_get_by_index内部会自动触发 clock 设备的 probe。缺点是如果依赖链很深,可能会导致 probe 递归过深,需要留意栈空间。

5.3 probe 失败的常见原因与排查手法

probe 失败的原因五花八门,我按出现频率排个序:

失败原因典型现象排查手法
时钟未就绪probe 返回 -ENODEV 或超时检查 clock 节点 status、确认 clk_get 返回值
GPIO 申请失败probe 返回 -EBUSY用 gpio_request 的返回值定位,检查 pinmux 冲突
电源域未开寄存器读写全 0 或全 F确认 power domain 驱动是否 probe、regulator 是否 enable
设备树属性缺失probe 里读属性返回空用 ofnode 逐属性打印,对照 dts 确认
compatible 不匹配设备根本没被 bind打开 dm debug 日志,确认匹配过程
驱动 name 冲突行为诡异、偶发失败grep 全局搜 driver name 是否重复

我个人的排查习惯是:先在 probe 函数入口加一条 dev_info,确认它有没有被调用。如果没被调用,问题在 bind 阶段;如果被调用了但返回错误,问题在 probe 内部。这一步能砍掉一半的无效排查。

还有一个技巧是善用dm tree命令(如果板子的 u-boot 支持命令行)。它能以树状结构打印所有设备和 uclass,直观展示父子关系和 probe 状态。比dm_dump_all的平铺列表好用得多。

6. 从零搭建一个能被 board_init_r 正确识别的 dm 驱动

6.1 驱动骨架的最小组成

一个能被 dm 正确识别的最小驱动,需要这么几个部分:

/* 1. 私有数据结构 */ struct my_dev_priv { void __iomem *base; struct clk clk; }; /* 2. 驱动 ops */ static int my_dev_of_to_plat(struct udevice *dev) { struct my_dev_priv *priv = dev_get_priv(dev); /* 解析设备树,填充 priv */ priv->base = dev_read_addr_ptr(dev); return 0; } static int my_dev_probe(struct udevice *dev) { struct my_dev_priv *priv = dev_get_priv(dev); /* 硬件初始化 */ return 0; } /* 3. of_match 表 */ static const struct udevice_id my_dev_ids[] = { { .compatible = "vendor,my-device" }, { } }; /* 4. driver 结构体 */ U_BOOT_DRIVER(my_dev) = { .name = "my_dev", .id = UCLASS_MISC, .of_match = my_dev_ids, .of_to_plat = my_dev_of_to_plat, .probe = my_dev_probe, .priv_auto = sizeof(struct my_dev_priv), };

这里有几个关键点:

of_to_plat是较新版本 u-boot 引入的回调,专门用来把设备树信息转换成平台数据。老版本可能直接在 probe 里做这件事。它的好处是职责分离——解析设备树和初始化硬件分开,便于测试和复用。

priv_auto让 dm 自动分配私有数据空间,省去了手动 malloc 的麻烦。注意它的单位是字节,dm 会在 bind 时自动分配。

id字段指定这个驱动属于哪个 uclass。如果你不确定该用哪个,UCLASS_MISC是个万能兜底,但更好的做法是找到最贴切的 uclass,这样能复用 uclass 提供的通用接口。

6.2 设备树节点的正确写法

驱动写好了,设备树节点也得配对:

my_device: my-device@10000000 { compatible = "vendor,my-device"; reg = <0x10000000 0x1000>; clocks = <&clk_my_dev>; status = "okay"; };

compatible必须和驱动里的of_match完全一致。reg提供寄存器基地址和长度,dev_read_addr_ptr会读这个属性。clocks是时钟引用,驱动里用clk_get_by_index获取。

如果你的设备有中断,还要加interrupts和interrupt-parent;有 GPIO 就加gpios。这些标准属性 dm 都有对应的读取接口,不用自己解析。

提示:设备树节点名(my-device@10000000里的my-device部分)建议和 compatible 的后半段保持一致,这是设备树的标准约定,便于阅读和维护。虽然 dm 不强制要求,但养成好习惯能省很多事。

6.3 验证驱动是否被正确 bind 和 probe

驱动写完后,怎么确认它被 dm 正确识别了?我通常按这个顺序验证:

第一步,编译后检查.u_boot_list段里有没有你的 driver。可以用nm或者objdump看符号,确认U_BOOT_DRIVER宏生成的符号存在。

第二步,启动时打开 dm 的 debug 日志。在board_init_r之前设置gd->flags |= GD_FLG_DM_DEBUG,或者在配置里打开CONFIG_DM_DEBUG。这样能看到 bind 和 probe 的详细过程。

第三步,用dm_dump_all或dm tree确认设备出现在列表里,且状态是 probed。

第四步,在 probe 函数里加dev_info,确认它被调用了,且返回值是 0。

这四步走下来,基本能覆盖 90% 的"驱动不工作"问题。剩下的 10% 通常是硬件层面的问题,比如寄存器地址写错、时钟频率不对,那就需要上示波器或者逻辑分析仪了。

7. 几个我踩过的 dm 骨架相关坑

7.1 设备树节点放错层级导致 probe 顺序错乱

这个坑我在一个电源管理芯片的驱动上踩过。当时把 PMIC 节点放在了 I2C 控制器节点的同级,而不是子级。结果 PMIC 的 probe 早于 I2C 控制器,导致 I2C 通信全部失败。

dm 的父子关系直接映射设备树的层级。有依赖关系的设备,子设备必须放在父设备节点下面。PMIC 挂在 I2C 上,就应该写成 I2C 节点的子节点。这样 dm 会先 probe I2C 控制器,再 probe PMIC,顺序天然正确。

排查这个问题的关键是看dm tree的输出。如果发现某个设备的父设备不是预期的那个,基本就能定位到设备树层级写错了。

7.2 uclass id 冲突引发的诡异行为

uclass id 是 dm 用来区分不同类别设备的标识。u-boot 预定义了一批标准 id(UCLASS_GPIO、UCLASS_I2C等),也允许自定义 id。自定义 id 必须从UCLASS_PRIVATE之后开始,否则会和标准 id 冲突。

我见过一个项目,两个不同的驱动用了同一个自定义 uclass id,结果 dm 把它们当成同一类设备管理,导致uclass_get_device拿到的设备时对时错。这种问题的现象非常随机,排查起来很痛苦。

避免方法很简单:自定义 uclass id 统一定义在一个头文件里,用UCLASS_PRIVATE作为起始偏移,并且加注释说明每个 id 的用途。新增时先 grep 确认没重复。

7.3 probe 函数里做太重的事情导致启动变慢

dm 的惰性 probe 设计本来是为了加快启动,但如果你在 probe 里做了太多事情(比如扫描整个 I2C 总线、读取大块 EEPROM),启动时间就会明显变长。

我的建议是:probe 只做"让设备可用"的最小初始化,把耗时的操作推迟到实际使用时。比如网口驱动,probe 里只初始化 MAC 控制器和 PHY 的基本寄存器,真正的链路协商放到start回调里。这样即使网口在启动阶段没被用到,也不会拖慢启动。

如果确实需要在启动阶段初始化某个耗时设备,可以考虑用DM_FLAG_ACTIVE_DMA之类的标记控制它的 probe 时机,或者干脆放到board_init_r之后的板级代码里手动触发。

7.4 忽略 of_to_plat 和 probe 的职责边界

of_to_plat和probe的职责边界,官方文档讲得比较清楚:of_to_plat负责把设备树信息转换成平台数据,probe负责硬件初始化。但实际写代码时很容易混。

我见过有人在of_to_plat里就去读写寄存器,结果因为时钟还没开而失败。也见过有人在probe里重新解析设备树,浪费了of_to_plat已经做好的工作。

正确的做法是:of_to_plat只做纯数据转换,不碰硬件;probe只做硬件初始化,不再解析设备树。这样职责清晰,也便于单元测试——你可以单独测试of_to_plat的转换逻辑,不需要真实硬件。

8. 把 dm 骨架图刻在脑子里

回到最开始的那个类比:board_init_r是总装车间,dm_init_and_scan是总装指令,udevice、uclass、driver是工位、车间和工人,设备树是图纸,probe 是真正开工。这套骨架搭好之后,u-boot 的整个驱动体系就能有条不紊地运转起来。

我个人的体会是,理解 dm 骨架最好的方式不是死记函数调用顺序,而是自己动手加一个驱动,然后顺着 bind 和 probe 的路径走一遍。你会遇到 compatible 不匹配、probe 顺序不对、uclass id 冲突这些真实问题,解决它们的过程比看十遍文档都管用。

最后分享一个实用技巧:在board_init_r里dm_init_and_scan调用前后各加一条dm_dump_all,对比两次输出,你就能直观看到 dm 骨架是怎么从无到有建立起来的。这个方法我在带新人的时候屡试不爽,比任何架构图都直观。

返回列表