news 2026/11/6 3:00:44

U-Boot设备模型全解析:board_init_r阶段驱动骨架如何搭起来

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
U-Boot设备模型全解析:board_init_r阶段驱动骨架如何搭起来

很多人一看到U-Boot里board_init_r那一长串initr_xxx就头疼,其实只要抓住设备模型(dm)里几个关键角色,整条启动路径就不难理解。这篇文章就锁定一件事:U-Boot在重定位完成后的board_init_r阶段,究竟是怎么把dm的驱动骨架搭起来的——uclass怎么注册、driver怎么入表、设备树节点又怎么变成udevice。适合想给板子加驱动、排查设备加载不出来,以及被U_BOOT_DRIVER这类宏绕晕的人。

我自己当年也是被U_BOOT_DRIVER和链接脚本之间的关联搞得晕头转向,后来在u-boot.map里搜索自己的驱动名,一下子全通了。你把这套骨架看懂以后,再去看具体驱动的代码,基本就是照着流程图填函数的事。

1. 先搞清楚dm到底解决了什么问题

1.1 没有设备模型时,驱动代码是怎么“糊”出来的

在没有设备模型的传统U-Boot里,每加一个外设驱动,基本流程就是:在板级文件里加一个结构体数组,在init_sequence里插入一个初始化函数,不同板子之间靠#ifdef CONFIG_XXX来铺。代码越多,#ifdef越厚,最后板级文件膨胀到几千行,驱动也没法复用。

这种做法的痛点很直接:同样是GPIO控制器,A板子是一套寄存器,B板子是另一套寄存器。想复用驱动?要么复制粘贴,要么用一堆宏把寄存器地址硬编码。设备模型(dm)提供了一套统一抽象,驱动只负责描述“我能匹配哪个硬件、怎么做初始化、提供什么接口”,至于这个硬件挂在哪个设备树节点、什么时候被创建、依赖的父设备怎么先初始化,全部交给框架处理。

所以,理解dm要从一个角度看:它不是“必须用的新东西”,而是U-Boot社区为了解决驱动复用和启动初始化混乱而沉淀出来的框架。你现在看到的以uclass、driver、udevice为核心的架构,就是它的最终形态。

1.2 先认识三个关键结构体:uclass、driver、udevice

dm的核心数据结构其实就三个,搞懂这三个名词,后面的代码路径就通了。

  • uclass(设备类):一类设备的抽象接口。比如UCLASS_GPIO统一提供gpio_request、gpio_direction_input这类API,不管底层驱动是STM32还是全志,上层用法完全一样。
  • driver(具体驱动):对应某个具体硬件控制器。比如stm32_gpio、sunxi_gpio,每个driver都有一个.id指向某个uclass,还带一组of_match表用来和设备树节点匹配。
  • udevice(设备实例):设备树节点经过匹配后创建出来的实例,记录驱动是谁、父设备是谁、uclass是谁,以及各自的私有数据指针。

我习惯用一个类比来记:uclass像“岗位说明书”,规定这个岗位需要会哪些技能;driver是某个人的专业能力;udevice就是今天坐在工位上的那个人。岗位可以一直在,人(driver)可以换,但一旦“坐上去”形成了一个具体udevice,框架就能通过统一的uclass接口去操作它。

1.3 board_init_f和board_init_r:为什么骨架在这个阶段搭

不少初学者会把board_init_f和board_init_r混在一起。简单说,U-Boot的C语言运行环境初始化分两个大阶段。board_init_f是重定位之前,代码通常还跑在只读存储或早期RAM里,条件简陋,只能做时钟、DDR初始化等少量工作;board_init_r是重定位之后,代码已经搬到DDR里,可以放开手脚,绝大多数外设驱动都在这个阶段初始化。

设备模型的完整骨架正是在board_init_r里搭起来的。看代码的话,board_init_r里有个init_sequence_r数组,里面排了几十个initr_xxx函数:

static init_fnc_t const init_sequence_r[] = { initr_trace, initr_reloc, ... initr_dm, /* 设备模型初始化,dm骨架的关键入口 */ ... initr_serial, initr_announce, ... };

注意initr_dm的位置排在很前面,后面那些initr_serial、initr_mmc、initr_net函数都要依赖dm。dms如果没搭起来,后面全部空转。所以启动序列里,dm的初始化顺序靠前不是巧合,而是一个必然设计。

2. 从board_init_r到dm_init_and_scan:骨架搭建的第一推

2.1 initr_dm:启动序列里看似普通的一步

看到initr_dm的实现时,很多人会觉得“就这?”:

static int initr_dm(void) { return dm_init_and_scan(false); }

