☰
U-Boot设备模型初始化流程解析:从dm_init到dm_probe_devices
2026/10/1 1:52:53 网站建设 项目流程

1. 先从为什么要有设备模型说起

如果你写过几年U-Boot驱动,多半经历过“只要上电打印乱码,就得手动去翻板级头文件”的日子。传统U-Boot里驱动之间没有统一接口,串口驱动要自己在board_init_f里手动初始化,GPIO、I2C、SPI、MMC各玩各的,换一颗SoC,板级代码就要重写一遍。后来社区引入了设备模型(Driver Model,以下都叫DM),把“驱动”和“设备”和“设备树”三者绑定在一起,board_init_r就是DM骨架搭起来的核心现场。这篇博文就是一步步拆解这个骨架是怎么从零“搭”出来的,适合正在啃U-Boot源码、被dm tree这命令搞懵、或者想在U-Boot里加自己外设驱动的开发者看。

先说结论:整个DM框架的初始化主线,集中在common/board_r.c的init_sequence_r里,通过initr_dm->dm_init_and_scan->dm_init/dm_scan_platdata/dm_scan_fdt/dm_scan_other,再到initr_dm_devices->dm_probe_devices,一套组合拳打下来,设备树的节点变成了udevice,驱动从链接段里被找到,uclass这个“设备类班长”也建好了。手里有这把流程地图,再翻代码就不会迷路。

2. init_sequence_r里的DM入口:initr_dm和它的小伙伴们

2.1 initcall机制先扫一眼

U-Boot的初始化分两大阶段:board_init_f负责早期CPU、时钟、DDR这类“没内存不行”的东西,board_init_r则是在代码重定位到DRAM之后,把整个板级运行环境彻底铺开。

board_init_r靠一张函数指针表init_sequence_r逐个执行初始化项,每个initcall返回0表示成功,非0就挂掉。这张表里,跟DM直接相关的通常是initr_of_live、initr_dm、initr_dm_devices三项,它们的顺序非常重要。initr_of_live负责在开启CONFIG_OF_LIVE时把扁平设备树转换成live tree,这一步必须在initr_dm之前,因为后面扫描设备树时要用到活树节点访问接口。

2.2 initr_dm本身做了什么

initr_dm在绝大多数主流版本里长这样:

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

dm_init_and_scan是DM初始化的总入口,参数pre_reloc_only表示“是不是只要pre-reloc设备”。在board_init_r这里,内存已经就位,所有设备都可以被绑定,所以传false。如果在SPL或早期资源受限的环境,才会用true只绑定带DM_FLAG_PRE_RELOC标志或设备树里有u-boot,dm-pre-reloc属性的设备,避免在堆栈只有几KB的时候绑一大堆设备把内存吃光。

2.3 dm_init_and_scan四步走

dm_init_and_scan内部非常干净,就是对四个子功能依次调用:

int dm_init_and_scan(bool pre_reloc_only) { int ret; ret = dm_init(); if (ret) return ret; ret = dm_scan_platdata(pre_reloc_only); if (ret) return ret; ret = dm_scan_fdt(pre_reloc_only); if (ret) return ret; return dm_scan_other(pre_reloc_only); }

dm_init建立整个DM框架的根,dm_scan_platdata扫描编译期静态平台数据,dm_scan_fdt扫描设备树(这是绝大部分设备来源),dm_scan_other留给板级弱函数补充特殊设备。四步走完,设备只是“绑定”了,还没“启动”,真正的启动交给后文要说的dm_probe_devices。

3. dm_init:root设备与uclass总表的建立

3.1 一切从root_driver开始

dm_init的核心动作是绑定一个叫root_driver的特殊驱动,生成整个设备树的根设备。这个驱动在drivers/core/root.c里定义:

U_BOOT_DRIVER(root_driver) = { .name = "root_driver", .id = UCLASS_ROOT, };

绑定的调用走到device_bind_common(NULL, &root_driver, "root_driver", ...),注意第一个parent参数传的是NULL,因为根设备没有爹。绑定完成后,gd->dm_root就指向这个root设备,它就是之后所有设备的祖先节点。

代码大致逻辑如下:

