我一直觉得,U-Boot里看懂DM(Driver Model)驱动骨架,是嵌入式引导开发里的一道分水岭。很多人能改设备树、能看串口日志,但一遇到“为什么我的设备没被绑定”、“为什么probe函数压根没进”就抓瞎,根本原因就是不清楚board_init_r里这一摊DM初始化到底是怎么搭起来的。这篇文章不做空泛的原理介绍,直接以board_init_r为入口,把DM初始化的调用链一路拆下去:initr_dm、dm_init_and_scan、dm_init、dm_scan,看到最后你就能明白一个设备树节点是怎么一步步变成可用的udevice实例。内容适合正在啃U-Boot启动流程、想在U-Boot里添加自定义外设驱动的BSP工程师,也适合想从全局理解驱动框架的嵌入式入门者。
1. 先把DM是什么说清楚再动手
1.1 传统U-Boot驱动代码的痛点
在DM框架大规模铺开之前,U-Boot里的驱动代码是很“原始”的。每换一块板子,经常要往board目录塞一堆初始化函数,串口、GPIO、I2C各自为战,寄存器地址到处硬编码,驱动之间几乎没有抽象层。最烦的是你想换一颗PHY芯片,往往要把原来的驱动结构体翻个底朝天,甚至从头改一遍回调函数,代码复用基本靠复制粘贴。
DM出来之后,情况有了根本变化。它把驱动的组织方式拆成了三个层次:device表示设备实例,driver表示驱动本身,uclass表示设备类别。设备树里每一个兼容节点,都有可能被一个driver接管,再由uclass统一管理这一类设备的行为。听起来像Linux的driver core,但U-Boot为了保持轻量,只实现了最核心的那几条线。理解了这个思想,后面看代码才不会迷路。
1.2 DM框架的四个核心对象
要理解DM骨架,先记住这几个结构体,后面所有代码都绕着它们转:
struct udevice:设备实例,代表设备树里的一个节点,或者一份platform data。struct driver:驱动本体,包含驱动名、所属uclass的id、of_match匹配表、probe函数等。struct uclass:设备类,比如所有GPIO设备属于UCLASS_GPIO,所有串口属于UCLASS_SERIAL。一个uclass管理同一类设备的公共行为。struct uclass_driver:类级驱动,定义uclass层面的钩子函数,比如post_bind、pre_probe、post_probe。
除了这四个,还有一个容易忽略的角色:gd->dm_root,它是DM树的根设备。所有设备最终都挂在这个根下面,可以理解成文件系统里的根目录。没有这个根,后面所有uclass_get_device、device_probe都无从谈起。
1.3 为什么骨架要从board_init_r讲
很多新手看DM源码,喜欢从drivers/core/device.c开始一读到底,结果越读越蒙。原因在于这个框架不是“先初始化完再用”,而是“先用起来再初始化”。U-Boot在board_init_f阶段就已经有DM在跑了,但那个阶段很多资源受限,真正完整的设备扫描和全量绑定,发生在重定位进RAM后的board_init_r里。
所以,理解了board_init_r这一整条初始化链,就掌握了DM的主干,剩下的device_probe、uclass操作都只是分支细节。这也是我写这篇文章的初衷:不扯别的,就盯着board_init_r,把骨架拆给你看。
2. 从start.S到board_init_r:DM骨架在哪一步开始搭
2.1 U-Boot启动路径回忆
先简单捋一下U-Boot的启动路径。硬件复位后,CPU从start.S开始执行,完成基本异常向量、低等级初始化,然后进入C语言阶段的board_init_f。这个阶段代码通常还在Flash或SRAM里运行,使用位置无关代码,主要任务是初始化早期时钟、串口、内存等,并且把全局数据结构gd准备好。
board_init_f做完内存初始化后,会把代码和数据relocate到RAM,然后跳到board_init_r。board_init_r的名字很容易让人以为它是“板级第二阶段的普通初始化”,但它的地位远比想象中重要。几乎所有主流程关键初始化,比如malloc堆、DM设备模型、串口的完整初始化、网卡、mmc扫描等,都在这个函数所驱动的init_sequence_r列表里。
可以这么说:board_init_f是保命的草稿,board_init_r才是正式开工的蓝图。DM从草稿阶段开始参与,但真正搭骨架是在蓝图阶段。
2.2 board_f阶段迫不得已的“半成品”DM
board_init_f阶段,init_sequence_f里有一个initf_dm,它会调用dm_init_and_scan(true)。这里的第二个参数pre_reloc_only是true,含义是只绑定那些带有“重定位前可用”标记的设备。为什么这么严格?
因为此时malloc堆还没有完整建立,很多驱动依赖的时钟、电源、pinctrl都没有起来。如果强行把设备树里所有节点都绑一遍,大概率会在probe阶段翻车。所以U-Boot只让串口这类最基础、必须在早期打印日志的设备参与DM,其余设备统统押后。
你可以在设备树节点里看到这样的标记:
uart0 { compatible = "vendor,uart"; u-boot,dm-pre-reloc; };或者在该驱动的U_BOOT_DRIVER里设置DM_FLAG_PRE_RELOC标志。这个机制保证了早期DM是一个精简、可靠、只包含必要设备的“半成品”。
2.3 重定位后board_init_r要收拾什么
进入board_init_r的时候,代码已经跑在RAM里,gd重新定位到新地址,malloc堆也已经初始化好。这意味着DM有了足够的内存环境来创建设备对象、分配私有数据。此时init_sequence_r里的initr_dm正式接管,它会调用dm_init_and_scan(false),参数为false,也就是不限制pre-reloc,要把设备树里所有能绑的设备全绑一遍。
注意,这不是简单地把board_f阶段再做一遍,而是重新构建完整的DM树。board_f阶段可能已经绑了几个设备,到了board_r阶段,DM会把整棵设备树从头扫描、重新绑定,包括普通驱动和pinctrl、clock这些依赖设备。后续的initr_serial、initr_net、initr_mmc才能通过DM拿到完整的设备列表。
3. board_init_r里那条真正的DM初始化链
3.1 init_sequence_r与initr_dm的位置
board_init_r的核心是initcall_run_list(init_sequence_r),这是一个函数指针数组,按顺序执行每一项。简化后的典型顺序如下:
| 函数 | 作用 |
|---|---|
initr_reloc | 重定位相关收尾 |
initr_caches | 启用缓存 |
initr_malloc | 初始化malloc堆 |
initr_dm | 重新初始化DM设备模型 |
initr_serial | 初始化串口设备 |
initr_net | 初始化网络设备 |
initr_mmc | 初始化MMC设备 |
重点看initr_dm的位置,它排在initr_serial之前。这意味着串口初始化也要走DM,必须先有DM树,串口设备才能被找到并probe。如果DM骨架没搭好,后面所有依赖DM的设备初始化都会失败。
很多实际故障,比如网卡无法识别、eeprom低电压没读到,追根溯源都能追到initr_dm这一步,因为设备压根没被绑定进来。
3.2 dm_init_and_scan干了什么
initr_dm里最终会调用dm_init_and_scan,这个函数是理解整个DM骨架的钥匙。把它拆开看,主要做了两件事:
int dm_init_and_scan(bool pre_reloc_only) { int ret; ret = dm_init(); if (ret) return ret; ret = dm_scan(pre_reloc_only); if (ret) return ret; return 0; }第一步dm_init()负责DM框架自身的基础设施初始化,这一步要做的事情包括:找到根uclass、创建根设备、初始化uclass链表、设置gd->dm_root。可以类比成搭一个“驱动管理系统”的管理员账号和目录结构。
第二步dm_scan(pre_reloc_only)负责扫描设备来源,把设备树节点、platform data或者其他预留设备逐一创建为udevice,并挂到对应的uclass下面。这一步才是真正“填设备”的过程。
3.3 gd->dm_root是怎么长出来的
gd->dm_root是整个DM树的根,很多DM API最终都会回溯到这个根设备。dm_init()内部会从uclass链表中找到UCLASS_ROOT对应的uclass_driver,然后调用device_bind创建一个根设备。
这个根设备很特殊,它没有父设备,也没有普通驱动对应的硬件地址,它只是作为所有设备绑定关系的起点。你可以把它理解为一个虚拟的根节点,下面的simple_bus、pinctrl、serial等设备都挂在它下面。代码里经常出现:
gd->dm_root = root;如果gd->dm_root是NULL,说明dm_init()没有成功执行,或者gd被意外破坏。调试早期启动阶段时,这一步经常是问题焦点。
4. dm_scan:把设备树“翻译”成device列表
4.1 三条数据来源:platdata、FDT、其他
dm_scan并不是只处理设备树。在U-Boot里,设备实例可以来自三条途径:
dm_scan_platdata:处理通过platform data描述的静态设备,常见于SPL阶段,用dtoc工具从设备树生成C数组,省去设备树解析带来的开销。dm_scan_fdt:处理设备树节点,这是主阶段最常用的方式。每一个带compatible属性的节点,都有机会被扫描并绑定。dm_scan_other:处理一些特殊预留设备,例如某些体系结构上固定需要的设备,不通过设备树描述。
对于大多数板卡,CONFIG_OF_CONTROL是打开的,所以dm_scan_fdt是主要路径。理解它处理FDT节点的办法,就理解了设备绑定的大半逻辑。
4.2 driver与compatible的匹配过程
设备树节点扫描到以后,内核要做的事情是通过compatible字符串找到匹配的driver。U-Boot的DM也是类似思路。它遍历driver链接段里的所有driver,把每个driver的of_match表里的compatible字符串和节点里的compatible属性逐一比对。
找到匹配项后,device_bind_common会创建udevice实例,这个实例会同时关联到driver和对应uclass。这里有个容易被忽略的细节:匹配时不仅看of_match,还必须保证driver的id与uclass的id一致,否则绑定会失败。比如你写了一个GPIO驱动,却把id写成UCLASS_SERIAL,那就对不上号了。
匹配失败不一定报错。很多节点找不到对应driver时会被跳过,只有那些带有u-boot,dm-pre-reloc且又匹配失败的节点才可能产生警告。所以“设备没绑上”这个现象,首先要查compatible拼写,再查driver是否真的编进了镜像。
4.3 pre_reloc_only为什么那么重要
pre_reloc_only这个参数贯穿了整个DM扫描逻辑。它的作用是告诉扫描器:现在环境中哪些设备可用。board_f阶段传true,board_r阶段传false,这个区别不能乱。
如果传true,扫描器只接受两种设备:一是在设备树节点里带u-boot,dm-pre-reloc属性的设备,二是driver或uclass_driver本身带DM_FLAG_PRE_RELOC标志的设备。这样做能避免在早期阶段解析到一堆需要时钟、电源、复杂依赖的设备,从而引发潜在崩坏。
如果传false,扫描器就不再过滤pre-reloc标志,把设备树里所有可识别的节点都绑一遍。这也是为什么很多设备树节点在前半程看不到,到board_init_r以后突然出现在dm tree列表里的原因。
4.4 probe触发与依赖关系
扫描绑定和设备probe是两回事。设备被绑定到DM树,不代表它的probe函数立刻会被调用。probe的触发时机通常有几个:显式调用uclass_get_device、父设备probe时自动probe子设备、其他设备通过依赖关系拉起来。
device_probe的核心逻辑是:先确保父设备已经probe成功,再调用driver的of_to_platdata把设备树里的寄存器、中断、属性解析成私有数据,再分配私有数据空间,最后调用driver的probe函数。父先子后这个规则非常关键,比如GPIO控制器没probe成功前,依赖它的LED设备是不可能先probe的。
如果某个设备的依赖暂时不满足,现代U-Boot还支持返回-EPROBE_DEFER延迟probe,等依赖设备就绪后再重新尝试。这个机制让驱动之间的耦合度大大降低。
5. 用一块GPIO驱动看骨架怎么挂上去
5.1 设备树侧的准备
理论讲多了,来点实操。假设板子上有一颗自定义GPIO控制器,设备树节点大概长这样:
gpio0: gpio@ffd00000 { compatible = "vendor,my-gpio"; reg = <0xffd00000 0x100>; gpio-controller; #gpio-cells = <2>; };设备树编译后,U-Boot在扫描FDT时会读取这个节点,拿到compatible、reg等属性。接下来要做的就是在驱动侧提供对应的匹配表和驱动结构体。
5.2 uclass_driver注册
如果这个控制器属于GPIO类别,需要注册对应uclass的类驱动。多数情况下GPIO的uclass已经存在,但如果你想自定义一个新的设备类别,就得自己写一个UCLASS_DRIVER:
UCLASS_DRIVER(my_gpio) = { .id = UCLASS_GPIO, .name = "my_gpio", };这里.id决定了这个类驱动绑定到哪个uclass。如果用的是已有UCLASS_GPIO,要确认U-Boot自身的gpio uclass已经注册,否则会出现uclass找不到的问题。
5.3 driver注册
真正的驱动部分靠U_BOOT_DRIVER宏注册:
static const struct udevice_id my_gpio_match[] = { { .compatible = "vendor,my-gpio", .data = 0 }, { } }; static int my_gpio_probe(struct udevice *dev) { /* 从设备树解析reg,ioremap地址,初始化控制器 */ return 0; } U_BOOT_DRIVER(my_gpio) = { .name = "my_gpio", .id = UCLASS_GPIO, .of_match = my_gpio_match, .probe = my_gpio_probe, .ops = &my_gpio_ops, };.of_match是设备树匹配表,.id必须和uclass id对应,.ops是这套驱动对外提供的操作函数集。这里最容易犯的错就是compatible写错一个字符,或者忘配CONFIG_DM_GPIO,导致驱动链接段没进镜像,扫描器永远找不到它。
5.4 从绑定到probe的调用链
当dm_scan_fdt遍历到gpio@ffd00000时,它会根据compatible找到my_gpiodriver,然后创建udevice并挂到UCLASS_GPIO的链表中。这个阶段只完成绑定,没做硬件操作。
之后当某个模块调用uclass_get_device(UCLASS_GPIO, 0, &dev)时,DM才会触发该设备的probe流程。此时parent设备先probe,然后of_to_platdata解析reg属性,最后my_gpio_probe执行,控制器寄存器才真正被初始化。
如果你在U-Boot命令行执行dm tree,会看到一棵从root到gpio@ffd00000的设备树,设备状态为“probe OK”之类的标记。如果状态还是“未probe”,说明它没被真正使用过,或者被依赖关系卡住了。
6. 实战排查:骨架搭不起来的几个典型症状
6.1 设备树节点没被绑定
这是最常见的问题,现象是dm tree里看不到节点,或者代码里uclass_get_device返回错误。排查顺序建议如下:
- 确认设备树里compatible和驱动的
of_match完全一致,包括大小写。 - 确认
CONFIG_OF_CONTROL已经开启,U-Boot能正确解析到你的dtb。 - 确认驱动宏编译进了镜像。
U_BOOT_DRIVER宏依赖链接器段,如果裁剪配置把段丢了,驱动就不会参与匹配。 - 如果问题发生在早期阶段,检查设备树节点是否带
u-boot,dm-pre-reloc,没有就被跳过了。
很多PHY识别失败、eeprom不识别,本质都是这里。先跑一次dm tree,对比有设备和无设备的差异,很快能定位。
6.2 driver probe失败
设备被绑定了,但probe回调里没走通。这种情况通常伴随返回错误码。device_probe失败时日志里会体现错误码,比如-22对应EINVAL,说明of_to_platdata解析参数时出了问题;-19对应ENODEV,说明硬件寄存器映射失败。
这时候建议在probe开头加打印,确认走到哪一步崩了。很多probe失败不是驱动本身的问题,而是依赖的clk、reset没有就绪。你可以在probe里通过clk_get_by_index获取时钟,通过reset_get_by_index获取复位,任何一个返回错误都会导致整个probe失败。这种问题需要从依赖链上排查,而不是盯着驱动代码硬看。
6.3 uclass_get返回NULL
调用uclass_get或者uclass_get_device时,如果返回-ENODEV或者得到的指针是NULL,说明这个uclass下面根本没有设备。原因一般是两个:设备没有绑定到这个uclass,或者uclass_driver没有注册。
比如你定义了UCLASS_MY_DEV,但驱动里的.id还是UCLASS_GPIO,那uclass_get(UCLASS_MY_DEV)自然找不到。解决方法是打印所有uclass列表,检查是否存在你期望的uclass。
6.4 驱动匹配成功但probe没被调用
还有一种情况很迷惑:设备在dm tree里能看到,但probe一直没触发。这其实是正常的,因为DM采用“按需probe”,设备被绑定不等于被probe。只有当代码显式获取设备,或者父设备probe时拉起了子设备,probe才会执行。
如果你希望启动阶段就确保某个设备probe过,可以在板级初始化里显式调用uclass_get_device,或者使用dm_scan_fdt之后调用device_probe。不要期望扫描绑定阶段就把所有设备全probe一遍,那是U-Boot的设计选择。
6.5 调试手段与日志开关
调试DM最直接的工具是命令行里的dm tree和dm uclass,前者输出设备树状态,后者输出uclass列表。代码层面,drivers/core/目录下的debug()宏可以通过定义DEBUG开启,如果使用新的log系统,还可以设置CONFIG_LOG并调整对应category的日志级别。
我自己的习惯是,在任何新板子上先把dm tree拍个快照,再对照初始化链查缺失项。这个习惯帮我在PHY识别失败、MMC探测超时这类问题上节省了大量时间。
最后分享一点个人感受:U-Boot的DM骨架其实不复杂,核心就是“根设备-设备树扫描-驱动匹配-按需probe”这条链。别一开始就钻进device.c的细节里,先把board_init_r这条主路上的几个函数吃透,再回头调驱动,思路会清晰很多。如果你手头有开发板,建议现在就编译一版带dm tree命令的固件,跑一下,亲眼看看根设备下面挂着哪些东西,比你读十个文档都管用。