就这么一行,真正的活全在下游函数里。这个“普通”恰恰是dm设计得好,框架把复杂的注册、扫描、绑定逻辑全部封装起来,启动代码根本不用关心细节。从架构角度说,这种封装让U-Boot的启动序列非常稳定,新板卡适配基本不用改公共代码。

有一点需要注意,不同U-Boot版本里dm_init_and_scan的参数可能不一样。老版本可能传true,表示只扫描带pre-reloc标记的设备;新版本一般传false,要求把所有设备树节点都扫描绑定,让后续驱动都能正常使用。如果你在移植老代码,这个差异特别容易踩坑。

2.2 dm_init:root device是怎么诞生的

进入dm_init_and_scan后,第一步是dm_init。它做的事情是绑定一个根设备:

int dm_init(void) { if (gd->dm_root) return 0; return device_bind_common(NULL, &root_driver, "root", "root", NULL, -1, &gd->dm_root); }

gd->dm_root是全局数据里的一个指针,指向根设备。root_driver是一个id为UCLASS_ROOT的特殊驱动,它没有寄存器、没有硬件,只是作为整棵设备树的根节点。你可以把它理解成文件系统里的根目录“/”,后面所有udevice都是它的子节点或子节点的子节点。

这里要留意一个点:dm_init执行前,gd->dm_root必须为空。如果设备模型在board_init_f阶段已经初始化过,这里会直接返回。很多板子在SPL里已经跑过dm,跳到U-Boot正文后又走一遍board_init_r,正是靠这个幂等保护避免了重复初始化。这个防御逻辑看着简单,但在复杂启动流程里确实很关键。

2.3 dm_scan:从零开始扫描整个设备树

dm_init结束后,dm_scan开始干活:

int dm_scan(bool pre_reloc_only) { dm_scan_platdata(pre_reloc_only); return dm_scan_fdt(pre_reloc_only); }

这里有两个分支。dm_scan_platdata扫描的是静态定义的平台数据,也就是通过U_BOOT_DEVICE宏定义的设备,没有设备树的老板子走这条路。dm_scan_fdt才是现代U-Boot的主力路径,它从设备树根节点往下一层层遍历,把每个节点和driver匹配,匹配成功就创建一个udevice。

扫描过程并不是简单的递归。框架会检查节点的status属性,disabled节点直接跳过;还会根据pre_reloc_only参数决定是否只绑定带pre-reloc标记的设备。等扫描结束,设备树里所有active的硬件节点基本都被bind成了udevice,挂在root下面,形成一棵完整的设备树。这一步做完,dm骨架从“根目录”到“各个挂载点”就都建好了。

3. 链接脚本与U_BOOT_DRIVER:驱动表是怎么“自己长出来”的

3.1 编译期收集、运行期遍历:linker list的魔法

这里有个关键问题:dm_scan_fdt怎么知道U-Boot里有哪些driver?总不能靠一个写死的数组吧。U-Boot的做法是“编译期收集、运行期遍历”。这是整个设备模型最巧妙、也最容易劝退新人的地方。

当你写一个U_BOOT_DRIVER(my_drv)宏的时候,宏本质上是把driver结构体放到一个特定的section里:

#define U_BOOT_DRIVER(__name) \ ll_entry_declare(struct driver, __name, driver)

ll_entry_declare会把变量名变成_u_boot_list_2_driver_2_my_drv,属性是section(".u_boot_list_2_driver_2_my_drv")。也就是说,每个驱动都生成一个以自己名字命名的section。链接脚本里再把这些section按顺序排在一起:

.u_boot_list : { KEEP(*(SORT(.u_boot_list_2_driver_1))); KEEP(*(SORT(.u_boot_list_2_driver_2))); KEEP(*(SORT(.u_boot_list_2_driver_3))); KEEP(*(SORT(.u_boot_list_2_driver_4))); ... KEEP(*(SORT(.u_boot_list_2_uclass_1))); ... }

SORT()按名字排序,这样运行期从数组头遍历到数组尾,就能拿到所有driver。这个技术不只用于驱动,命令、clk、reset、dma等子系统全都用同一套linker list机制。所以一旦理解了它,很多“莫名奇妙就能被找到”的U-Boot设施,你都能一眼看穿。

3.2 U_BOOT_DRIVER、U_BOOT_UCLASS、U_BOOT_DRIVER_ALIAS

除了U_BOOT_DRIVER,你经常还会见到U_BOOT_UCLASS和U_BOOT_DRIVER_ALIAS。

U_BOOT_UCLASS用于注册类驱动,比如:

U_BOOT_UCLASS(misc) = { .id = UCLASS_MISC, .name = "misc", };