int dm_init(void) { int ret; ret = device_bind_common(NULL, &root_driver, "root_driver", NULL, NULL, 0, &DM_ROOT_NON_CONST); if (ret) return ret; INIT_LIST_HEAD(&gd->uclass_root); return 0; }

gd->uclass_root是uclass总链表头,以后每个uclass都会挂在这条链表上。从这一刻起,DM框架有了“根”,有了“类目录”,后面接进来的设备才知道往哪里挂。

3.2 uclass为什么是惰性创建的

uclass是设备类的抽象,比如UCLASS_GPIO管所有GPIO控制器,UCLASS_SERIAL管所有串口。很多人以为dm_init会把所有uclass一口气建出来,其实不是。uclass是在绑定某个设备、发现它所属的uclass还不存在时,才通过uclass_get动态创建。

uclass的创建逻辑在drivers/core/uclass.c,大致是遍历gd->uclass_root链表,按id做匹配:

int uclass_get(enum uclass_id id, struct uclass **ucp) { struct uclass *uc; int ret; list_for_each_entry(uc, gd->uclass_root, node) { if (uc->uc_drv->id == id) ... } ret = uclass_add(id, &uc); ... }

这种惰性方式最直接的好处是避免浪费内存。嵌入式设备上资源紧张,如果一上来把所有uclass driver都实例化成struct uclass,光链表头和私有数据就够喝一壶。等用到再创建,框架轻盈,启动快。

3.3 uclass_driver寄存器里藏着什么

uclass driver通过UCLASS_DRIVER宏注册到链接段.u_boot_list_2_uclass_driver_2,类似这样:

UCLASS_DRIVER(misc) = { .id = UCLASS_MISC, .name = "misc", .post_bind = misc_post_bind, };

比较重要的是post_bind和post_probe回调。post_bind在这个类下每个设备绑定完成后触发,适合做统一初始化;post_probe在这个类下每个设备probe完成后触发。很多人写驱动只盯着driver的probe,却忽略了uclass层面的回调,等遇到“所有GPIO设备都要在绑定后设一个公共属性”这种需求时,只能满地找方案。

4. dm_scan_platdata:编译期静态设备怎么上车

4.1 U_BOOT_DRVINFO的来龙去脉

在没有设备树或者设备树还没解析的年代,设备信息靠编译期静态结构体描述。这类结构体用U_BOOT_DRVINFO宏注册:

U_BOOT_DRVINFO(usb_host0) = { .name = "usb_host0", .driver = &ehci_mxc_driver, .platdata = &usb_host0_plat, };

dm_scan_platdata做的事情就是从链接段起始遍历这些struct driver_info,逐个调用device_bind_common把设备绑定到root下,同时把编译期的platdata指给设备。

for (entry = ll_entry_start(struct driver_info, driver_info); entry < ll_entry_end(struct driver_info, driver_info); entry++) { if (pre_reloc_only && !(entry->flags & DM_FLAG_PRE_RELOC)) continue; ret = device_bind_common(NULL, entry->driver, entry->name, entry->platdata, 0, entry->of_offset, &dev); }

entry->flags里的DM_FLAG_PRE_RELOC是判断是否提前绑定的关键。现代板上设备树已经普及,这块用得少了,但兼容老平台时你还会看到它。

4.2 静态platdata与设备树platdata的冲突

device_bind_common里会先分配dev->platdata,并将编译期提供的platdata拷贝进去。如果同一驱动同时支持静态平台数据又支持设备树,代码里通常用ofdata_to_platdata回调在probe前把设备树属性填进platdata。此时静态数据会被设备树覆盖,所以别指望两个来源能同时生效。

我遇到过一种情况:从老版本板级代码移植驱动,原项目用U_BOOT_DRVINFO注册,新平台设备树里也配了相同节点,结果设备树属性优先级高,原来写在platdata里的参数全部失效。排查半天后直接删掉静态注册,统一走设备树,世界才清净。做平台迁移时,建议先查清楚驱动来源,不要混着喂数据。

5. dm_scan_fdt:设备树节点到udevice的完整旅程

5.1 从root节点开始递归

dm_scan_fdt是四个步骤里最硬核的一步,几乎所有设备都在这里“诞生”。它调用dm_scan_fdt_node从gd->dm_root出发,以递归方式遍历设备树每个子节点:

