U-Boot设备模型初始化全解析:从board_init_r到dm_init_and_scan

发布时间:2026/11/6 1:08:43

U-Boot设备模型初始化全解析:从board_init_r到dm_init_and_scan 不少人在移植U-Boot的时候都遇过这种场景设备树里节点写得明明白白驱动文件也编译进去了可启动以后设备就是死活找不到。查来查去最后发现board_init_r里那套dm驱动模型压根没按预期搭起来。如果你也有类似困惑这篇就是为你准备的。我打算从board_init_r这个入口出发把U-Boot的设备模型driver model简称dm骨架是如何一层层建立起来的彻底拆开揉碎。全文以U-Boot v2023.04源码为参考适合正在读源码、准备移植驱动到新板子的开发者即便你刚接触U-Boot把这几条初始化链路跟下来之后看任何子系统驱动都不会再觉得dm是个黑盒。1. 为什么非要先看board_init_rdm启动前的三件大事1.1 入口那么多为什么偏偏是board_init_rU-Boot从复位向量开始经历汇编、SPL、重定位最终进入一个负责整机初始化的C函数board_init_r。很多初学者翻源码时第一反应是去找board_init_f但真正影响dm骨架的恰恰是board_init_r这个阶段。原因不复杂dm需要依赖设备树fdt、需要内存堆来分配udevice、需要链表系统可以正常工作这些条件在board_init_f阶段往往只准备了一半重定位完成后的board_init_r才是所有资源都齐活的时机。你在board_init_r里会看到一张非常长的初始化列表名为init_sequence_r。它本质上是一个函数指针数组U-Boot主流程会按顺序逐个调用里面的函数。传统外设初始化函数散落在各处而dm的初始化也被抽象成其中一个initcall。这样做的好处是无论板子是什么架构、什么厂商初始化顺序都统一由这张表管理移植者只需要关注自己的驱动注册逻辑。1.2 从initcall看dm初始化在启动序列里的位置为了直观感受我把简化后的init_sequence_r拿出来看static init_fnc_t init_sequence_r[] { initr_trace, initr_reloc, initr_caches, ... initr_dm, ... initr_announce, initr_jumptable, initr_console, ... initr_lcd, initr_mmc, ... NULL, };其中initr_dm这个函数就是dm初始化的入口它的定义往往只有一行static int initr_dm(void) { return dm_init_and_scan(true); }也就是说整个设备模型的起点其实是dm_init_and_scan。别小看这短短一次调用它内部要完成两件大事先建立驱动模型内部的管理骨架再扫描设备树把节点全部变成dm对象。后面两章我会把这两个部分逐一拆开。提示不同版本U-Boot里init_sequence_r的排列顺序会有出入但initr_dm这个语义基本保留了。如果你在调试时发现驱动probe太早或太晚优先确认自己的board_init_r版本中initr_dm的位置。2. dm骨架的三个核心概念udevice、uclass、uclass_driver如何在内存中串起来2.1 管理台账里的三个角色说黑盒之前先记住一句话dm不是一个复杂的框架它只是用链表把三类对象串了起来。这三类对象分别是udevice、uclass、uclass_driver。我把它们类比成物业公司的三张表udevice每一户业主。对应设备树里的一个节点描述“硬件某处有一个外设”。uclass一个业主群。把同类设备归到同一个群里比如所有GPIO控制器归到gpio这个uclass。uclass_driver这个群的管理公约。规定这个类别的设备必须提供哪些操作接口比如GPIO类必须能读、能写、能设置方向。硬件上可以有很多GPIO控制器每个控制器有自己的udevice但它们都挂在同一个uclass上拥有同一个uclass_driver。上层代码要操作GPIO时根本不关心具体是哪个厂商的控制器只要调用uclass规定的API即可。这就是dm最核心的价值让驱动开发从“逐个外设各写一套”变成“按类别统一抽象”。2.2 关键数据结构里都有什么下面的表格列出了三个核心结构里最值得关注的字段。不需要死记但调试时你需要知道它们的存在。结构体关键字段作用struct udevicepriv_ / plat_驱动私有数据和平台数据指针probe时分配struct udeviceuclass / uclass_priv指向所属uclass以及该设备在uclass中的私有数据struct udevicedriver / parent匹配到的driver以及父设备struct udevicedev_head / sibling_node挂入全局设备链表和父设备的子链表struct uclassdev_head同类设备的链表头struct uclassuclass_drv指向该类的uclass_driverstruct uclass_driverpost_bind / pre_probe / post_probe类级别生命周期回调struct uclass_driverops该类别设备的统一操作接口链表关系是理解dm的钥匙。每个udevice有两个链表节点一个用来把自己挂进所属uclass的dev_head另一个用来把自己挂到父设备的子设备链表里。设备树中的父子关系就这样被搬进了内存里的链表结构。再补充一个容易混淆的点driver和uclass_driver是两个层面的东西。driver是具体到某个型号外设的驱动它通过U_BOOT_DRIVER宏注册uclass_driver是类别的驱动它通过UCLASS_DRIVER宏注册。一个驱动既有自己的driver又必须指明它属于哪个uclass。比如你的i2c GPIO扩展芯片驱动名列前茅的是它的具体driver同时它必须挂靠gpio这个uclass。3. 一步一步拆解dm_init_and_scan从链表到设备节点的完整链路3.1 dm_init先搭管理骨架dm_init_and_scan既然从名字就能看出两个动作我们就按顺序看。首先是dm_init它专门负责初始化全局的设备模型骨架。在这个阶段U-Boot会分配并初始化一个根设备对象我习惯叫它root device同时初始化所有uclass的管理链表。int dm_init_and_scan(bool early) { int ret; ret dm_init(early); ... ret dm_scan(NULL); ... return 0; }dm_init内部会调用device_bind_common创建一个root device这个设备没有实际的硬件对应只是作为设备树根节点在dm里的映射。所有顶层子节点最终都会挂在它下面。与此同时gd-dm_root会被赋值整个dm世界的入口指针就此确定。这里有个细节值得留意dm_init创建root device时其实调用的也是标准的device_bind流程。也就是说即使是最顶层的根节点也要走一遍驱动匹配的流程。只不过root device比较特殊它由DM内部的root_driver来匹配不依赖设备树里任何compatible字段。3.2 dm_scan把设备树变成内存链表骨架搭完下一步是dm_scan。它的任务非常明确递归遍历设备树找出所有需要纳入dm管理的节点为每个节点创建一个对应的udevice。int dm_scan(struct udevice *parent) { if (parent NULL) parent dm_root(); return dm_scan_fdt_node(parent, gd-fdt_blob, 0, true); }dm_scan_fdt_node遍历某个设备树节点的子节点。如果没有指定parent就是从root device开始遍历根节点。你可以把这一步理解成拿着设备树这棵静态的树在内存里照着一模一样地重建一棵动态链表树。每个设备树节点对应一个udevice父子关系保持完全一致。遍历过程中真正干活的是lists_bind_fdt。它会对当前节点去全局driver链表里逐个比对看有没有driver能够匹配这个节点。如果匹配成功就调用device_bind_common为这个节点分配一个udevice。3.3 设备节点的绑定匹配过程驱动是如何跟设备节点匹配上的关键在于设备树里的compatible属性。static int lists_bind_fdt(struct udevice *parent, ofnode node, struct udevice **devp) { ... for (entry driver_list; entry; entry entry-next) { struct driver *drv entry-driver; ... if (!driver_check_compatible(drv, ofnode_get_name(node))) { ... ret device_bind_common(parent, drv, name, NULL, node, 0, devp); ... } } }每个通过U_BOOT_DRIVER宏注册的driver都保存着一个或多个compatible字符串。driver_check_compatible会拿这些字符串跟设备树节点的compatible属性对比。一旦匹配上就完成绑定。绑定成功的标志就是一个新的udevice被创建并挂入链表。但请注意绑定阶段不做任何硬件操作。U-Boot不会在绑定的时候去读写寄存器也不会申请中断资源。它做的事情只是登记把设备树节点信息翻译成内存中的udevice对象并指明这个对象将来由哪个driver处理。注意如果设备树里某个节点没有任何driver匹配dm_scan不会报错只会默默跳过。这也是很多驱动“没生效”却看不到异常日志的原因。4. 驱动匹配背后compatible、uclass_bind_device与probe时机4.1 懒加载bind只是登记probe才是真正干活读完上一章你可能会有一个疑问如果dm_scan把所有节点都绑定了为什么不顺便把驱动probe一遍一次性初始化完呢答案很简单不需要。U-Boot的dm使用了懒加载机制。设备只有在被使用时也就是上层代码通过API主动获取设备时才会真正触发probe。这样的好处是一块复杂的板子可能设备树里有几百个节点但实际启动时只需要用到其中几十个其余设备根本不需要初始化省时省电。触发probe的常见API包括int uclass_get_device(uclass_id, devnum, struct udevice **devp); int uclass_get_device_by_name(uclass_id, const char *name, struct udevice **devp); int dm_get_device(struct udevice *dev, ofnode node, ...);这些API内部都会调用device_probe。device_probe的大致流程是int device_probe(struct udevice *dev) { ... if (dev-flags DM_FLAG_ACTIVATED) return 0; if (dev-parent) device_probe(dev-parent); ... if (dev-driver-of_to_platdata) dev-driver-of_to_platdata(dev); ... if (dev-driver-ops dev-uclass-uc_drv-pre_probe) dev-uclass-uc_drv-pre_probe(dev); ... if (dev-driver-probe) dev-driver-probe(dev); ... dev-flags | DM_FLAG_ACTIVATED; ... }注意到一个关键细节device_probe会先递归probe父设备。这保证了设备树中的依赖关系一定被满足。比如I2C设备在probe之前它所属的I2C控制器一定已经初始化完成。这是dm框架替你处理掉的一大类顺序问题。4.2 手写一个最小的uclass驱动验证从compatible到probe的完整路径光说不练没有用。这里我写一个基于虚拟设备的极简驱动展示一个完整的驱动骨架需要哪几样东西。设备树里定义一个节点{ virtual_demo { compatible sandbox,virtual-demo; reg 0x1000 0x100; }; };先注册一个uclass_driver也就是定义这个设备类别UCLASS_DRIVER(virtual_demo) { .name virtual_demo, .id UCLASS_MISC, };再注册具体的driverstatic int virtual_demo_probe(struct udevice *dev) { printf(virtual demovice probe done, reg_base%lx\n, dev_read_addr(dev)); return 0; } static const struct udevice_id virtual_demo_ids[] { { .compatible sandbox,virtual-demo }, { } }; U_BOOT_DRIVER(virtual_demo) { .name virtual_demo, .id UCLASS_MISC, .of_match virtual_demo_ids, .probe virtual_demo_probe, };这段代码里U_BOOT_DRIVER负责注册具体驱动id字段把驱动挂到UCLASS_MISC这个uclass上of_match数组提供compatible匹配表。把这段代码编译进U-Boot后dm_scan阶段就能在设备树里找到这个虚拟设备完成bind之后如果你调用uclass_get_device去获取它probe函数就会打印reg地址。这个例子虽小五脏俱全。实际项目里的GPIO、SD、网络驱动本质也是同样的结构只是uclass_driver里多了ops回调。5. 移植实战当我给一个新平台添加dm驱动时踩过的坑5.1 bind失败不报错的假象如何从dm tree揪出失踪设备第一次给新平台移植驱动时我遇到过最诡异的情况是驱动编译进去了设备树节点也有了满心欢喜启动后日志里却找不到任何跟驱动相关的打印。U-Boot完全没有报错系统正常运行只是我的设备就好像不存在一样。后来通过dm命令发现了真相。U-Boot的dm tree命令可以打印当前所有已绑定的udevice以及它们的驱动名、状态和uclass。用法很简单在U-Boot控制台输入 dm tree输出大致长这样Class Probed Name ---------------------------------------- misc [ ] root_driver misc [ ] virtual_demo ...如果我的virtual_demo连一行都没有出现说明设备压根没有bind成功。这时候排查顺序很关键确认设备树节点编译进了最终blob。在U-Boot下执行fdt list检查节点是否存在。确认driver里的compatible字符串和设备树里的compatible是否完全一致包括大小写和标点。确认U_BOOT_DRIVER宏是否真的编译进去了。有时候Makefile和Kconfig没有把文件加进构建体系驱动对象根本没进镜像。前两步是最高频的坑尤其compatible字符串不一致最常见的是少打了个逗号或者型号后缀写错。5.2 probe阶段卡死或段错误私有数据priv与platdata的坑bind没问题之后下一个坑往往出在probe阶段。设备在dm tree里能看到一操作就卡死或者直接段错误这通常发生在驱动访问私有数据的时候。很多新手在probe里写了一行my_priv-base_addr dev_read_addr(dev);却没意识到my_priv这个指针可能为NULL。dm并不会自动帮你分配priv空间前提是你在U_BOOT_DRIVER里定义了.priv_auto。正确的声明方式是U_BOOT_DRIVER(virtual_demo) { ... .priv_auto sizeof(struct virtual_demo_priv), ... };加上这行之后dm会在bind/probe阶段自动为dev-priv分配好内存。同理如果需要读取设备树信息存到平台数据中用.plat_auto声明即可。另外访问设备树属性时尽量用dev_read_xxx系列API而不是直接操作fdt指针。dev_read_xxx会自动处理“实时设备树”和“扁平设备树”两种模式直接操作fdt_blob往往在开启OF_LIVE后失效。5.3 用dm命令验证骨架dm tree、dm uclass、dm devres的实操姿势调试dm骨架阶段除了dm tree还有两个命令值得养成习惯 dm uclass dm devresdm uclass可以列出所有uclass以及每个uclass下的设备数量。当你怀疑某个设备归类错误时这个命令很有用。dm devres则能显示设备的内存资源映射情况。设备在probe过程中申请的资源、映射的地址全部记录在devres里。如果probe之后地址映射异常这里能直接看到。我自己的调试习惯是在board_init_r里initr_dm附近加一个debug打印确认dm_init_and_scan是否正常返回static int initr_dm(void) { int ret dm_init_and_scan(true); printf(dm init and scan: ret%d\n, ret); return ret; }这样每次启动都能第一时间知道是dm骨架没搭起来还是后续驱动probe出了问题。从经验来看骨架层的失败极少绝大多数问题还是集中在compatible不匹配和私有数据未分配上。踩过几次坑之后我个人最大的体会是把dm当成一个静态的登记系统bind是造册probe才是干活。遇到驱动不生效先看dm tree确认登记造册是否完成再查probe是否被触发按这两步排查比盲目加打印效率高得多。希望这篇从board_init_r到dm_init_and_scan的拆解能帮你少走弯路。
延伸阅读

更多相关文章

2026/11/6 1:08:43

I2C时钟延展与死锁恢复:嵌入式总线鲁棒性实战

/* 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 1:08:43

ESP32一站式智能家居:WiFi+BLE双模网关从零搭建

/* 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 1:08:43

U-Boot设备模型初始化全解析:从board_init_r到dm_init_and_scan

/* 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 1:08:43

I2C时钟延展与死锁恢复:嵌入式总线鲁棒性实战

/* 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 1:08:43

ESP32一站式智能家居:WiFi+BLE双模网关从零搭建

/* 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 1:08:43

偶发故障排查实战:串口假故障、蓝牙断连与烧录批次对照

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

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* 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 1:03:39

MCU晶振电路设计全解析:从负载电容计算到PCB布局调试

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

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