这个宏把uclass_driver结构体也放进linker list,运行期通过id查找对应的uclass_driver。

U_BOOT_DRIVER_ALIAS则是解决“不同厂商用同一个compatible”的问题。比如你实现了一个通用MMC驱动,想让这个driver也能匹配"mmc"这个通用compatible,就可以写U_BOOT_DRIVER_ALIAS(mmc_drv, mmc)。匹配时框架会额外查这层alias映射。

理解了这三个宏,你再去看任何驱动源码,开头那一小段定义就再也不是“咒语”了,而是有明确语义的注册语句。

3.3 怎么验证驱动有没有被编进来

这是非常实用的排查动作:去u-boot.map(编译生成的map文件)里搜你的驱动名。

_u_boot_list_2_driver_2_my_drv

如果搜到,说明driver结构体已经进入driver表。如果搜不到,多半是Kconfig没开、Makefile没编、或者链接脚本段没排进来。我在好几个平台都碰到过“代码明明写了U_BOOT_DRIVER但设备就是匹配不上”的问题,最后都是在map文件里发现符号根本没进去,一下子省下很多排查时间。

提示:如果你改了Kconfig或Makefile,一定记得重新编译,并且去map文件确认。不要只看代码,U-Boot这种链接器收集机制,必须以最终链接产物为准。

4. 从设备树节点到udevice:bind过程拆解

4.1 compatible匹配:我怎么认出你

dm_scan_fdt进入某个设备树节点后,会调用lists_bind_fdt。核心逻辑是遍历driver表,对每个driver调用driver_check_compatible,拿节点compatible和driver的of_match表对比:

const struct udevice_id demo_ids[] = { { .compatible = "myvendor,demo", .data = 42 }, { } };

只要有一个compatible字符串和节点匹配上,框架就认为“这个驱动能管这个硬件”,随即执行device_bind_common。这个函数做的事比较多,我拆解一下:

  • calloc一个struct udevice
  • 通过drv->id找到对应的uclass,把dev加入uclass的设备列表
  • 把dev挂到parent的child_list
  • 记录设备树偏移of_offset
  • 设置seq序号,也就是设备的编号
  • 调用uclass_bind_device回调

到这里,这个udevice在dm树里已经有了位置,但驱动还没跑起来。很多人在这个阶段就以为“设备可用了”,其实还差最后一步。

4.2 匹配失败的高频原因

我帮人排查设备加载问题,遇到最多的几个原因,列出来给大家参考:

  • compatible拼写不一致。dts里写的是vendor,device,驱动里却少了个逗号或者多了一个字母。这种错误最常见也最隐蔽,肉眼很难看出来,建议打开dm调试日志辅助定位。
  • status为disabled。status = "disabled"或"fail"的节点会被跳过,这是设计行为。排查时先看节点状态,别只盯着compatible。
  • driver没有of_match表。如果驱动只用U_BOOT_DEVICE静态平台数据,没有of_match,设备树扫描不会匹配它。
  • 父节点没绑定。如果某个父节点本身没能创建udevice,它下面的所有子节点都会出问题,经常表现为整棵子树消失。

4.3 bind和probe:一字之差,天壤之别

这是新手最容易翻车的概念。bind只是“建档”,把设备信息加入dm树;probe才是“上电初始化”,执行driver的probe函数。

一个udevice被bind之后,可能很长时间都不会probe,直到有代码真正去使用它。比如:

struct udevice *dev; uclass_get_device_by_name(UCLASS_MISC, "demo-device", &dev);

一旦调用这类接口,框架会立即触发device_probe。所以你在dm tree里如果看到某个设备Probed显示[ ],不用奇怪,它只是还没被使用而已。

顺便说,probe过程本身也有明确顺序:先执行uclass的pre_probe回调,再执行driver自己的probe,最后执行uclass的post_probe回调。如果驱动在probe阶段申请了资源——映射寄存器、获取时钟、复位设备——这些动作都发生在这里。

5. probe才是真正的初始化:时机、顺序与自定义驱动

5.1 谁会在什么时候去probe设备

实际开发中,有几种典型触发路径:

  • 通过uclass API查找设备,比如gpio_lookup_name、uclass_get_device_by_ofnode
  • 子系统初始化函数里显式调用uclass_get_device,比如mmc_probe、eth_initialize
  • 设备树中带u-boot,dm-pre-reloc或bootph-all等标志的设备,在早期扫描阶段就可能被主动probe

我的建议是:驱动里尽量把初始化都放进probe,不要在bind里做过多真实硬件操作。原因很简单,bind阶段整个环境可能还不完整,寄存器映射、时钟获取这类动作放到probe里更安全。如果你在bind里贸然访问寄存器,某些外设还没上电,轻则读回垃圾值,重则直接卡死。

