ARTICLE DETAIL

资讯详情

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

U-Boot设备模型解析:board_init_r中的dm骨架搭建与probe机制

U-Boot设备模型解析:board_init_r中的dm骨架搭建与probe机制 1. 从 board_init_r 看 U-Boot 设备模型的骨架搭建逻辑搞嵌入式的人对 U-Boot 都不陌生但真正把它的设备模型driver model简称 dm吃透的人其实不多。大多数人改板子的时候就是照着已有的板级文件抄一遍能跑就行至于board_init_r里那一串dm_开头的函数到底在干什么往往是模糊的。我当初也是这么过来的直到有一次调试一个 I2C 设备死活 probe 不上才被迫把这条链路从头到尾捋了一遍捋完之后发现整个 dm 骨架的设计其实非常清晰只是官方文档写得比较散。这篇内容就是把我自己踩坑之后的理解整理出来围绕board_init_r这个函数讲清楚 U-Boot 的 dm 驱动骨架到底是怎么一步步搭起来的。适合已经能编译 U-Boot、能烧录、但对 dm 内部机制还比较模糊的嵌入式工程师也适合想从传统board_init_f/board_init_r那套老式初始化流程迁移到 dm 框架的朋友。读完之后你至少能做到知道每个dm_调用发生在什么阶段、设备树里的节点是怎么变成udevice的、驱动是怎么和udevice绑定的、以及 probe 到底在什么时候被触发。先把结论摆出来U-Boot 的 dm 骨架本质上是一个两阶段扫描 延迟绑定 按需 probe的机制。board_init_r里做的事情核心就是把设备树里的节点信息先登记进来把驱动挂上去但真正让硬件动起来的 probe很多是推迟到具体驱动第一次被使用的时候才发生的。理解了这个登记和激活分离的设计后面所有的疑惑基本都能自己推出来。2. board_init_r 在启动流程里的位置与职责2.1 从 board_init_f 到 board_init_r 的交接要讲board_init_r得先知道它前面发生了什么。U-Boot 的启动分两个大阶段board_init_f和board_init_r。前者运行在重定位之前内存还没完全就绪主要干的是初始化 DRAM、串口、计算重定位地址这些生存必需的事情后者运行在重定位之后代码已经搬到 RAM 里了这时候才有条件去做那些依赖完整内存环境的事情比如设备模型的初始化。这个分界点很关键。因为 dm 框架需要动态分配内存来创建udevice、uclass、driver这些结构体如果放在board_init_f阶段内存管理器还没起来根本没法 malloc。所以 dm 的初始化天然只能放在board_init_r里。你在board_init_f里是看不到dm_init这类调用的这不是设计者偷懒而是被内存环境逼出来的必然选择。board_init_r的典型签名是void board_init_r(gd_t *new_gd, ulong dest_addr)它拿到重定位后的全局数据指针和目的地址然后开始一系列初始化。在较新的 U-Boot 版本里这个函数内部会依次调用dm_init_and_scan、initr_*系列函数、dm_scan_other等最后进入run_main_loop或者main_loop。dm 的骨架就是在这个函数的前半段搭起来的。2.2 board_init_r 里 dm 相关调用的顺序不同版本的 U-Boot 在board_init_r里的具体调用顺序会有差异但大致的骨架是稳定的。以常见的 ARM 平台为例dm 相关的调用大致按这个顺序出现dm_init_and_scan(false)—— 初始化 dm 核心并扫描设备树各种initr_*函数 —— 比如initr_env、initr_serial、initr_net等dm_scan_other(false)—— 扫描非设备树来源的设备进入主循环这里有个容易搞混的点dm_init_and_scan的参数是pre_reloc_only传false表示不只是重定位前的设备全部都要扫描。在board_init_r阶段我们传false因为这时候重定位已经完成所有设备都应该被纳入管理。注意如果你在调试时发现某个设备在board_init_r之后依然没有对应的udevice第一件事就是确认它的设备树节点有没有被正确编译进 dtb以及它的u-boot,dm-pre-reloc或u-boot,dm-spl属性是否影响了扫描范围。2.3 为什么 dm 初始化要放在这么靠前的位置有人可能会问为什么 dm 初始化要放在board_init_r的这么靠前的位置而不是等所有东西都准备好了再搞原因在于后面大量的initr_*函数本身就依赖 dm。比如initr_serial需要通过 dm 找到串口设备initr_net需要通过 dm 找到网卡设备。如果 dm 骨架没搭好这些函数就没法通过uclass_get_device之类的接口拿到设备句柄。所以顺序上必须是先搭骨架再填血肉。骨架就是dm_init建立起来的uclass链表和udevice链表血肉就是后续各个initr_*里对具体设备的 probe 和配置。这个先后关系搞反了系统直接起不来。3. dm 骨架的三大核心数据结构3.1 udevice设备的运行时代表udevice是 dm 框架里最核心的结构体一个udevice就代表一个设备实例。注意是实例不是类型。比如你有两个 I2C 控制器那就有两个udevice但它们可能共用同一个driver。udevice里几个关键字段值得记住driver—— 指向这个设备使用的驱动uclass—— 指向这个设备所属的 uclassparent—— 父设备指针体现设备树的层级plat—— 平台数据platform data来自设备树或板级代码priv—— 驱动的私有数据由驱动自己定义flags—— 状态标志比如DM_FLAG_ACTIVATED表示已经 probe 过plat和priv的区别是新手最容易搞混的。简单说plat是描述这个设备是什么的数据通常从设备树解析而来在绑定阶段就填好了priv是驱动运行过程中需要记住什么的数据在 probe 阶段才分配和初始化。打个比方plat像是设备的身份证priv像是驱动的工作笔记。3.2 uclass设备的分类管理者uclass是设备类的概念比如所有 I2C 控制器属于UCLASS_I2C所有 GPIO 属于UCLASS_GPIO。每个 uclass 有自己的操作函数集uclass_driver定义了这类设备的通用行为比如post_probe、pre_remove、child_post_bind等。uclass 存在的意义是提供一层抽象。上层代码想用一个 I2C 设备不需要知道具体是哪个厂商的控制器只需要通过uclass_get_device(UCLASS_I2C, ...)拿到一个udevice然后调用 uclass 定义的标准接口就行。这就是驱动模型解耦的核心价值。在board_init_r的 dm 初始化过程中uclass 是按需创建的。当扫描到一个设备发现它所属的 uclass 还不存在就会先创建这个 uclass把对应的uclass_driver挂上去然后再把设备挂到这个 uclass 下面。3.3 driver设备与代码的桥梁driver结构体把设备和操作代码连接起来。它里面最关键的是of_match表用来做设备树节点和驱动的匹配。当 dm 扫描到一个设备树节点时会遍历所有已注册的 driver用of_match里的compatible字符串去比对匹配上了就把这个 driver 绑定到设备上。driver里还有bind、probe、remove、unbind这几个回调。bind在绑定阶段调用负责解析设备树、填充platprobe在激活阶段调用负责真正初始化硬件、填充priv。这个 bind 和 probe 的分离就是前面说的登记和激活分离的具体体现。结构体职责创建时机关键字段udevice代表一个设备实例扫描设备树时driver, uclass, parent, plat, privuclass管理一类设备首次遇到该类设备时uclass_driver, dev_headdriver连接设备与代码编译时静态注册of_match, bind, probe4. dm_init_and_scan 的内部执行链路4.1 dm_init搭起最基础的框架dm_init_and_scan的第一步是dm_init。这个函数做的事情相对简单但很关键初始化gd-dm_root创建根设备root初始化 uclass 链表头。根设备是一个特殊的udevice它不对应任何真实硬件但作为整棵设备树的根存在所有顶级设备都是它的子设备。dm_init还会根据配置决定是否初始化dm_root的plat。在设备树里根节点/对应的就是dm_root它的compatible通常是u-boot,dm-root之类。这一步完成后dm 框架就有了一个可以挂载子设备的根。我个人的经验是如果你在调试时打印gd-dm_root发现它是 NULL那基本可以确定dm_init没有被调用或者调用失败了。这种情况常见于你自己裁剪了board_init_r但忘了保留 dm 初始化调用。4.2 dm_scan_fdt从设备树批量登记设备dm_init之后是dm_scan_fdt这是骨架搭建的主力。它会遍历设备树对每个节点尝试创建udevice。具体流程是从根节点的子节点开始递归遍历对每个节点检查它是否有compatible属性没有的话可能是纯容器节点跳过或特殊处理用compatible去匹配已注册的 driver匹配成功则创建udevice调用 driver 的bind回调把udevice挂到对应的 uclass 和父设备下这里有个细节dm_scan_fdt默认只扫描标记了u-boot,dm-pre-reloc的节点还是扫描全部节点取决于传入的参数。在board_init_r里我们传false所以是扫描全部。但在 SPL 阶段为了节省空间通常只扫描u-boot,dm-pre-reloc的节点。实操心得设备树节点的status属性如果是disableddm 扫描时会跳过它。我遇到过好几次设备 probe 不上最后发现是设备树里status忘了改成okay。这个坑很隐蔽因为编译不会报错运行时也不会有明显提示。4.3 dm_scan_platdata 与 dm_scan_other补充非设备树设备不是所有设备都来自设备树。有些老平台或者特殊设备是通过板级代码里的platdata数组静态定义的。dm_scan_platdata就是负责扫描这些静态定义的设备把它们也纳入 dm 管理。dm_scan_other则是一个弱函数允许具体平台自己实现额外的扫描逻辑。比如某些平台有动态生成的设备就可以在这里挂进去。这两个函数保证了 dm 框架的兼容性——既支持现代的纯设备树方式也支持传统的静态定义方式。4.4 扫描完成后的状态dm_init_and_scan返回时dm 骨架已经搭好了所有设备树里的设备都有了对应的udevicedriver 的bind回调都调用过了plat数据都填充好了uclass 也都创建好了。但注意这时候大部分设备的probe还没有被调用硬件还没有真正初始化。这个状态可以理解为花名册已经建好了每个人都知道自己是谁、归哪个部门管但还没开始干活。真正让设备干活要等到后续initr_*函数或者驱动使用者主动调用device_probe或uclass_get_device的时候。5. 绑定与探测bind 和 probe 的分离设计5.1 bind 阶段到底做了什么bind是设备与驱动联姻的时刻。当dm_scan_fdt找到一个匹配的设备树节点和驱动后会调用驱动的bind回调。这个回调的典型工作是解析设备树节点里的属性填充udevice-plat做一些不需要硬件访问的准备工作如果有子设备可能需要在这里处理以 I2C 控制器驱动为例bind里通常会读取reg属性拿到寄存器基地址读取clock-frequency拿到总线频率把这些存到plat里。但这时候不会去读写 I2C 寄存器因为硬件可能还没上电时钟可能还没使能。bind阶段的一个重要约束是不能做任何可能失败的硬件操作。因为bind的返回值虽然可以表示失败但失败处理比较麻烦而且bind是在扫描阶段批量调用的一个失败可能影响后续设备。所以约定俗成bind只做纯软件的准备工作。5.2 probe 阶段才是真正的硬件初始化probe是设备上岗的时刻。当某个设备第一次被使用时dm 框架会调用它的probe回调。probe里做的事情包括使能时钟、复位控制器配置寄存器分配和初始化priv数据注册子设备如果有设置DM_FLAG_ACTIVATED标志probe的触发是惰性的。也就是说如果一个设备从来没被使用过它的probe就永远不会被调用。这个设计的好处是启动快——不需要在启动时初始化所有硬件只初始化真正用到的。对于 U-Boot 这种追求快速启动的场景这个优化很有价值。但惰性 probe 也带来一个问题probe 的时机不确定调试时不好定位。我的做法是在probe函数入口加一句debug打印这样从启动日志里就能看到每个设备的 probe 顺序排查依赖问题特别有用。5.3 probe 的顺序与依赖处理probe 的顺序由依赖关系决定。如果设备 A 依赖设备 B那么使用 A 之前必须先 probe B。dm 框架通过uclass_get_device_by_phandle这类接口来解析依赖确保被依赖的设备先 probe。举个实际例子一个 I2C 温度传感器它的probe需要先通过uclass_get_device_by_phandle(UCLASS_I2C, ...)拿到它挂载的 I2C 总线设备。这个调用会触发 I2C 总线的 probe如果还没 probe 的话保证总线先就绪然后传感器才能通信。这个依赖链是自动解析的不需要手动排序。但前提是设备树里的phandle引用要写对。我踩过的坑是设备树里i2c-bus i2c1写成了i2c-bus i2c0结果传感器跑到另一条总线上去了怎么都读不到数据。5.4 手动触发 probe 的场景虽然 probe 大多是惰性的但有些设备需要在启动阶段就强制 probe。比如串口因为要用来打印日志必须在initr_serial里主动 probe。这时候会用到uclass_get_device或device_probe显式触发。uclass_get_device(UCLASS_SERIAL, 0, dev)这个调用会做两件事找到 UCLASS_SERIAL 下的第 0 个设备如果它还没 probe 就 probe 它。这个找到并激活的语义是 dm 里最常用的接口模式。注意uclass_get_device的第二个参数是设备序号不是任意 ID。序号是按设备在 uclass 里的注册顺序分配的从 0 开始。如果你有多个同类设备序号顺序可能和你想的不一样最好用uclass_get_device_by_name或uclass_get_device_by_ofnode来精确定位。6. 实操跟踪一次完整的 dm 初始化过程6.1 准备一个可调试的环境要真正理解 dm 骨架光看代码不够得动手跟踪一次。我建议用一个 QEMU 能跑的 ARM 平台比如qemu_arm或vexpress_ca9x4这样不需要真实硬件编译和运行都快。配置上打开 dm 的调试输出CONFIG_DM_DEBUGy CONFIG_DEBUG_UARTyCONFIG_DM_DEBUG会打开 dm 核心的调试打印能看到每个设备的 bind 和 probe 过程。编译后运行日志里会出现类似这样的输出dm_scan_fdt: scanning node /soc/serial10000000 dm_bind: bound driver serial_pl01x to device serial10000000 dm_probe: probing device serial10000000这些日志就是骨架搭建过程的直接证据。6.2 在关键函数下断点如果条件允许用 GDB 连上 QEMU在dm_init_and_scan、dm_scan_fdt、device_probe这几个函数下断点单步跟踪。你会看到dm_init_and_scan进入后先调dm_init创建 root 设备然后调dm_scan_fdt开始递归遍历设备树每遇到一个匹配的节点就创建udevice调bind扫描完成后返回此时gd-dm_root下已经挂了一串设备这个过程用 GDB 看一遍比读十遍代码都管用。我第一次跟的时候看到udevice一个个被创建出来挂到链表上那种原来如此的感觉很强烈。6.3 观察 probe 的触发时机继续跟踪你会发现在dm_init_and_scan返回后大部分设备还是未 probe 状态。然后进入initr_serial这时候uclass_get_device(UCLASS_SERIAL, ...)被调用串口设备的probe才被触发。再往后initr_net触发网卡 probeinitr_mmc触发 MMC 控制器 probe。每个initr_*函数负责激活一类设备。这个按需激活的模式就是 U-Boot 快速启动的关键。6.4 用 dm 命令在运行时查看状态U-Boot 命令行里有个dm命令可以查看 dm 的运行时状态。常用子命令dm tree—— 打印设备树显示所有 udevice 的层级关系dm uclass—— 按 uclass 列出设备dm devres—— 查看设备资源dm drivers—— 列出所有注册的驱动dm tree特别有用它能直观地展示骨架结构。输出类似root |-- serial10000000 |-- i2c10010000 | |-- eeprom50 |-- mmc10020000从缩进就能看出父子关系从名字能看出设备树节点路径。调试设备找不到的问题时先dm tree看一眼设备在不在在的话再看它有没有 probe基本就能定位问题。7. 常见问题与排查技巧实录7.1 设备没有出现在 dm tree 里这是最常见的问题。设备树里明明写了节点但dm tree里看不到。排查顺序确认 dtb 是否真的包含了这个节点。用fdtdump或dtc -I dtb -O dts反编译 dtb搜节点名。确认节点的status是不是okay。默认缺省是 okay但显式写了disabled就会被跳过。确认节点的compatible有没有对应的 driver。用dm drivers看已注册驱动的of_match列表。确认节点没有被u-boot,dm-pre-reloc之类的属性限制在特定阶段。我遇到过一次节点在 dtb 里status也是 okay但就是不出现在 dm tree。最后发现是compatible字符串拼错了一个字母driver 匹配不上dm 就默默跳过了。这种错误没有任何报错只能靠仔细核对。7.2 设备在 dm tree 里但 probe 失败设备登记了但 probe 报错通常是硬件初始化的问题。常见原因时钟没使能。很多 SoC 的外设需要先使能时钟才能访问寄存器如果驱动里忘了clk_get_by_indexclk_enableprobe 就会挂。复位没释放。类似时钟复位信号没释放的话寄存器访问会失败。电源域没打开。一些低功耗 SoC 有电源域控制需要先power_domain_on。寄存器地址错误。设备树里的reg和实际不符或者地址映射没建立。排查方法是在 probe 函数里加打印看走到哪一步失败。dm 框架在 probe 失败时会打印错误码结合错误码查include/linux/errno.h基本能定位方向。7.3 probe 顺序导致的依赖问题有时候单个设备 probe 没问题但组合起来就出问题这往往是依赖顺序不对。比如一个 GPIO 驱动的 probe 里需要访问 pinctrl但 pinctrl 还没 probe就会失败。dm 框架理论上会自动处理依赖但前提是依赖关系在设备树里正确表达。如果驱动里是硬编码去拿某个设备而不是通过phandle就可能绕过依赖解析导致顺序错误。解决办法是尽量用uclass_get_device_by_phandle这类接口让 dm 框架感知依赖。如果确实需要手动控制顺序可以在initr_*函数里显式按顺序 probe。7.4 常见问题速查表现象可能原因排查方法设备不在 dm treedtb 未包含节点反编译 dtb 确认设备不在 dm treestatus 为 disabled检查设备树 status 属性设备不在 dm treecompatible 无匹配驱动dm drivers 查看 of_matchprobe 失败时钟/复位/电源未就绪检查驱动初始化顺序probe 失败寄存器地址错误核对 reg 属性与手册依赖设备未就绪phandle 引用错误检查设备树引用关系probe 时机不对惰性 probe 未触发显式调用 uclass_get_device7.5 几个独家避坑技巧第一个技巧在board_init_r里 dm 初始化之后加一句dm_dump_all()如果版本支持能把当前所有设备状态打印出来。这比dm tree命令更早能在进入命令行之前就看到骨架状态。第二个技巧如果怀疑某个驱动的bind没被调用可以在bind函数入口加printk因为bind阶段printf可能还没完全就绪printk更可靠。第三个技巧设备树的u-boot,dm-pre-reloc属性会影响扫描范围。如果你在 SPL 阶段需要某个设备必须给它加这个属性如果只在 U-Boot 主体需要就不要加否则会浪费 SPL 空间。第四个技巧dm tree的输出里已经 probe 的设备和未 probe 的设备显示可能不同取决于版本。如果看不出来用dm uclass配合看或者直接在代码里检查dev-flags DM_FLAG_ACTIVATED。8. 从骨架到血肉dm 设计的取舍与启示8.1 为什么 U-Boot 要引入 dm在 dm 之前U-Boot 的设备初始化是一堆散落在板级文件里的函数调用每个板子都要写一遍类似的代码重复且容易出错。dm 引入之后设备信息集中到设备树驱动代码可以跨平台复用板级文件大幅简化。这个演进和 Linux 内核走的路是一样的。本质上是用数据驱动替代代码驱动——设备是什么写在设备树里怎么操作写在驱动里两者通过匹配机制关联。这样换一个板子只要改设备树驱动不用动。8.2 两阶段设计的代价与收益bind 和 probe 分离收益是启动快、内存省。代价是调试复杂——你得理解两个阶段各自做什么出问题时要判断是 bind 阶段还是 probe 阶段的问题。我的体会是这个代价是值得的。一旦你习惯了这种思维模式排查问题反而更有条理先确认 bind 成功设备在 dm tree 里再确认 probe 成功设备被激活两个阶段分开看问题范围立刻缩小一半。8.3 对驱动开发者的实际要求写 dm 驱动最重要的是把 bind 和 probe 的职责分清楚。bind 里只做纯软件的事probe 里才碰硬件。这个边界如果模糊了比如在 bind 里访问寄存器可能在扫描阶段就崩溃而且崩溃点很难定位。另外plat和priv的分配要用 dm 提供的接口dev_get_plat、dev_get_priv不要自己 malloc。dm 框架会统一管理这些内存自己 malloc 容易泄漏而且和框架的生命周期管理冲突。8.4 后续可以深入的方向把board_init_r这条链路捋清楚之后可以继续深入几个方向一是 uclass 的post_probe、pre_remove这些钩子的使用场景二是device_remove和unbind的流程理解设备销毁三是 SPL 阶段的 dm 裁剪看看怎么在有限空间里只保留必要的设备。还有一个很实用的方向是ofnode接口。dm 里大量用ofnode来操作设备树比直接操作fdt更安全方便。把ofnode的常用接口过一遍写驱动会顺手很多。我个人在实际操作中的体会是dm 这套东西初看复杂但它的复杂度是必要的复杂度——为了跨平台复用和快速启动这些抽象是躲不掉的。与其绕过去用老办法不如花两天时间把它吃透后面改板子、调驱动会轻松很多。踩过几次 probe 顺序的坑之后我现在拿到一块新板子第一件事就是dm tree看骨架然后顺着 probe 日志找问题基本不会再像以前那样盲目试错了。
返回列表