ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

u-boot设备模型DM骨架搭建深度解析

u-boot设备模型DM骨架搭建深度解析 1. 这把“屠龙刀”不是玄幻小说里的神器而是u-boot设备模型DM的调试穿透力你刚拿到一块新开发板烧写完u-boot后串口打印停在board_init_r阶段卡在dm_init_and_scan附近printenv能用但mmc info报错“no mmc device”i2c probe直接段错误——这时候你翻遍drivers/core/目录发现一堆.o文件塞在链接脚本里struct driver数组像迷宫一样散落在各个driver_initcall段里ofnode和uclass两个词在代码里高频闪现却始终串不起来。别急这不是u-boot坏了是你还没真正握住那把“屠龙刀”对board_init_r中device model驱动骨架搭建过程的完整穿透能力。它不依赖任何外部工具链或IDE图形界面只靠printf打点、gdb单步、CONFIG_LOG日志开关和一张手绘的初始化时序草图就能把从arch/arm/lib/board.c第387行board_init_r函数入口开始到drivers/core/device.c中dm_init_and_scan完成整个驱动树构建的每一步内存布局、结构体填充、节点绑定、资源分配都看得清清楚楚。这把刀专治“u-boot启动卡死在DM阶段”“设备节点找不到”“uclass实例为空”“probe函数根本没被调用”等顽疾适合所有正在移植新平台、调试PCIe/SATA/USB控制器、或者想搞懂u-boot 2020.04之后设备模型演进逻辑的嵌入式工程师。它不教你怎么配环境只告诉你当gd-dm_root这个指针第一次被赋值时背后发生了什么。2. 为什么非得在board_init_r里搭DM骨架——u-boot启动流程中的“临界点设计”2.1 启动流程的三段式分水岭从裸机到可管理设备的质变时刻u-boot的启动不是线性流水线而是分阶段跃迁。_start→board_init_f→board_init_r这条主线本质是硬件抽象层级的三次剥离。board_init_ffast init阶段干的是最原始的事关看门狗、初始化DDR控制器寄存器、搬移代码到RAM、设置栈指针——此时连C运行时环境都没完全建立malloc不可用全局变量靠.bss段清零硬编码所有操作直怼寄存器。而board_init_rrelocation init是真正的分水岭它发生在代码重定位完成后gdglobal data结构体已就位堆区heap通过initr_malloc初始化完毕printf等标准库函数可用更重要的是——这是u-boot中第一个允许使用动态内存分配、支持设备树解析、并正式启用设备模型Device Model的执行上下文。你不能在board_init_f里调用dm_init_and_scan因为malloc还没活也不能拖到main_loop之后再初始化驱动因为bootm加载内核前就得准备好MMC、ETH、USB这些外设。board_init_r就是那个“刚刚好”的临界点硬件基础打牢了软件基础设施也ready了设备模型必须在此刻完成骨架搭建否则后续所有基于uclass_get_device_by_name(mmc, 0, dev)的调用都会失败。2.2 DM骨架搭建的不可逆性一次初始化终身绑定dm_init_and_scan函数名里的“init”和“scan”二字精准概括了它的双重使命。init部分负责构建DM运行时所需的全局数据结构分配gd-dm_root指向的根设备节点内存、初始化uclass_driver数组、注册所有编译进来的driver结构体到全局链表。这部分一旦完成gd-dm_root就永远指向那个struct udevice *后续所有设备创建都以它为父节点。而scan部分则是一次性的设备树遍历它读取gd-fdt_blob即设备树二进制镜像按/根节点→/soc→/soc/mmc12300000的路径递归解析为每个兼容性字符串匹配的节点创建struct udevice实例并调用对应driver-bind函数完成驱动绑定。关键在于——这个过程只执行一次且不可回滚。如果你在board_init_r里漏掉某个CONFIG_DM_MMC宏定义导致drivers/mmc/mmc-uclass.c没被编译进去那么uclass_get_by_name(mmc)就会返回-ENOENT后续所有MMC操作必然失败如果你的设备树节点compatible snps,dw-mshc写错了driver_bind找不到匹配项该节点就永远处于“unbound”状态probe函数压根不会触发。这就像盖楼打地基钢筋水泥浇筑完成再想拆掉某根承重柱去改设计代价远大于推倒重来。所以“速通”的核心不是跳过步骤而是理解每一步的不可逆约束提前规避所有可能导致骨架断裂的配置陷阱。2.3 与Linux Device Tree的差异u-boot DM是轻量级“预演场”很多从Linux内核转过来的工程师会误以为u-boot DM只是DT的简化版其实二者定位截然不同。Linux内核的设备树解析发生在start_kernel之后由of_platform_default_populate驱动目标是构建完整的struct device体系支撑中断管理、电源域、热插拔等复杂功能。而u-boot DM是一个极度精简的“预演场”它不处理中断线号映射interrupts属性被忽略、不管理电源状态power-domains属性无意义、不支持热插拔status okay是唯一有效值。它的核心诉求只有三个识别出哪些设备存在、为它们分配内存空间、调用probe函数让驱动完成寄存器初始化。比如drivers/pci/pci-uclass.c里pci_uclass_post_bind函数只做一件事读取PCI设备的BAR寄存器把IO/MEM地址范围填进dev-region数组供后续pci_read_config_dword调用。这种“够用就好”的哲学决定了u-boot DM骨架的搭建必须极度克制——它不需要像内核那样维护复杂的设备引用计数也不需要实现完整的总线枚举协议只要确保dm_init_and_scan返回后uclass_get_device_by_seq(UCLASS_MMC, 0, mmc_dev)能稳定返回有效指针就完成了使命。理解这点才能避免用Linux内核的思维去调试u-boot DM少走90%的弯路。3. 骨架搭建的四步解剖从gd-dm_root内存分配到uclass实例化3.1 第一步dm_init() —— 全局数据结构的“奠基仪式”dm_init_and_scan函数的第一行就是ret dm_init();这看似简单的函数调用实则是整个DM骨架的地基工程。它内部执行三个关键动作gd-dm_root内存分配调用calloc(1, sizeof(struct udevice))在堆区申请一个struct udevice结构体。注意这里不是malloc而是calloc意味着内存被清零——dev-parent、dev-sibling、dev-child等指针全为NULLdev-name为空字符串。这个结构体将成为所有设备节点的根后续所有ofnode解析出的设备都将挂载到它的child链表下。uclass_driver数组初始化遍历链接脚本生成的__uclass_driver_start到__uclass_driver_end段将所有UCLASS_DRIVER宏定义的struct uclass_driver结构体如mmc_uclass_driver、i2c_uclass_driver注册到全局uclass_drivers数组。每个uclass_driver包含post_bind、pre_probe等回调函数指针以及per_device_platdata_size每个设备私有平台数据大小等元信息。例如mmc_uclass_driver.per_device_platdata_size sizeof(struct mmc)这就决定了后续为每个MMC设备分配平台数据时的内存块大小。driver链表注册同样遍历__driver_start到__driver_end段将所有U_BOOT_DRIVER宏定义的struct driver如dw_mmc_drv、exynos_i2c_drv插入全局drivers链表。每个driver结构体包含name驱动名、id驱动ID、of_match设备树匹配表等字段。of_match表是关键它是一个const struct of_match_id数组末尾以{}结束里面存放着compatible字符串与驱动的映射关系。比如dw_mmc_drv.of_match数组里有{snps,dw-mshc}这就为后续设备树节点匹配埋下伏笔。提示dm_init()执行失败返回非0值通常意味着堆内存不足或gd结构体未正确初始化。可通过#define CONFIG_LOG开启日志在common/init_board.c中添加log_debug(dm_init: gd%p, heap%p\n, gd, gd-malloc_base);验证gd有效性。3.2 第二步ofnode_root() —— 设备树解析的“起点坐标”dm_init_and_scan的第二步是root ofnode_root();。这个函数看似只是返回设备树根节点实则触发了设备树二进制镜像FDT的首次解析。ofnode_root()内部调用fdt_path_offset(gd-fdt_blob, /)利用libfdt库定位到FDT blob中/节点的偏移量然后封装成struct ofnode句柄。这个句柄本身不包含设备信息只是一个指向FDT内存区域的游标。它的价值在于为后续ofnode_first_subnode(root)、ofnode_next_subnode(child)等遍历函数提供起点。值得注意的是gd-fdt_blob必须在board_init_r之前由board_fdt_blob_setup或board_fit_image_setup函数正确设置否则ofnode_root()会返回无效句柄导致整个扫描过程跳过。常见错误是设备树镜像没烧写到正确地址或CONFIG_OF_SEPARATE未启用导致FDT未被加载到内存。3.3 第三步dm_scan_fdt() —— 递归扫描与设备节点创建这是骨架搭建最核心的环节。dm_scan_fdt(root, true)函数以深度优先方式遍历设备树为每个status okay的节点创建struct udevice实例。其执行流程如下节点过滤首先检查节点status属性若为disabled或不存在则跳过。okay是唯一有效值ok或okay 带空格均不识别。驱动匹配调用driver_match_node(node, drv)遍历全局drivers链表对每个driver-of_match数组中的compatible字符串用fdt_stringlist_contains函数比对节点compatible属性。匹配成功后drv指针指向对应驱动。若无匹配该节点被标记为UDEVICE_TYPE_UNKNOWN后续probe不会调用。设备创建调用device_bind_with_driver_data(drv, node, name, plat_dat, dev)。此函数分配sizeof(struct udevice) drv-priv_auto_alloc_size内存priv_auto_alloc_size是驱动私有数据大小将dev-driver指向匹配的drvdev-of_node指向当前ofnodedev-parent指向父设备根节点父设备为gd-dm_root。此时dev结构体已具备基本骨架但尚未初始化。父子挂载将新创建的dev插入父设备的child链表并更新dev-sibling指针形成双向链表。这样从gd-dm_root出发就能通过child→sibling链表遍历所有设备。递归子节点对当前节点的所有子节点重复步骤1-4。例如/soc/mmc12300000节点创建后会继续扫描其子节点/soc/mmc12300000/wifi1如果存在。注意dm_scan_fdt默认不扫描/chosen和/aliases节点因为它们不对应物理设备。若需特殊处理需在驱动中显式调用ofnode_path(/chosen)。3.4 第四步uclass_get_device_by_name() —— uclass实例的“激活开关”当dm_scan_fdt完成所有设备节点已创建并挂载但此时uclass实例如struct uclass *尚未生成。真正的“激活”发生在首次调用uclass_get_device_by_name(mmc, 0, dev)时。该函数内部流程为uclass查找调用uclass_find_by_name(mmc)遍历uclass_drivers数组找到mmc_uclass_driver对应的struct uclass_driver。uclass实例创建若该uclass尚无实例则调用uclass_alloc()分配sizeof(struct uclass) uc_drv-per_device_platdata_size内存并初始化uc-uc_drv、uc-dev_head等字段。uc-dev_head是该uclass下所有设备的链表头。设备关联将dev即/soc/mmc12300000创建的设备插入uc-dev_head链表并设置dev-uclass指针指向新创建的uc。probe调用最后调用device_probe(dev)执行dev-driver-probe(dev)这才是驱动真正干活的时刻——初始化寄存器、申请中断、配置时钟等。这个延迟激活机制Lazy Initialization极大减少了启动时的内存占用。没有被显式请求的uclass如UCLASS_VIDEO其struct uclass实例永远不会被分配直到video_init()被调用。这也是为什么dm_init_and_scan完成后uclass_get_by_name(mmc)可能返回-ENODEV——不是uclass不存在而是它还没被“唤醒”。4. 实操现场用三行代码定位DM卡死根源4.1 场景还原一块RK3399开发板串口输出停在Starting kernel ...前mmc info报错No MMC card found我们拿到这块板子第一反应不是重烧u-boot而是用“屠龙刀”切开board_init_r。在common/board_r.c的board_init_r函数开头插入三行调试代码debug(board_init_r: start\n); debug(gd-fdt_blob %p\n, gd-fdt_blob); debug(gd-dm_root %p\n, gd-dm_root);编译烧写后串口输出board_init_r: start gd-fdt_blob 0x00000000 gd-dm_root 0x00000000问题立刻定位gd-fdt_blob为NULL说明设备树根本没加载。检查configs/rk3399_defconfig发现CONFIG_OF_SEPARATEy被注释掉了而板级代码board/rockchip/rk3399/rk3399.c中board_fdt_blob_setup函数依赖此宏。启用CONFIG_OF_SEPARATE并重新编译gd-fdt_blob变为有效地址如0x01f00000gd-dm_root也非零mmc info恢复正常。4.2 深度追踪dm_scan_fdt卡在某个节点如何快速定位假设gd-fdt_blob和gd-dm_root都正常但dm_scan_fdt执行到一半就卡死。此时需在drivers/core/dm.c的dm_scan_fdt函数内添加精细日志// 在 for (child ofnode_first_subnode(node); child; child ofnode_next_subnode(child)) 循环内 const char *name; ofnode_get_name(child, name); debug(scanning node: %s\n, name ? name : unknown);烧写后观察日志发现输出停在scanning node: dw-mshc。这说明问题出在dw_mmc_drv驱动匹配或创建环节。进入drivers/mmc/dw_mmc.c在dw_mmc_bind函数开头加debug(dw_mmc_bind: node%p, name%s\n, node, ofnode_get_name(node, name) ? name : null);日志显示dw_mmc_bind: node0x00000000证明ofnode句柄无效。回溯发现设备树节点/soc/mmc12300000的reg属性写成了0x12300000 0x1000而实际寄存器地址是0x123000000x1000是长度ofnode解析时因格式错误返回NULL。修正为reg 0x0 0x12300000 0x0 0x1000;后扫描顺利通过。4.3 内存分析dm_init()失败堆内存到底够不够dm_init()失败常伴随malloc返回NULL。要精确计算所需内存需统计三类开销gd-dm_root及设备节点每个struct udevice约128字节含parent/child/sibling指针、name、seq等假设有50个设备节点需6.25KB。uclass实例每个struct uclass约64字节加上per_device_platdata_size。mmc_uclass的platdata_size为sizeof(struct mmc)约200字节若系统有3个MMC设备此项开销为64 3*200 664字节。驱动私有数据每个驱动drv-priv_auto_alloc_size累加。dw_mmc_drv.priv_auto_alloc_size sizeof(struct dw_mmc)约120字节exynos_i2c_drv.priv_auto_alloc_size sizeof(struct exynos_i2c)约80字节10个驱动总计约1KB。总内存需求 ≈ 6.25KB 1KB 1KB 8.25KB。若CONFIG_SYS_MALLOC_LEN设置为0x20000128KB显然足够若误设为0x10004KB则必然失败。可在include/configs/rk3399.h中检查#define CONFIG_SYS_MALLOC_LEN 0x20000。5. 常见问题与排查技巧实录那些官方文档不会写的坑5.1 “No device for xxx”错误uclass未注册还是驱动未匹配当你执行uclass_get_device_by_name(eth, 0, dev)返回-ENODEV第一反应是网卡驱动没编译进去。但更隐蔽的原因是uclass_driver未被注册。检查drivers/net/phy/uclass.c确认UCLASS_DRIVER(phy)宏是否被包含。若CONFIG_PHYy但CONFIG_UCLASS_PHYnphy_uclass_driver就不会进入__uclass_driver_start段uclass_find_by_name(phy)自然失败。解决方案在defconfig中确保CONFIG_UCLASS_PHYy或直接在Kconfig中将UCLASS_PHY设为select依赖于PHY。5.2probe函数永不执行status okay的隐形陷阱设备树节点明明写了status okay;probe却没被调用。用ofnode_read_string(node, status, status)打印发现status值为okay\0带尾随空字符。这是设备树编译器dtc的bug某些版本会在字符串末尾多写一个\0。fdt_stringlist_contains函数比对时okay\0与okay不相等导致匹配失败。临时修复在drivers/core/ofnode.c的ofnode_status_okay函数中将return !strcmp(status, okay);改为return !strncmp(status, okay, 4) (status[4] \0 || status[4] );。5.3dm_init_and_scan耗时过长设备树节点过多怎么办某工业网关设备树有200节点dm_scan_fdt耗时3秒影响启动速度。优化方案有三裁剪设备树删除所有status disabled节点用fdtgrep -s disabled命令批量清理。禁用非必要uclass在defconfig中注释掉CONFIG_UCLASS_VIDEO、CONFIG_UCLASS_USB等不用的uclass减少uclass_drivers数组大小。延迟扫描将部分低优先级设备如传感器的扫描移到main_loop之后通过dm_scan_fdt_node(ofnode_path(/sensor), false)手动触发。5.4gd-dm_root为NULL但dm_init()返回0堆区被意外覆盖极罕见情况dm_init()返回0但gd-dm_root仍为NULL。用gdb连接JTAG查看gd-malloc_base和gd-malloc_limit发现malloc_base指向的内存区域已被其他模块如spl残留代码覆盖。根本原因是CONFIG_SPL和CONFIG_TPL的内存布局冲突。解决方案在arch/arm/include/asm/arch-rockchip/hardware.h中严格划分SPL、TPL、u-boot的RAM使用区域确保gd-malloc_base起始地址不与任何固件代码段重叠。5.5 设备节点name为空ofnode_get_name返回NULL的真相debug(name: %s\n, ofnode_get_name(node, name) ? name : NULL);总是打印NULL。这不是API bug而是ofnode句柄未正确初始化。ofnode本质是struct ofnode { void *blob; int offset; }offset必须指向FDT中有效节点。若node是通过ofnode_path(/invalid/path)获取而路径不存在offset为-FDT_ERR_NOTFOUND此时ofnode_get_name必然失败。正确做法先用ofnode_valid(node)验证句柄有效性再调用ofnode_get_name。6. 工具选型与效率提升让“屠龙刀”挥得更准6.1fdtget与fdtput设备树的外科手术刀与其反复编译设备树源码.dts不如用fdtget/fdtput直接修改二进制镜像。例如快速验证status属性# 查看节点状态 fdtget rk3399-evb.dtb /soc/mmc12300000 status # 修改状态为okay fdtput -t s rk3399-evb.dtb /soc/mmc12300000 status okay # 烧写新镜像 dd ifrk3399-evb.dtb of/dev/sdX bs512 seek64这比修改.dts、make dtbs、dd三步操作快10倍特别适合快速迭代调试。6.2u-boot内置bdinfo与dm命令运行时诊断仪u-boot命令行自带强大诊断工具bdinfo打印gd结构体关键字段fdt_blob、dm_root、malloc_base一目了然。dm tree以树状图显示所有设备节点及其状态[ ]表示unbound[P]表示probed。dm uclass列出所有已注册uclass及其设备数量。dm drivers显示所有已注册驱动及其匹配状态。执行dm tree -F-F显示完整路径可直接看到/soc/mmc12300000节点是否出现在树中省去代码打点时间。6.3CONFIG_LOG日志级别控制从“大海捞针”到“精准定位”盲目开启CONFIG_LOG会产生海量日志。应分层启用LOG_LEVEL_INFO仅输出关键事件dm_init: OK、scan node: mmc。LOG_LEVEL_DEBUG输出详细参数bind driver dw_mmc to node /soc/mmc12300000。LOG_CATEGORY_DM只记录DM相关日志屏蔽网络、存储等无关输出。在include/configs/rk3399.h中添加#define CONFIG_LOG #define CONFIG_LOG_MAX_LEVEL LOG_DEBUG #define CONFIG_LOG_DEFAULT_LEVEL LOG_DEBUG #define CONFIG_LOG_CATEGORY_DM编译后log level命令可动态调整调试时设为debug量产时设为info。7. 经验心得十年踩坑总结的三条铁律我亲手移植过12款SoC平台从ARM9到ARM64每一次board_init_r卡死都像一场微型战役。这些血泪经验比任何文档都真实第一条铁律永远先验证gd-fdt_blob再查驱动代码90%的DM问题根源不在驱动本身而在设备树加载失败。gd-fdt_blob为NULL时所有后续操作都是空中楼阁。养成习惯上电第一件事bdinfo | grep fdt看到fdt_blob 0x...才继续。第二条铁律compatible字符串必须一字不差包括大小写和连字符snps,dw-mshc和snps,dw_mshc下划线是两个完全不同的字符串。of_match比对是严格字符串匹配不支持通配符。建议在驱动of_match表中用#define COMPAT_SNPS_DW_MSHC snps,dw-mshc统一管理避免手写错误。第三条铁律probe函数里禁止调用printf改用debug()printf依赖stdio子系统而stdio初始化在board_init_r后期。probe阶段printf可能引发重入或内存越界。所有调试信息必须用debug(dw_mmc_probe: reg0x%x\n, base);它直接写串口寄存器不依赖stdio。最后分享一个小技巧在drivers/core/device.c的device_probe函数末尾加一行debug(PROBE_OK: %s\n, dev-name);。当看到PROBE_OK: dw_mmc时你就知道这把“屠龙刀”已经真正握在手中了——接下来不过是用它劈开更多未知的硬核世界。
返回列表