尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

u-boot board_init_r 中 dm 驱动骨架搭建与调试实战

u-boot board_init_r 中 dm 驱动骨架搭建与调试实战 1. 从 board_init_r 说起为什么 dm 驱动骨架值得单独拎出来讲搞 u-boot 移植的人迟早会在board_init_r这个函数里迷路。它不像board_init_f那样只做早期内存布局和重定位board_init_r是真正把板级外设一个个叫醒的地方——串口要能用、MMC 要能读、网口要能通、环境变量要能存。而现代 u-boot 里这些外设几乎全部走driver modeldm这套框架来管理。我见过太多人移植新板子时串口能打印就以为大功告成结果一进board_init_r后半段就卡死或者dm树根本没建起来uclass找不到设备最后只能靠printf硬怼。问题的根子往往不在驱动本身而在于没搞清楚board_init_r里 dm 骨架到底是怎么一步步搭起来的。这篇内容就是把这个骨架拆开给你看。核心关键词是u-boot、board_init_r、dm 驱动、设备模型、driver。我会从整体设计思路讲到具体函数调用链再到实际移植中怎么验证 dm 是否正常、遇到uclass找不到设备怎么排查。适合正在做 u-boot 移植、想搞懂 dm 机制、或者被board_init_r里各种 init 顺序坑过的嵌入式工程师。读完你至少能做到看到board_init_r里的 dm 相关调用不再发怵能自己判断 dm 骨架有没有搭对出问题知道往哪查。先说结论board_init_r里的 dm 骨架不是一次性建成的它分几个阶段——先是dm_init_and_scan把驱动模型初始化并扫描设备树然后各uclass按优先级依次probe最后才是具体外设的初始化。这个顺序不能乱乱了就是各种-ENODEV或者空指针。2. 整体设计思路dm 骨架为什么长这样2.1 从“直接调驱动”到“设备模型”的转变早期 u-boot 的板级初始化很直接board_init_r里想初始化网口就直接调eth_initialize里面再硬编码去操作寄存器。板子一多同样的驱动代码被复制粘贴到各个板级文件里改一个寄存器地址要改十几处。这就是没有设备模型的典型症状。dm 驱动模型引入后核心思想变成设备device和驱动driver分离通过uclass做中间层。设备树里描述“这里有个串口寄存器基址是 0x...用的是 ns16550 驱动”u-boot 启动时解析设备树自动把设备和驱动匹配起来然后按uclass统一管理。board_init_r要做的就是触发这个匹配和初始化流程。为什么选在board_init_r而不是board_init_f因为board_init_f阶段内存还没重定位dm 需要动态分配内存来建设备链表、驱动链表这些操作在重定位前做会出问题。board_init_r跑在重定位后的 RAM 里堆已经可用才能安全地malloc出udevice和uclass结构。2.2 dm 骨架的三个关键阶段整个 dm 骨架在board_init_r里的搭建可以分成三个阶段我用一个表格先给你个全局印象阶段核心动作关键函数失败表现初始化与扫描建 uclass 链表、解析设备树、绑定设备与驱动dm_init_and_scan串口无输出、uclass为空按序 probe各 uclass 按优先级依次激活设备uclass_probe_all/ 各init调用某外设-ENODEV板级收尾具体外设初始化、环境变量加载board_init_r后半段启动卡死、env 读写失败这个顺序是有讲究的。dm_init_and_scan必须最先做因为它建立了整个 dm 的数据结构。如果这一步失败后面所有uclass操作都是空中楼阁。而probe阶段之所以要按优先级是因为有些设备依赖另一些设备——比如 MMC 控制器可能依赖 GPIO 和时钟GPIO 和时钟必须先 probe 好。2.3 为什么不用“一次性全部 probe”有人会问既然设备树都解析出来了为什么不一次性把所有设备都 probe 了原因有两个。第一是依赖顺序设备之间有依赖关系比如 I2C 控制器没初始化好挂在 I2C 上的 PMIC 就没法 probe。第二是按需初始化u-boot 是 bootloader不是操作系统没必要把所有外设都初始化。比如你这次启动只需要从 MMC 读内核那网口就可以不 probe省时间。dm 的uclass优先级机制正好支持这种按需、按序的初始化。提示uclass的优先级在UCLASS_DRIVER里通过.pre_probe和.post_probe控制但更关键的是uclass本身的id顺序它决定了uclass_probe_all的遍历顺序。3. 核心细节解析dm_init_and_scan 到底干了什么3.1 函数调用链拆解board_init_r里跟 dm 直接相关的调用最核心的就是dm_init_and_scan。它的调用链大致是这样的board_init_r - initr_dm() // 如果配置了 CONFIG_DM - dm_init_and_scan(false) - dm_init() // 初始化 dm 全局结构 - dm_scan_platdata() // 扫描平台数据老式板级文件 - dm_scan_fdt() // 扫描设备树 - dm_scan_other() // 其他扫描dm_init做的是最基础的初始化把gd-dm_root置空、初始化uclass链表头、设置dm_initialized标志。这一步如果失败后面全完。dm_scan_fdt是重头戏。它遍历设备树里所有带u-boot,dm-pre-reloc或者普通状态的节点为每个节点创建udevice然后根据compatible属性去驱动链表里找匹配的driver。找到就绑定找不到就标记为未绑定等后续uclass再处理。3.2 设备与驱动的匹配逻辑匹配的核心是compatible字符串。设备树节点里写uart0: serial10000000 { compatible ns16550a; reg 0x10000000 0x100; ... };驱动里写U_BOOT_DRIVER(ns16550_serial) { .name ns16550_serial, .id UCLASS_SERIAL, .of_match ns16550_serial_ids, ... };其中ns16550_serial_ids里包含ns16550a。dm_scan_fdt就是拿设备树的compatible去跟所有驱动的of_match表比对匹配上了就调用驱动的.bind方法如果有。这里有个容易踩的坑compatible字符串必须完全一致包括大小写和连字符。我遇到过设备树写ns16550而驱动只匹配ns16550a结果串口死活起不来查了半天才发现是字符串不匹配。3.3 uclass 的建立时机uclass不是设备树里显式描述的而是驱动通过.id字段声明的。当第一个属于某个uclass的设备被绑定时dm 会自动创建对应的uclass结构。比如第一个UCLASS_SERIAL的设备绑定后serial这个uclass就被创建出来后续所有串口设备都挂到这个uclass下。这个机制的好处是uclass是懒加载的不需要提前声明。但坏处是如果你在dm_init_and_scan之后立刻去访问某个uclass而该uclass下还没有设备被绑定那uclass_get就会返回-ENODEV。这就是为什么有些代码要在board_init_r里显式调用uclass_get并处理失败。注意uclass的创建依赖于设备绑定而设备绑定依赖于设备树解析。如果设备树里某个节点被status disabled关掉了那对应设备就不会绑定uclass可能就不会创建。4. 实操过程在 board_init_r 里验证 dm 骨架4.1 打开 dm 调试输出最直接的验证方式是把 dm 的调试开关打开。在include/configs/你的板子.h里加#define DEBUG #define CONFIG_DM_DEBUG #define CONFIG_DM_WARN重新编译烧录启动时你会看到类似这样的输出dm_init: dm_root 0x... dm_scan_fdt: scanning /serial10000000 dm_scan_fdt: bound ns16550_serial to serial10000000 uclass: created UCLASS_SERIAL ...这些输出能告诉你设备树节点有没有被扫描到、驱动有没有匹配上、uclass有没有创建。如果某个设备你期望它出现但没出现就顺着这条线查。4.2 手动 dump dm 树u-boot 提供了dm tree命令需要CONFIG_CMD_DM。在board_init_r完成后进入命令行敲 dm tree会打印出完整的 dm 设备树包括每个设备的uclass、驱动名、绑定状态。我一般会重点看几个东西串口设备在不在、MMC 设备在不在、它们的probe状态是不是active。如果某个设备显示probed false说明它还没被 probe可能是uclass优先级问题或者依赖没满足。4.3 关键代码段initr_dm 的实现以常见的 ARM 平台为例initr_dm在common/board_r.c里static int initr_dm(void) { int ret; /* 保存重定位前的 dm 根节点 */ gd-dm_root NULL; gd-dm_root_f NULL; ret dm_init_and_scan(false); if (ret) { debug(dm_init_and_scan() failed: %d\n, ret); return ret; } /* 按优先级 probe 所有 uclass */ ret uclass_probe_all(); if (ret) { debug(uclass_probe_all() failed: %d\n, ret); return ret; } return 0; }这段代码里dm_init_and_scan(false)的参数false表示不扫描pre-reloc设备那些在重定位前就已经初始化过的。uclass_probe_all则是按uclass的id顺序依次调用每个uclass的probe方法。4.4 参数计算uclass 优先级怎么定uclass的优先级实际上由UCLASS_DRIVER里的.id决定而.id是在include/dm/uclass-id.h里枚举定义的。顺序大致是enum uclass_id { UCLASS_ROOT 0, UCLASS_SIMPLE_BUS, UCLASS_GPIO, UCLASS_PINCTRL, UCLASS_CLK, UCLASS_SERIAL, UCLASS_MMC, UCLASS_I2C, ... };这个顺序不是随便排的。GPIO、PINCTRL、CLK这些基础服务排在前面因为很多设备依赖它们。SERIAL排在中间因为串口是调试输出越早越好但不能早于时钟。MMC、I2C这些排在后面因为它们可能依赖前面的基础服务。如果你要加自定义uclass插入位置要仔细考虑。我一般会把依赖基础服务的uclass放在CLK之后把纯应用层的放在最后。5. 常见问题与排查技巧实录5.1 串口无输出但代码明明跑了这是最经典的问题。现象是板子上电后串口一点输出都没有但用调试器看 PC 指针确实在跑。原因通常是dm_init_and_scan失败了导致串口设备没绑定serial_putc找不到设备。排查步骤确认CONFIG_DM_SERIAL是否打开。没打开的话串口走的是老式驱动不走 dm。确认设备树里串口节点的compatible跟驱动匹配。确认串口节点的status不是disabled。在dm_init_and_scan后面加printf看返回值是不是 0。我遇到过一次设备树里串口节点写的是serial10000000但reg属性写成了0x10000000 0x1000而驱动里假设的是0x100结果寄存器访问越界串口初始化失败但没报错。后来把reg改对就好了。5.2 uclass_get 返回 -ENODEV这个错误的意思是你请求的uclass不存在或者该uclass下没有设备。常见原因设备树里没有对应节点或者节点被disabled。驱动没有编译进去U_BOOT_DRIVER没被链接。uclass的id跟驱动里声明的不一致。排查时我会先用dm tree看这个uclass在不在。如果不在就去检查驱动有没有被编译。如果驱动在但设备不在就去检查设备树。5.3 probe 顺序导致的依赖问题有个坑我踩过MMC 控制器依赖一个 GPIO 做电源控制但 GPIO 的uclass优先级比 MMC 低结果 MMC probe 的时候 GPIO 还没准备好gpio_request失败。解决办法是把 GPIO 的uclass优先级调高或者在 MMC 驱动里用device_probe显式 probe GPIO 设备。提示device_probe可以强制 probe 一个设备但要注意不要造成循环依赖。dm 里有probe深度限制循环依赖会导致栈溢出。5.4 常见问题速查表现象可能原因排查方法串口无输出dm 未初始化 / 串口未绑定查dm_init_and_scan返回值uclass_get返回-ENODEVuclass 未创建 / 设备未绑定dm tree查看某外设 probe 失败依赖设备未就绪检查 uclass 优先级启动卡死probe 死循环 / 空指针加DEBUG看最后输出设备树节点被忽略status disabled检查设备树5.5 独家避坑技巧第一个技巧在board_init_r开头加一句dm_init_and_scan的返回值打印。很多人移植时直接跳过这步结果后面出问题不知道从哪查。加一句printf(dm init ret %d\n, ret)能省你半天时间。第二个技巧用dm tree命令做回归测试。每次改完设备树或驱动进命令行敲一下dm tree看设备树结构有没有变化。这比反复烧录看串口输出快得多。第三个技巧设备树里的u-boot,dm-pre-reloc要慎用。这个属性会让设备在重定位前就被扫描适合串口这种早期调试设备。但如果滥用会导致重定位前内存分配过多甚至重定位失败。我一般只给串口和时钟加这个属性。6. 驱动骨架的扩展从 board_init_r 到完整启动6.1 板级文件里的 dm 初始化board_init_r里的 dm 初始化只是骨架具体板级文件里还会做补充。比如board/你的厂商/你的板子/你的板子.c里可能有int board_init(void) { /* 板级早期初始化 */ return 0; } int board_late_init(void) { /* 板级晚期初始化此时 dm 已经就绪 */ return 0; }board_init在board_init_r早期被调用此时 dm 可能还没完全初始化。board_late_init在 dm 之后被调用可以安全地使用uclass_get和device_probe。6.2 环境变量与 dm 的关系环境变量存储设备比如 SPI Flash、MMC也是 dm 设备。env_init会通过uclass_get找到存储设备然后调用它的读写方法。如果 dm 骨架没搭好env_init就会失败表现为环境变量读写异常。我遇到过一种情况环境变量存在 MMC 里但 MMC 的uclass优先级比ENV低结果env_init时 MMC 还没 probe环境变量加载失败。解决办法是调整uclass顺序或者在env_init里显式 probe MMC 设备。6.3 从 dm 骨架看启动流程优化理解了 dm 骨架后你可以做启动优化。比如把不需要的外设uclass从uclass_probe_all里排除减少 probe 时间。把关键外设的uclass优先级调高让它们尽早 probe。用dm_scan_fdt的pre-reloc机制把串口等调试设备提前初始化。这些优化在启动时间敏感的场合很有用。我做过一个项目通过调整uclass顺序和裁剪不需要的驱动启动时间从 1.2 秒降到了 0.8 秒。6.4 一个完整的 dm 初始化检查清单最后给你一个检查清单移植新板子时按这个顺序查CONFIG_DM是否打开。CONFIG_DM_SERIAL、CONFIG_DM_MMC等具体驱动是否打开。设备树里对应节点是否存在且status为okay。compatible字符串是否跟驱动的of_match一致。dm_init_and_scan返回值是否为 0。dm tree里设备是否显示为probed。uclass_get是否能拿到对应uclass。这个清单我用了好几年每次移植新板子都按这个走基本能覆盖 90% 的 dm 相关问题。剩下的 10% 通常是硬件问题或者设备树写错了那就得具体问题具体分析了。我个人在实际操作中的体会是dm 骨架看起来复杂但核心就是“扫描-绑定-probe”三步。把这三步的日志打开顺着日志查没有查不出来的问题。最怕的是不看日志瞎猜那才是真的浪费时间。
返回列表