int dm_scan_fdt_node(struct udevice *parent, const void *blob, int offset, bool pre_reloc_only) { for (offset = fdt_first_subnode(blob, offset); offset > 0; offset = fdt_next_subnode(blob, offset)) { const char *name = fdt_get_name(blob, offset, NULL); if (pre_reloc_only && !pre_reloc_only_ok(....)) continue; ret = lists_bind_fdt(parent, blob, offset, NULL, NULL); if (ret) continue; ret = dm_scan_fdt_node(parent, blob, offset, pre_reloc_only); ... } }

注意它不关心父节点是不是“总线”,连根节点下面不是simple-bus的子节点也会被尝试绑定。这是U-Boot DM和Linux驱动模型的一个明显差异——Linux要求父节点是能识别的bus,U-Boot则暴力得多,挨个节点都要去查有没有匹配的driver。节点匹配不到driver也不会导致整体失败,会被跳过继续往下。

5.2 compatible匹配driver的全过程

lists_bind_fdt是核心匹配函数,它遍历链接段里所有U_BOOT_DRIVER注册的driver,用fdt_node_match拿节点的compatible字符串和driver的of_match表逐条比对:

for (entry = driver; entry < driver + n_ents; entry++) { if (fdt_node_match(blob, offset, entry->of_match)) return device_bind_with_driver_data(parent, entry, blob, offset, devp); }

driver最常见的写法是:

static const struct udevice_id my_demo_ids[] = { { .compatible = "my-demo" }, { } }; U_BOOT_DRIVER(my_demo) = { .name = "my_demo", .id = UCLASS_MISC, .of_match = my_demo_ids, .probe = my_demo_probe, };

设备树里这么写:

my-demo { compatible = "my-demo"; status = "okay"; };

匹配时注意几点。第一,设备节点的compatible领先项一定要尽可能具体,driver的of_match表顺序也有讲究,U-Boot按driver注册顺序来查,不是按设备树先来后到。第二,status = "disabled"的节点会被跳过,这是很多设备“明明写了节点却不生效”的头号原因。第三,u-boot,dm-pre-reloc属性是早期绑定设备的关键,它和driver的DM_FLAG_PRE_RELOC标志是等效的。

调试时可以临时把status改为"okay",或者直接在.config里打开CONFIG_DEBUG_DRIVER,用debug日志看匹配过程,远比盲改设备树高效。

5.3 uclass_get完成第一次握手

匹配到driver之后,device_bind_with_driver_data会先通过uclass_get(drv->id, &uc)拿到这个设备所属的uclass。如果uclass尚不存在,这里会触发uclass_add创建。整个过程可以概括为:

  1. 节点compatible命中driver;
  2. 由driver的id找到或创建uclass;
  3. 分配struct udevice并初始化;
  4. 把设备挂进dev->parent的child_head链表;
  5. 把设备挂进uclass的dev_head链表;
  6. 调用uclass的post_bind回调。

这一步相当于给新生儿上了户口:既有爹(parent),也报了班(uclass)。udevice、driver、uclass三者在这个节点正式握手成功。

5.4 device_bind_common内部的分配逻辑

device_bind_common里有个细节值得多说两句。设备的私有数据和平台数据大小都来自driver和uclass driver里声明的priv_auto_alloc_size和platdata_auto_alloc_size:

struct udevice { const struct driver *driver; struct uclass *uclass; void *priv; void *platdata; ... struct udevice *parent; struct list_head child_head; struct list_head sibling_node; u32 flags; };

priv是驱动自己的运行期数据结构,platdata是从设备树或静态注册表填充的配置数据。分配时device_bind_common会一次性把这两块结合设备本身空间从内存池取出来。很多新手写驱动不设置priv_auto_alloc_size,然后在probe里疯狂给priv分配内存,不仅慢,还容易漏释放。直接在声明的size里定义好结构体,probe时赋值即可。

还有一点,device_bind_common里会给设备分配seq序号。如果一个uclass下面多个设备,seq可以通过设备树reg或alias指定,否则动态分配。动态分配的seq不保证稳定,跨板子换节点顺序就会变。

6. dm_scan_other与pre-reloc设备的特殊处理

6.1 弱函数留给板级后门

