尧图精选

U-Boot设备模型速通:从board_init_r到驱动probe的完整链路

🕒 发布时间:2026/10/1 8:58:55 📁 来源:尧图网络
干了几年嵌入式BSP我发现自己对U-Boot的认知经历过三个阶段先是照着文档把各种命令跑起来接着能改驱动、调设备树最后才敢说自己真正看懂了u-boot设备模型通常简称dm。如果你正处于第二个阶段想搞明白board_init_r里驱动到底是怎么被组织起来的那这篇内容就是给你的。标题里“屠龙刀在手”是个玩笑话但“速通设备模型”是真的。U-Boot这套dm框架说复杂也复杂说简单也简单——本质上就是几张链表、几组宏、一段链接脚本外加一个统一的probe流程。只要把这几样东西串起来从u-boot启动到驱动被真正使用整个过程就是一条清晰的链路。本文不讨论应用层写法直接扎到board_init_r这条主线上看一个驱动节点是如何一步步变成udevice、被挂进uclass、最后被probe起来的。1. 为什么要盯住board_init_r驱动模型这副骨架的“施工节点”1.1 DM出现之前的世界乱在哪早期U-Boot里每个驱动几乎都是“自扫门前雪”串口驱动维护自己的全局数组网卡驱动自己注册一个eth_deviceGPIO控制器又走另一套私有接口。上层的命令代码要操作硬件就得到处#ifdef或者靠一堆松散的函数指针约定来“勾搭”。新板卡bring-up时最烦的就是明明硬件都在驱动却因为错误的条件编译没有编进来或者两个驱动用了同一个全局变量名导致链接冲突。DM要解决的就是把“设备”和“驱动”这两个概念彻底标准化。所有硬件外设在模型里都叫udevice所有驱动代码都描述为struct driver同类设备再归入uclass比如UCLASS_GPIO、UCLASS_SERIAL。从此谁也不用另起炉灶维护私有链表设备树里写清楚驱动注册好剩下的都由框架统一调度。1.2 board_init_f和board_init_r怎么分工如果你打开common/board_f.c和common/board_r.c会发现两个巨大的函数指针数组init_sequence_f和init_sequence_r。U-Boot启动时先跑board_init_f它干的是“搬家和基础建设”——重定位前把代码搬去DDR、初始化早期malloc、建立早期串口。这时候内存环境还很局促所以DM只能做“限量版”用dm_init_and_scan(true)只扫描那些明确带了u-boot,dm-pre-reloc属性的节点。真正的“全家福”扫描发生在board_init_r。此时代码已经重定位到高地址堆想用多少用多少设备树里所有节点都可以被正经地绑定一遍。所以题主问“board_init_r里的dm驱动骨架怎么搭起来的”本质就是在问这段完整扫描和绑定流程。1.3 骨架在board_init_r中要完成的最小工作集在common/board_r.c的init_sequence_r里和dm相关的入口通常只有一两个其中最关键的就是initr_dm。别小看这个“小函数”它背后要完成四件事把dm框架本身初始化干净确保gd-dm_root存在递归扫描设备树根节点下的所有子节点对每个节点做compatible匹配找到对应的struct driver为匹配成功的节点创建udevice并挂到父设备、uclass两条链表上。这四件事做完U-Boot的驱动世界就有了完整的“骨架”设备树里每个外设节点都在内存里有了对应的udevice实例等着被使用方通过uclass_get_device或dm_get_device这类接口真正“唤醒”。骨架搭好不代表驱动运行——真正运行是probe的事后面会专门展开。2. 沿initcall链找到DM入口initr_dm和它背后那三次调用2.1 init_sequence_r机制简述init_sequence_r是一个init_fnc_t类型的数组每个元素都是“返回int、入参为空”的函数。board_init_r遍历这个数组挨个调用遇到返回值非0就直接停下来报错。这种initcall机制的好处是简单粗暴每个初始化步骤做成一个函数顺序写在数组里增删都很直观。在这个数组里你大概率能找到这样两行static init_fnc_t init_sequence_r[] { ... initr_dm, initr_dm_devices, ... };不同版本细节有差异有些版本把设备树扫描和平台数据扫描都塞进initr_dm有些版本还单独保留initr_dm_devices做静态设备非设备树设备扫描。你不需要死记版本差异只需要知道board_init_r的DM初始化核心就是“initr_dm - dm_init_and_scan(false)”这一条线。2.2 dm_init_and_scan的三个动作dm_init_and_scan在drivers/core/root.c里定义逻辑特别直白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 dm_scan_platdata(pre_reloc_only); }第一个动作dm_init负责把dm框架的基础设施弄好初始化uclass链表、创建root设备root_driver挂在UCLASS_ROOT下并把root设备指针存进gd-dm_root。如果board_init_f阶段已经初始化过dm_init内部会检查到gd-dm_root非空直接复用不会重复创建。第二个动作dm_scan是设备树扫描的主入口。它会从gd-dm_root出发用fdt_first_subnode、fdt_next_subnode这组函数遍历设备树。第三个动作dm_scan_platdata处理那些不走设备树、靠U_BOOT_DEVICE静态描述的平台数据设备。大多数时候你的板卡用的是设备树所以前两个动作是主角。2.3 pre_reloc_only参数的前世今生刚接触dm的人十有八九会在这个bool参数上栽跟头。true表示“只扫描带u-boot,dm-pre-reloc属性的节点”false表示“有多少扫多少”。在board_init_f里调用的是dm_init_and_scan(true)因为那时候代码还在低地址执行malloc也很小不可能给每个设备都分配内存。所以设备树里只有那些“早点起来才能打印、才能配时钟”的关键节点才打上u-boot,dm-pre-reloc标记老内核里叫u-boot,dm-pre-reloc新一点还会有bootph-pre-ram这类新写法。到了board_init_r调用的是dm_init_and_scan(false)就是不筛了所有有驱动匹配的节点全部绑定。这一点一定要记牢如果你在设备树里新增一个节点又在menuconfig里把对应驱动编进去了结果启动后dm tree里看不到它第一反应就该去检查是不是把参数或者属性搞错了。3. 三大数据结构udevice、driver、uclass是怎么咬合在一起的3.1 三张表的关系dm框架里存在三张平行的链表struct driver定义“怎么驱动”每个驱动代码实例对应一个条目来自U_BOOT_DRIVER宏struct udevice描述“设备状态”一个设备树节点对应一个实例由device_bind_common创建struct uclass定义“设备类别”同类设备共享一套操作方法来自UCLASS_DRIVER宏。udevice同时指向自己的driver和uclass所以从任意一个设备实例出发都能找到它属于什么驱动、什么类别。而uclass内部维护dev_head链表挂载所有属于这个类别的udevice。3.2 关键字段逐一拆解先看struct driver位于include/dm/device.h挑核心的列字段作用name驱动名字用于链表查找iduclass编号比如UCLASS_GPIOof_matchstruct udevice_id数组存compatible匹配表bind绑定阶段回调创建父/子关系时调ofdata_to_platdata把设备树属性转换成平台数据probe启动设备时调用真正的初始化逻辑platdata_auto_alloc_size告诉框架给dev-platdata分配多大内存再看struct udevice字段作用parent父设备指针sibling_node挂在父设备child_head链表上的节点uclass_node挂在uclass的dev_head链表上的节点uclass所属uclass指针driver所属驱动指针platdata平台数据由ofdata_to_platdata填充priv私有数据供驱动运行时使用seq/req_seq设备序号用于uclass_get_device按序号查找最后是struct uclass它比较轻量字段作用uc_drv指向uclass_driverdev_head该uclass下所有设备链表头privuclass私有数据3.3 用“线路、司机、车辆”类比uclass像公交线路GPIO线、串口线driver像公交车司机某个具体型号的驱动代码udevice像一辆正在运营的车某个具体的GPIO控制器实例。线路上挂了哪些车看dev_head每辆车是哪位司机开、属于哪条线看udevice里存的driver和uclass两个指针。框架要做的事就是把“线路表”“司机表”“车辆表”三个账本维护好谁来了都能查。4. U_BOOT_DRIVER宏与链接表驱动注册登记的底层魔术4.1 宏展开与section写过U-Boot驱动的人对U_BOOT_DRIVER肯定不陌生看起来像魔法其实它只是一个往特殊section里塞结构体的宏#define U_BOOT_DRIVER(__name) \ ll_entry_declare(struct driver, __name, driver)而ll_entry_declare的定义在include/linker_lists.h是#define ll_entry_declare(_type, _name, _list) \ _type _u_boot_list_2_##_list##_2_##_name __aligned(4) \ __attribute__((unused, section(.u_boot_list_2_#_list_2_#_name)))展开后你的驱动结构体变量名变成了_u_boot_list_2_driver_2_demo_led并被放到名为.u_boot_list_2_driver_2_demo_led的section里。也就是说你用U_BOOT_DRIVER(demo_led)定义的那个struct driver并没有进普通的.data段而是被单独扔进了一个以u_boot_list为前缀的专用区域。4.2 链接脚本怎么组织表项光有section还不够链接脚本得把这些散落的结构体按顺序归拢成一张连续的“表”。在u-boot.lds里能看到类似这样的东西.u_boot_list : { ... KEEP(*(SORT_BY_NAME(.u_boot_list_2_driver_1))); ... KEEP(*(SORT_BY_NAME(.u_boot_list_2_driver_3))); ... }注意这里有driver_1、driver_2、driver_3之类多个小节编译时会按名字排序。每个U_BOOT_DRIVER条目最终被放进driver_2这一层框架再通过SORT_BY_NAME保证凡是叫driver_*的输入section都能紧凑排列。这就是为什么明明几十个驱动分散在几十个.c文件里链接之后却能形成一个连续数组从ll_entry_start到ll_entry_end扫一遍就能遍历所有驱动。我一直觉得理解这一层才是真正“速通”dm的开始。很多教程只讲宏怎么写不讲链接表怎么组织可一旦你知道了这是链接脚本在背后排好序的数组那后面所有“遍历驱动”的代码都是顺理成章的。4.3 查找driver的两种路径有了连续数组查找驱动就是写个for循环struct driver *lists_driver_lookup_name(const char *name) { struct driver *entry; for (entry ll_entry_start(struct driver, driver); entry ll_entry_end(struct driver, driver); entry) if (strcmp(entry-name, name) 0) return entry; return NULL; }这是“按名字查”。另一种更常用的是“按compatible查”遍历所有struct driver对每个driver的of_match表调用driver_of_match拿fdt_getprop读到的compatible字符串去和表里的compatible字段比对。匹配成功就把driver和匹配到的udevice_id一起返回。设备树扫描走的就是第二种。5. 从设备树节点到udevicedm_scan的完整旅程5.1 递归遍历的逻辑dm_scan最终会走到dm_scan_fdt_node它的核心是一个递归遍历static int dm_scan_fdt_node(struct udevice *parent, const void *blob, int offset, bool pre_reloc_only) { int node, ret; for (node fdt_first_subnode(blob, offset); node 0; node fdt_next_subnode(blob, node)) { ret lists_bind_fdt(parent, blob, node, NULL, pre_reloc_only); if (ret ret ! -ENODEV) return ret; if (!ret) dm_scan_fdt_node(dev, blob, node, pre_reloc_only); } return 0; }注意这里的逻辑先对每个子节点执行lists_bind_fdt如果绑定成功就拿到一个udevice然后对这个udevice所对应的设备树节点继续递归扫描它下面的子节点。如果某个节点没有匹配到任何驱动lists_bind_fdt返回-ENODEV那它的子节点也不会被深入扫描。这解释了U-Boot设备树里一个常见的现象总线控制器节点比如I2C控制器、PCIe控制器必须先有匹配的驱动挂在这条总线上的子设备才可能被扫描绑定。子设备能否“见光”前提是父节点先成功绑定。5.2 lists_bind_fdt的compatible匹配lists_bind_fdt干了这么几件事用fdt_get_name拿节点名字用fdt_getprop读compatible属性调用driver_of_find_matching遍历所有driver拿compatible去和driver的of_match表比对如果找到driver再检查pre_reloc_only约束通过device_bind_common创建并初始化udevice。这里有个容易忽略的点compatible匹配是拿设备树节点的compatible字符串去和每个driver的of_match数组里的compatible字段做字符串比较。比如你设备树里写compatible myvendor,led驱动里of_match就要有{ .compatible myvendor,led }。字符串一个字符都不能差多了空格、大小写不对都会静默匹配失败。5.3 device_bind_common的绑定细节device_bind_common是真正的“造物主”它完成以下步骤用calloc分配struct udevice内存调uclass_find在uclass链表里查找这个驱动对应的uclass没找到就用uclass_add新建填充dev-parent、dev-driver、dev-name、dev-uclass把dev挂入uclass的dev_head链表uclass_bind_device把dev挂入父设备的child_head链表调用driver的bind回调再调用uclass_driver的post_bind回调如果设备树节点带有u-boot,dm-pre-reloc之类的属性还会给dev打上对应标志。看到没有这个时候udevice已经有了但它内部的platdata、priv还是空的probe回调也没被调。这就是“骨架”的含义——先搭结构不急着通电。5.4 为什么绑定不等于探测很多人第一次看dm tree命令时会困惑为什么设备树里明明有某个节点驱动也匹配上了但那一行显示probed状态不是ok而是空或unproved因为U-Boot的DM设计原则是“按需probe”。绑定只是建了关系probe才是实际初始化。如果启动时把设备树里所有设备都probe一遍内存和启动时间都会吃不消很多设备比如某个没被用到的编解码器根本没必要初始化。所以框架默认只绑定等上层代码通过uclass_get_device、dm_get_device、dev_get_parent等方式真正要使用它或者驱动声明了DM_FLAG_PROBE_ON_SCAN标志少数特殊驱动会才会触发probe。这个特点直接影响你的调试思路在dm tree里看到了设备不代表驱动probe已经执行过。如果某个驱动初始化日志没打出来先确认是不是根本没有代码路径去触发它。6. 驱动真正“起电”的瞬间device_probe的调用链6.1 probe前先probe父设备device_probe这个函数堪称整个dm框架的“心脏”。它的第一个关键设计是递归一个设备要probe它的父设备必须已经probe成功。if (dev-parent) { ret device_probe(dev-parent); if (ret) return ret; }这个设计很符合硬件逻辑你要点亮一颗挂在I2C总线上的传感器那你首先得把I2C控制器初始化好你要操作某个GPIO引脚首先要保证GPIO控制器起来了。所以probe顺序天然就是从根设备一路向下层层递进。6.2 ofdata_to_platdata、pre_probe、probe、post_probe的顺序device_probe的内部顺序我建议每个驱动开发者都当成口诀背下来检查dev-probed已经probe过就直接返回递归probe父设备调用drv-ofdata_to_platdata(dev)把设备树里的reg、clock、gpio等属性读出来转存到dev-platdata根据priv_auto_alloc_size给dev-priv分配私有数据内存调用uc-uc_drv-pre_probe(dev)让uclass先做准备调用drv-probe(dev)驱动核心初始化就在这一步probe成功后设置dev-probed 1调用uc-uc_drv-post_probe(dev)uclass收尾。这个顺序解释了为什么驱动里访问dev_get_platdata是安全的——因为框架保证platdata一定在probe之前就准备好了。也解释了为什么你在probe回调里能拿到uclass的上下文——因为uclass的pre_probe已经在前面执行过了。6.3 什么场景会触发probe最典型的触发点struct udevice *dev; int ret uclass_get_device(UCLASS_MISC, 0, dev);uclass_get_device内部会先在uclass的dev_head链表里按序号找到设备然后调用device_probe。如果你不想通过uclass也可以直接拿设备名查int ret uclass_get_device_by_name(UCLASS_MISC, demo-led, dev);或者干脆手动pret device_probe(dev);必须说清楚的是bind阶段绝对不触发ofdata_to_platdata。你如果在sof阶段依赖设备树属性那必须等probe流程跑起来才会填充。我在实际调试中就曾犯过“在bind回调里读设备树属性”的错结果读到的全是默认值排查了半天才反应过来bind和probe不是一回事。7. 实操写一个能被board_init_r扫描绑定的最小驱动7.1 驱动源码纸上谈兵终觉浅我们直接写一个最小驱动。假设我手上有个名为demo,led的假想LED控制器新建文件drivers/misc/demo-led.c#include dm.h #include linux/compat.h #include linux/err.h struct demo_led_priv { bool enabled; }; static int demo_led_ofdata_to_platdata(struct udevice *dev) { struct demo_led_priv *priv dev_get_platdata(dev); priv-enabled dev_read_bool(dev, demo,enabled); return 0; } static int demo_led_probe(struct udevice *dev) { struct demo_led_priv *priv dev_get_platdata(dev); printf(demo_led: %s probed, enabled%d\n, dev-name, priv-enabled); return 0; } static const struct udevice_id demo_led_ids[] { { .compatible demo,led }, { } }; U_BOOT_DRIVER(demo_led) { .name demo_led, .id UCLASS_MISC, .of_match demo_led_ids, .ofdata_to_platdata demo_led_ofdata_to_platdata, .platdata_auto_alloc_size sizeof(struct demo_led_priv), .probe demo_led_probe, };这里把id设成UCLASS_MISC是因为UCLASS_MISC是万金油类不需要额外实现uclass_driver。platdata_auto_alloc_size告诉框架给dev-platdata分配多大空间。7.2 设备树节点在板级dts里加一个节点demo-led { compatible demo,led; demo,enabled; };注意如果你的板卡在board_init_f阶段就要用到这个设备需要加u-boot,dm-pre-reloc新语法是bootph-pre-ramdemo-led { compatible demo,led; demo,enabled; u-boot,dm-pre-reloc; };不加这个属性设备在board_init_f阶段不会绑定但加了之后它会早于大多数设备进入骨架如果依赖的父设备还没起来probe时反而可能报错。所以一般情况下普通demo设备不加pre-reloc属性就好。7.3 编译接入与验证在drivers/misc/Makefile里加一行obj-$(CONFIG_DEMO_LED) demo-led.o在drivers/misc/Kconfig里加config DEMO_LED bool Demo LED driver depends on DM default y然后在menuconfig里打开CONFIG_DEMO_LED重新编译U-Boot烧录启动。启动后会发现在串口里能看到驱动自己打的probe日志不一定。因为demo-led设备虽然会被dm_scan绑定但默认不会被probe。想要验证绑定结果进入U-Boot命令行执行dm tree你应该能看到类似这样的输出Class Index Probed Driver Name ... misc 0 [ ] demo_led |-- demo-led注意Probed列是空的说明绑定成功但尚未probe。想让它probe可以在源代码里用uclass_get_device主动获取或者直接在命令行里用bind命令其实更方便的做法是在demo_led_probe里打日志然后在某个初始化函数里调用一次uclass_get_device(UCLASS_MISC, 0, dev)。比如放在board_init_r之后的某个initcall里这样串口就能看到demo_led: demo-led probed, enabled1。8. 调骨架时踩过的坑与排查套路8.1 设备“消失”pre-reloc属性没带先说我最常遇到的坑。某次bring-upCPU刚起来就要操作一个eFUSE控制器读芯片版本我把驱动写好、设备树节点也加了启动时却死活不打印probe日志。查了半天才发现board_init_f阶段要求这个控制器必须可用所以得加u-boot,dm-pre-reloc而board_init_r阶段又因为同样的节点在扫描时返回的错误被掩盖导致设备一直没绑上。排查套路其实很固定打开CONFIG_DM_DEBUG不同版本可能有差异或直接看启动日志里有没有dm_scan_fdt_node相关的debug输出。如果发现某个节点被跳过优先检查它是否因为在pre_reloc_only扫描阶段没有pre-reloc属性而被过滤。8.2 compatible匹配不上时先查什么dm tree里看不到节点时不要急着怀疑框架先做三件事确认驱动源文件真的编进去了grep一下u-boot.map里的_u_boot_list_2_driver_2_demo_led符号搜不到就是编译环节断了确认设备树编译进去了fdtdump或fdtget查看dtb里节点是否存在compatible字符串一个字符都不能错确认驱动里的of_match表和设备树compatible严格一致包括大小写和厂商前缀。我以前遇到过最无语的情况是设备树里写了demo,led驱动里写的是demo,LED大小写不一致整整折腾了一个下午。8.3 dm tree看状态、打开调试宏U-Boot自带的dm tree命令是调驱动模型的利器。它能列出每个设备属于哪个uclass、probe了没有。看到一个设备既没报错又没probe多半就是没有代码路径去触发它。如果你怀疑框架内部出问题可以考虑打开CONFIG_DM_DEBUG它会输出更多bind/probe阶段的详细信息。还有一个比较隐蔽的点dm tree只显示已经绑定的设备如果一个节点完全没有对应driver它是不会出现在dm tree里的。所以“没出现”和“出现了但没probe”代表着两类完全不同的故障。8.4 probe不执行却绑定成功是怎么回事绑定成功但probe不执行通常有三种原因设备确实没有被任何使用者获取这只是正常行为骨架本来就只是骨架probe被某个前置条件卡住了比如父设备probe失败device_probe直接return错误probe被调用了但迅速失败而失败路径没有明显打印——建议在probe开头无脑加一句printf这是最土也最有效的排查法。如果你想让一个驱动绑定后立即probe可以在struct driver里加DM_FLAG_PROBE_ON_SCAN标志老版本叫DM_FLAG_PROBE_ON_SCAN新版本改为编译期.flags例如U_BOOT_DRIVER(demo_led) { ... .flags DM_FLAG_PROBE_ON_SCAN, };加了之后dm_scan绑定udevice时会检测这个标志并主动调用device_probe。但这个标志不要乱用它适合时钟、电源、串口这类“启动核心链路必须随时可用”的设备普通外设别跟风。最后再分享一个个人心得学习DM骨架最好的路径不是把每个宏每个函数都背下来而是抓住“绑定bind—探测probe—使用get”这条主线再配合dm tree命令反复做实验。真正看懂board_init_r里那次dm_init_and_scan(false)你就等于拿到了U-Boot驱动框架的钥匙——以后再看到任何新的驱动、新的uclass脑子里都会自动把它映射到“链接表里的一个条目、设备树里的一个节点、uclass里的一块内存”这三个位置上。这套思维习惯值得花点时间养起来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →