
干U-Boot底层开发的人多多少少都被driver model驱动模型下文统一叫dm折磨过。尤其是移植新板子的时候经常会遇到一个诡异现象驱动代码明明写进去了设备树节点看起来也没问题但系统起来之后串口就是不出声或者某个外设怎么调都调不出来。这时候你要是能看懂board_init_r里那串又长又有序的初始化序列顺着dm的骨架一路摸下去大多数问题都能定位到根因。这篇文章我就想聊一件事U-Boot在board_init_r里到底是怎么把dm的驱动骨架搭起来的。我会沿着代码路径从initr_dm开始拆到dm_init_and_scan再拆到lists_bind_fdt把每一步的关键数据结构和调用关系讲清楚。看完之后你再遇到“设备没绑定”“驱动没匹配”“probe失败”这类问题心里就会有一张完整的地图而不是靠瞎猜和反复加打印碰运气。这篇文章适合正在做U-Boot移植、底层驱动bringup或者对dm tree命令打出来的信息一头雾水的人。就算你只是被某个“离奇的uart不工作”问题困住把dm骨架这部分搞明白也能帮你节省至少一个下午的调试时间。1. 从board_init_r这口大锅开始1.1 为什么“初始化全部”要写成一张表board_init_r是U-Boot完成代码重定位之后调用的总调度函数位置在common/board_r.c。它的写法很老派但非常实用内部维护了一个init_sequence_r[]函数指针数组一个个顺序调用。核心循环逻辑大致是for (init_fnc_ptr init_sequence_r; *init_fnc_ptr; init_fnc_ptr) { if ((*init_fnc_ptr)() ! 0) { hang(); } }每个initr_xxx函数返回0表示成功返回非0就hang住。这种设计有一个很直接的好处初始化步骤被拍平成一串串行的依赖链任何一步挂了后面就不用跑了。你只需要看最后打印到哪一行就知道是哪个初始化组件出了问题。这个数组里塞了几十项从initr_preserve_ram、initr_reloc、initr_malloc一路排到initr_serial、initr_net、initr_flash等。跟dm直接相关的关键步骤大致排布是initr_reloc完成代码重定位initr_malloc把malloc切到真正的完整内存区域initr_dm启动驱动模型建立dm骨架initr_of_live如果开启CONFIG_OF_LIVE把扁平设备树展开成live tree方便运行时操作initr_dm_devices部分版本存在补充扫描initr_serial串口子系统开始工作走dm框架去probe串口设备。注意这个顺序dm必须在串口之前就绪。因为串口本身也是设备树里的一个节点也是挂在dm下面的一个udevice。没有dm骨架后面所有驱动都没法正常“登记”和“开工”。1.2 dm在“总调度表”里的特殊地位init_sequence_r里大部分初始化函数都是相对独立的比如清BSS、设置环境变量、初始化时钟。但dm不一样它不是某个具体硬件外设而是一套基础设施。后面所有外设驱动无论是串口、网卡、GPIO还是MMC都要通过dm来管理。所以你可以把dm理解成“物业的总管理系统”。board_init_r这口大锅里的其他函数是各个商户而initr_dm是在商户营业之前先把整栋楼的登记系统、门禁体系、房间号分配规则搭好。没有这套系统后面的驱动就是无头苍蝇。而且dm的初始化分两个阶段dm_init负责建立根骨架dm_scan负责扫描设备树并绑定设备。这种“先立骨架、再填血肉”的思路贯穿整个dm设计。搞明白这两个阶段你就能看懂后续所有驱动探测流程。2. initr_dmdm的启动扳机2.1 保存内存池这句代码不能跳过initr_dm的实现大致如下static int initr_dm(void) { /* Save the driver model memory pool */ dm_mem_pool mem_malloc_start; if (!CONFIG_IS_ENABLED(OF_CONTROL)) return 0; #if CONFIG_IS_ENABLED(OF_LIVE) if (!gd-of_root) gd-of_root oftree_live_fdt(gd-fdt_blob); #endif return dm_init_and_scan(true); }第一眼看上去dm_mem_pool mem_malloc_start;这句赋值很像废话但背后是有讲究的。U-Boot的malloc分阶段重定位之前用CONFIG_SYS_MALLOC_F_LEN定义的那一小块区域重定位之后才切换到完整内存。dm在早期创建的全部uclass、udevice结构体都是从malloc区域里分配的而重定位过程中malloc区域的地址和大小会变化。记下mem_malloc_start相当于给dm结构体所在的“老地盘”留一个锚点方便将来判断哪些内存是dm占用的或者在某些配置下做统一释放。旧版U-Boot在dm内存管理上比现在更依赖这个池现在保留这句算是历史包袱加实际用途兼备。紧接着如果启用了设备树控制OF_CONTROL和OF_LIVE这里还可能会提前把gd-of_root建立起来。这一步的意义是后续dm扫描时如果要用live tree的节点操作接口直接能用不用再临时展开。最后一行dm_init_and_scan(true)正式拉开dm骨架搭建的大幕。2.2 dm_init_and_scan先立根再扫描dm_init_and_scan是board_init_r里dm初始化的总入口。它做了两件事先调dm_init再调dm_scan。看代码int dm_init_and_scan(bool pre_reloc_only) { int ret; if (gd-dm_root) { /* Driver model already initialized */ return 0; } ret dm_init(); if (ret) return ret; ret dm_scan(pre_reloc_only); if (ret) { log_debug(dm_scan() failed: %d\n, ret); return ret; } return 0; }gd-dm_root是dm是否已经初始化的标志。它是全局数据global_data里的一个指针指向根设备。只要它非空说明骨架已经存在不再重复初始化。dm_init()做的事情更纯粹int dm_init(void) { int ret; if (gd-dm_root) { /* Driver model already initialized */ return 0; } INIT_LIST_HEAD(gd-uclass_root); ret device_bind_by_name(NULL, false, root_info, gd-dm_root); if (ret) return ret; return 0; }这里面有两个关键动作第一初始化gd-uclass_root链表头。往后每注册一个uclass都会挂到这个链表上。可以说这就是dm骨架的“主脊梁”。第二通过device_bind_by_name创建根设备。root_info只提供了驱动名字static const struct driver_info root_info { .name root_driver, };这个root device没有任何硬件含义纯粹是给整棵设备树一个挂载点。设备树上所有的节点最终都会以它为祖父或祖先节点一层层挂下来。dm_init结束后dm骨架的“根”已经立起来了。但此刻整个dm体系还是空的除了一个root device什么都没有。接下来要看dm_scan如何填充血肉。3. dm的核心三角uclass、driver、device3.1 用物业管理系统理解三个角色在继续追dm_scan的代码之前必须先把uclass、driver、device这三者的关系讲透。很多人被dm绕晕就是没分清这三个概念。打个比方一家物业公司管理一个小区uclass/uclass_driver相当于“类目中心”物业把所有房屋分成住宅、商铺、车位、仓库等类型。对应到dm就是UCLASS_SERIAL、UCLASS_GPIO、UCLASS_MMC这样的类别。每个uclass有一个uclass_driver提供这类设备的公共行为比如post_probe回调当这类设备probe完成后统一做点收尾工作。driver相当于“管家服务标准”。比如住宅有住宅的管理细则商铺有商铺的管理细则。在dm里ns16550_serial_driver就是专门处理一类16550兼容串口硬件的驱动。它里面定义了of_match兼容表、probe回调、ofdata_to_platdata回调等。deviceudevice相当于具体某个“房间号”。设备树里每个带compatible属性的节点在匹配到driver之后会被实例化成一个udevice挂到对应的uclass下面。两个相同串口控制器共享同一个driver但会生成两个独立的udevice各自管自己的寄存器地址和状态。关键要记住的一点bind和probe是分离的。bind只是“登记”给这个设备建立一个udevice记录分配序号挂链表不动硬件。probe才是“开工”驱动代码里的probe回调就是这时候执行的访问寄存器、配置时钟、注册中断都发生在probe阶段。在board_init_r的dm初始化阶段只做bind绝大多数设备不会马上probe。真正触发probe的是后续某个子系统调用uclass_get_device或device_probe的时候。这就是为什么你经常发现设备树里有个设备dm tree里也能看到但Probed列是空的——不是bug是它还没被“开工”。3.2 lists_bind_fdt设备树节点与驱动的“相亲大会”接着回到dm_scan。dm_scan内部根据配置会走两条路启用OF_CONTROL时走dm_scan_fdt否则走dm_scan_platdata后者多见于SPL阶段使用OF_PLATDATA的配置。主流情况下代码路径是int dm_scan(bool pre_reloc_only) { if (CONFIG_IS_ENABLED(OF_CONTROL)) { ret dm_scan_fdt(gd-fdt_blob, pre_reloc_only); } else { ret dm_scan_platdata(pre_reloc_only); } return ret; }dm_scan_fdt直接调用dm_scan_fdt_node从gd-dm_root这个根设备开始递归扫描设备树节点int dm_scan_fdt(const void *blob, bool pre_reloc_only) { return dm_scan_fdt_node(gd-dm_root, blob, 0, pre_reloc_only); }dm_scan_fdt_node遍历当前节点下的所有子节点对每个节点做三件事如果pre_reloc_only为true先判断这个节点是否被标记为“早期可用”不是的话就跳过调用lists_bind_fdt让这个节点去匹配驱动如果匹配成功创建了udevice就递归处理这个设备下的子节点。这里最关键的就是lists_bind_fdt。可以把它想成一场相亲大会设备树节点手上拿着自己的compatible属性遍历uclass链表每个uclass再带着自己关联的一批driver挨个比对compatible。比对上了就完成绑定。核心流程简化如下static int lists_bind_fdt(...) { for (uclass uclass_root; uclass; uclass uclass-next) { struct uclass_driver *uc_drv uclass-uc_drv; driver uclass_find_driver_by_ofnode(uc_drv, node); if (!driver) continue; ret device_bind_common(parent, driver, name, NULL, node, dev); ... } }device_bind_common是真正的“结婚登记”环节它会分配一个udevice结构体把dev-driver指向匹配到的driver把设备挂到parent的child链表上同时挂到对应uclass的device链表上根据alias和现有设备序号分配seq编号记录设备树节点偏移量of_offset但不调用probe。这个“仅登记、不干活”的设计让dm在初始化阶段可以快速扫描完整棵设备树开销很小。如果在这个阶段就去probe所有设备那板子刚起来就会因为大量设备争抢时钟、GPIO、复位资源而乱套。3.3 pre_reloc_only为什么有的设备不能急着绑定board_init_r里传给dm_init_and_scan的第一个参数是true也就是pre_reloc_only true。这意味着第一次扫描只绑定被标记为“早期可用”的设备。设备树里通过u-boot,dm-pre-reloc属性新版本U-Boot引入了bootph-pre-reloc两者兼容期并存来标记。为什么非要这个机制两个原因。第一内存有限。重定位之前的malloc区域很小整个dm结构体虽然不大但设备树里几十上百个节点全绑一遍内存还是吃紧。SPL阶段尤其明显很多SPL的malloc区就几KB根本扛不住全量扫描。第二依赖关系。很多设备要能正常工作依赖的父节点或关联节点比如clock controller、gpio controller、reset controller必须先就绪。如果在扫描阶段就把所有设备probe了很可能子设备的时钟还没配、复位还没解一通操作全白费。所以U-Boot的策略是现在只绑定那些“保命级”设备比如串口、内存控制器相关的节点等重定位完成、完整malloc区就绪后再全量扫描绑定。这也是为什么你在普通U-Boot启动日志里看不到dm_scan第二次调用的痕迹——它往往被封装在后续某个初始化步骤中按需补全。实际操作中如果你发现自己新加的某个外设节点在dm tree里死活不出现第一反应就应该是这个节点有没有打上u-boot,dm-pre-reloc标记有些情况下U-Boot的fdtgrep或SPL设备树裁剪甚至会把没打标记的节点直接删掉那就不只是dm的问题而是设备树在编译阶段就被裁掉了。4. 实操验证dm骨架是否搭好4.1 用dm tree查看绑定关系骨架搭没搭好空口无凭得上命令。U-Boot命令行里最常用的三个命令dm tree按父子层级打印所有设备同时显示Probed状态dm uclass打印所有uclass及其驱动信息dm devices打印所有设备实例不区分层级。执行dm tree后输出类似这样Class Probed Driver Name ------------------------------------------- root [ ] root_driver root_driver serial [ ] ns16550_serial serialff020000 gpio [ ] gpio_sunxi gpioff024000 mmc [ ] sunxi_mmc mmcff0f0000Probed列是[ ]代表已经probe过是空格代表只bind了这个设备还没probe。看到serial后面有[ ]说明串口驱动已经成功跑起来了。如果某个节点压根没出现在dm tree里说明bind阶段就没成功那问题大概率出在compatible匹配、pre_reloc标记或设备树裁剪上。实际排障时我习惯按这样的顺序操作先执行dm tree看目标设备在不在列表里不在查设备树节点是否被裁剪、compatible是否写对在但Probed为空查依赖设备有没有先probe成功以及驱动probe回调有没有报错如果连dm tree命令本身都跑不了那说明dm初始化阶段已经崩了回到代码加早点打印。4.2 设备没出现先从三个方向排查设备没出现在dm tree里是移植时最常见的问题。我总结下来90%的原因跑不出这三类第一类设备树裁剪。SPL阶段和正式U-Boot阶段用的设备树可能是不同的。SPL常用CONFIG_OF_SPL_REMOVE_PROPS甚至专门的SPL设备树来减少体积。如果你加节点只加在了主设备树里SPL用的那个裁剪版本里可能压根没有。正式U-Boot阶段也可能存在fdtgrep过滤需要确认编译产物里确实包含节点。第二类compatible匹配不上。驱动的of_match表里写的是什么字符串设备树节点里compatible第一个字符串是什么必须完全一致包括厂商前缀的大小写。最坑的是有些驱动用的是旧版绑定比如ns16550a但设备树里写的是snps,dw-apb-uart那自然匹配不上。第三类父节点没绑上。dm_scan_fdt_node是递归的一个节点要能被绑定它的父节点必须先绑定成功。如果父节点compatible写错了或者父节点被pre_reloc过滤掉了整棵子树都进不来。4.3 probe失败怎么查如果设备出现在dm tree里但Probed列一直是空的说明bind成功、probe失败或者压根没被probe。这里有一个关键区分是“没被probe”还是“probe了但失败”。如果是“没被probe”往往不是问题。dm设计的哲学是懒加载只有某个uclass真的需要设备时才会调用uclass_get_device去probe。比如一个GPIO控制器如果整个系统运行期间没有任何驱动请求GPIO它可能永远处于bind状态。如果是“probe了但失败”U-Boot会打印错误码。常见原因依赖设备没就绪。驱动probe里常常会调用dev_get_clk、reset_get_by_index、gpio_request_by_name这些调用会隐式probe依赖设备。如果依赖设备自身的probe失败这边也会跟着返回错误。寄存器地址解析失败。dev_read_addr拿到的地址如果不对访问寄存器直接崩或者读出来全是错误值。驱动私有数据没分配。driver定义里如果设了priv_auto_alloc_sizedevice_bind_common会自动分配私有数据如果没有probe里直接访问dev-priv就会访问空指针。排查probe问题时最有效的手段是打开U-Boot的日志系统。在板级配置里开启CONFIG_LOG然后在驱动文件里加上#define LOG_CATEGORY LOGC_DM设置loglevel到8以上你会看到dm层打印的详细调用链哪一步返回什么错误码清清楚楚。5. 避坑与心得5.1 移植新板子的dm排查清单干脆把这几年踩过的坑整理成一张速查表你照着排查就行现象第一步检查常见原因dm tree里没有目标设备节点是否有u-boot,dm-pre-reloc或bootph-pre-reloc被pre_reloc过滤或设备树裁剪dm tree里节点存在但compatible匹配不上驱动的of_match表与节点compatible字符串不一致含大小写和厂商前缀设备有名字但Probed一直为空谁调用了uclass_get_device懒加载设计没人请求就不probeprobe返回错误码依赖设备的probe状态clock/gpio/reset等父设备没就绪串口完全没有输出initr_serial是否在initr_dm之后初始化顺序被改动或dm崩在早期系统hang在board_init_r中途最后打印的initr_xxx是哪个该初始化函数返回非0这个表不能解决所有问题但能帮你快速缩小范围。5.2 几个容易被忽略的细节再分享几个纯经验层面的东西都是常规文档里不太会写到的第一u-boot,dm-pre-reloc的属性迁移。最新U-Boot2023.04之后开始用bootph-pre-reloc这些新属性替换老属性。两者在兼容期内都能用但你如果基于新版本U-Boot做移植最好直接采用新属性。网上大量老文章还在推老属性照抄到新版本上可能不生效或者行为有差异。第二CONFIG_DM_SEQ_ALIAS和alias编号。设备树里的aliases节点对设备编号影响很大。比如aliases { serial0 uart0; serial1 uart1; };。如果你的设备树alias写错或者多个串口节点共用同一序号的aliasU-Boot会分配出奇怪的seq编号导致console指向错误串口。这就是“明明串口驱动匹配了但println输出到别处去”的经典原因之一。第三dm结构体和malloc区的关系。如果CONFIG_SYS_MALLOC_F_LEN设得太小dm_init或第一次dm_scan时malloc申请会失败系统会在很早期就hang住。这类问题最坑因为日志可能只打印到malloc初始化就断了。我建议SPL阶段至少给CONFIG_SYS_MALLOC_F_LEN留足空间具体大小取决于设备树节点数量。第四多个设备共享一个driver时的静态变量陷阱。一个driver可以被多个udevice实例共享如果在driver的probe里用了静态局部变量来保存寄存器地址或状态那第二个设备probe时会把第一个设备的数据冲掉。必须用dev-priv这种per-device数据。我自己最初搞dm时被“设备就是不出现”的问题折磨了两天最后发现只是设备树里少了一个u-boot,dm-pre-reloc属性。从那以后我养成一个习惯任何驱动bringup第一步先跑dm tree看一眼设备在不在再看Probed状态最后才翻驱动代码。这套流程帮我省了不知道多少时间。还有一个救命小技巧如果你在串口起来前就要看dm状态可以在board_init_r里initr_serial之前临时加一个printf直接打印gd-dm_root和几个关键uclass链表的长度。即使串口最终没起来这些值也能通过JTAG或者LED调试看到。等确认dm骨架没问题了再回头查串口怎么挂的。因为串口挂不上往往只是结果dm骨架不稳才是根源。后续如果要扩展可以沿着dm_scan的递归逻辑往深里挖把uclass_find_driver_by_ofnode的匹配细节和device_bind_common的完整初始化流程过一遍。或者把SPL阶段的dm初始化拉出来对比你会发现SPL里对pre_reloc和内存的限制更严格理解会更深。不过那是另一个话题了先把board_init_r里这条主线吃透你手里的屠龙刀才算真正开刃。