dm_scan_other是个__weak函数,默认返回0。它的存在意义是给某些特殊平台一个“手动补设备”的入口。比如你的板子有一个设备既不在设备树里,也没有静态driver_info,就可以在这里直接device_bind挂上去。

这种需求在量产板里偶尔会出现,比如开机后根据某个拨码开关动态决定要不要挂载一个调试设备。我不建议把大量业务写进dm_scan_other,因为它在initr_dm阶段执行,此时很多外设还没probe,依赖关系很容易搞乱。最好只做最简单的绑定,真正的初始化交给probe。

6.2 设备数里的pre-reloc门神

前文反复提到pre_reloc_only,这里展开讲。在dm_scan_fdt_node里,如果参数为true,每个节点都要过pre_reloc_only_ok这个关卡:

static bool pre_reloc_only_ok(const struct udevice *dev) { if (dev->driver && (dev->driver->flags & DM_FLAG_PRE_RELOC)) return true; return ofnode_pre_reloc(dev->node); }

ofnode_pre_reloc主要检查设备树节点有没有u-boot,dm-pre-reloc属性。这条属性在早期SPL或者DDR初始化前段特别重要,比如串口要在board_init_f阶段就输出日志,就必须给串口节点加上这个属性。到了board_init_r,扫全量设备,它又被忽略,属于典型的“早期专用通道”。

7. dm_probe_devices:让骨架长出肌肉

7.1 bind和probe别搞混

绑定(bind)只是把设备树节点变成struct udevice登记在册,驱动真正开始工作要等探测(probe)。我常用一个生活类比:bind是出生登记,probe是开始上班。登记只占内存,上班才会去操作硬件。很多外设调试时最典型的错误就是设备已经在dm tree里出现,但probe没有调用,于是寄存器不工作——因为驱动根本没“醒来”。

7.2 probe的触发入口

board_init_r里initr_dm_devices调用dm_probe_devices,遍历UCLASS_ROOT下的所有直接子设备,逐个device_probe:

int dm_probe_devices(void) { struct udevice *dev; int ret; for (uclass_find_first_device(UCLASS_ROOT, &dev); dev; uclass_find_next_device(&dev)) { if (dev->flags & DM_FLAG_PROBE_ON_STOP) continue; ret = device_probe(dev); if (ret) return ret; } return 0; }

注意device_probe是递归的:在probe某个设备前,会先probe它的父设备。所以虽然dm_probe_devices只遍历root的直接子设备,最终整棵树都会被递归probe到位。

7.3 device_probe内部的关键顺序

int device_probe(struct udevice *dev) { ... if (dev->flags & DM_FLAG_ACTIVATED) return 0; /* 先probe父设备 */ if (dev->parent && !(dev->parent->flags & DM_FLAG_ACTIVATED)) { ret = device_probe(dev->parent); ... } /* uclass的pre_probe,driver的ofdata_to_platdata,然后driver->probe */ if (dev->driver->ofdata_to_platdata) { ret = dev->driver->ofdata_to_platdata(dev); ... } if (dev->driver->probe) { ret = dev->driver->probe(dev); ... } dev->flags |= DM_FLAG_ACTIVATED; /* uclass的post_probe */ ... }

ofdata_to_platdata的目的是把设备树里的reg、clocks、gpios等属性解析到platdata中,这样probe里不直接碰FDT接口,代码更干净。有一种不太规范但常见的写法是跳过ofdata_to_platdata,全部在probe里用dev_read_u32一类接口读设备树,能用,但不利于数据与访问逻辑分离。我自己的习惯是:凡是设备树属性超过3个,就老老实实写ofdata_to_platdata。

7.4 probe失败的处理

device_probe返回非0会导致dm_probe_devices整体失败,board_init_r就会停在这里。常见的probe失败原因包括:依赖的父设备probe失败、时钟或者电源节点还没probe到位、返回值不是EPROBE_DEFER而是普通错误。

U-Boot从某个版本起也支持deferred probe机制,当依赖未满足时,驱动probe可以返回-EPROBE_DEFER,设备会被放入等待队列,等后续依赖设备probe完成后重试。不过相比Linux,U-Boot的deferred机制引入较晚,各版本行为差异不小,别太依赖它。设计驱动时尽量让probe顺序自洽,或者用uclass的post_probe统一做二次初始化,比赌deferred机制靠谱。