5.2 probe的回调顺序细节

以device_probe为例,实际执行顺序大致是:

  1. 父设备probe(如果父设备还没probe,框架会递归先probe父设备)
  2. uclass的pre_probe或child_pre_probe
  3. driver自己的probe
  4. uclass的post_probe

第1点很容易被忽略,但非常重要。正因为父设备会先probe,很多复杂硬件——比如带电源域的控制器——只要简单传一下设备树节点,整个依赖链都能自动初始化。这也是设备树模型相对传统硬编码初始化的巨大优势,你不需要在板级文件里手动排序初始化函数。

5.3 手把手写一个最小demo

我在drivers/misc下新建一个文件demo_drv.c:

#include <common.h> #include <dm.h> #include <dm/device.h> static int demo_probe(struct udevice *dev) { printf("demo probe ok, data=%ld\n", dev_get_driver_data(dev)); return 0; } static const struct udevice_id demo_ids[] = { { .compatible = "myvendor,demo", .data = 42 }, { } }; U_BOOT_DRIVER(demo_drv) = { .name = "demo_drv", .id = UCLASS_MISC, .of_match = demo_ids, .probe = demo_probe, };

Makefile里加一行:

obj-$(CONFIG_MISC_DEMO) += demo_drv.o

Kconfig里给个开关:

config MISC_DEMO bool "Demo driver" depends on MISC && DM

设备树里加节点:

demo-device { compatible = "myvendor,demo"; status = "okay"; };

编译烧录后进U-Boot控制台:

=> dm tree Class Probed Driver Name ... misc [ + ] demo_drv |-- demo-device

probe成功会打印demo probe ok, data=42。这个demo虽然业务上毫无用处,但它是理解整套dm启动流程的最小闭环,建议花半小时跑一遍。跑通之后,你再去看任何真实驱动,代码结构基本都长这样。

6. 踩坑记录与排查手段

6.1 常见报错速查

我把实际调试中见过的典型问题和排查方向整理成一个速查表,方便大家定位:

现象优先排查方向
日志出现Cannot find uclassdrv->id指向的uclass没有注册,检查U_BOOT_UCLASS是否被编译进去
日志出现No such device设备树节点存在但没bind或没probe,先跑dm tree看设备在不在
probe失败或probe faileddriver的probe函数里出错,比如寄存器映射失败、时钟获取失败、GPIO请求失败
扫描阶段整个系统卡死某个节点compatible匹配到了不安全的驱动,或probe里出现死循环
控制台没有dm命令没有打开CONFIG_CMD_DM,或者链接脚本没排入命令段

6.2 dm命令族的正确用法

U-Boot控制台提供了几个调试命令,我用得最多的是:

  • dm tree:查看整棵设备树形结构以及每个设备的probe状态
  • dm uclass:列出所有uclass及设备个数
  • dm drivers:列出所有driver以及它们所属的uclass id
  • 部分版本还有dm dump,可以dump某个uclass下所有设备信息

如果命令行信息不够细,可以在drivers/core/device.c或drivers/core/lists.c顶部加#define DEBUG,重新编译后debug()函数会输出全流程。你能看到每个节点正在匹配哪个driver、为什么没匹配上。这是定位“compatible拼错”类问题的终极大招,比对着代码猜快得多。

6.3 一点实在的经验

我自己最大的体会是,dm这套东西,看十篇文章都不如亲手加一个驱动跑通一次。尤其是linker list机制,就算原理背得再熟,第一次在u-boot.map里看到_u_boot_list_2_driver_2_...和自己写的驱动名对齐时,那种“原来是这样”的踏实感很难替代。建议你手上准备一块开发板,把上面那个demo改改名字、换换compatible,来回跑几遍dm tree,你对设备模型的掌控力会立刻不一样。

还有一个技巧分享给你:在调试阶段,故意把一个compatible写错,然后加#define DEBUG看扫描日志,你会发现框架其实会把所有driver过一遍,然后明确告诉你“这个节点没匹配到任何驱动”。看到这个输出之后,你对dm扫描流程的理解就彻底落地了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 3:00:43

STM32启动流程深度解析:从上电到RTOS第一个任务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:00:07

光伏逆变器与AIDC算力电源协同系统方案解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:00:07

实际电感线圈下的并联谐振与滤波电路:Q值和谐振阻抗的影响

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:00:07

STM32上电启动全流程解析:从复位向量到RTOS任务切换

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 2:59:52

STM32寄存器级原理与GPIO/PWM实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 2:58:36

DP365替代LT3652实战:太阳能MPPT充电设计选型与调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华