8. 实战调试:用dm tree解剖骨架

8.1 dm tree/dm uclass命令怎么读

U-Boot命令行下最常用的三条DM调试命令是dm tree、dm uclass、dm devres。dm tree输出当前所有已绑定设备的树状关系,每一行包含序号 名字 父设备 驱动名 uclass名 是否激活:

Index Name Parent Driver uclass Status 0 root_driver root_driver root_driver root OK 1 serial@... root_driver serial_s5p serial OK 2 gpio@... root_driver gpio_s5p gpio OK 3 gpio@... gpio@... gpio_s5p gpio OK

Status列是OK表示probe成功,Pending表示已绑定但没probe或probe失败,Disabled表示设备树status不是okay。这套输出基本能一眼定位问题。

8.2 常见问题速查表

现象可能原因排查方向
dm tree里看不到设备设备树节点没写compatible,或者driver没注册检查U_BOOT_DRIVER和of_match表
看到设备但Status是Pending设备绑定成功但probe失败或被跳过打开CONFIG_DEBUG_DRIVER看probe日志
串口在初始化早期没输出设备没加u-boot,dm-pre-reloc属性节点里补属性,或给driver加DM_FLAG_PRE_RELOC
设备有compatible却不绑定节点里status = "disabled"改状态,或检查fdtdec_node_is_enabled逻辑
probe里读设备树属性为空ofdata_to_platdata没执行或顺序不对在probe前先调用dev_read_u32验证属性
多个同类设备seq混乱没配alias或者reg不可靠在设备树里用aliases固定序号

8.3 一个完整的驱动挂载流程样例

假设你要在U-Boot里加一个简单spi设备的驱动。前面的board_init_r骨架搭好之后,真正要做的就是四件事。

第一,在驱动文件里声明uclass和driver:

UCLASS_DRIVER(my_spi_slave) = { .id = UCLASS_MISC, .name = "my_spi_slave", }; U_BOOT_DRIVER(my_spi_slave) = { .name = "my_spi_slave", .id = UCLASS_MISC, .of_match = my_spi_slave_ids, .probe = my_spi_slave_probe, };

第二,在设备树里加上节点并确认status正常:

my_spi_slave { compatible = "my-spi-slave"; status = "okay"; reg = <0x1>; };

第三,编译烧录启动,输入dm tree看到设备出现在UCLASS_MISC下。

第四,在驱动里设断点或者加debug打印确认probe被调到。

这一步跑通,基本上就说明initr_dm到dm_probe_devices的整条链路衔接正常,后续再完善的业务逻辑都有可靠的执行环境。

8.4 调整initcall顺序的坑

我自己踩过一个非常典型的坑:为了在早期初始化一个自定义外设,把initr_dm往init_sequence_r前面挪了挪,结果设备树扫描时malloc池还没完全就绪,device_bind_common分配内存直接崩。后来才意识到,initr_dm的位置不是随便摆的,它前面必须已经有堆内存和重定位环境可用。

如果你确实需要某个外设特别早启动,正路是给设备加u-boot,dm-pre-reloc属性,把它放进board_init_f阶段的早期DM初始化里,而不是去挪board_init_r里整个DM的启动顺序。破坏initcall顺序,轻则打印乱码,重则启动挂死,还极难排查。

9. 我对DM骨架的理解

整个board_init_r里的DM初始化,本质就两个阶段:绑定与探测。绑定阶段从root设备出发,先扫编译期平台数据,再扫设备树,把节点转成udevice,同时把uclass建好;探测阶段把所有已绑定设备递归probe起来。两者之间靠uclass这个“班长”协调,靠DM_FLAG_PRE_RELOC这类标志分层。

想彻底吃透这套骨架,与其硬啃源码,不如在QEMU里模拟一块开发板,开启CONFIG_DEBUG_DRIVER后盯着日志看dm_scan_fdt和dm_probe_devices的实际执行顺序。我当初就是靠这种方式理清了parent、uclass、driver三者的关系。之后再遇到“某设备不工作”,第一反应不再是翻原理图乱猜,而是先跑一遍dm tree看它到底有没有出生、有没有上岗——这一步能过滤掉一半的问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询