ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

u-boot设备模型初始化:board_init_r中的dm骨架搭建与驱动probe实战

u-boot设备模型初始化:board_init_r中的dm骨架搭建与驱动probe实战 1. 从 board_init_r 这个总装车间说起很多人第一次翻 u-boot 源码翻到board_init_r的时候都会有点懵。前面board_init_f阶段还在忙着搬内存、初始化串口、点亮第一行 log怎么一进board_init_r画风突然就变了——满屏的dm_前缀函数dm_init_and_scan、dm_scan_platdata、dm_scan_fdt_devices一个接一个往外冒。如果你只是想把板子跑起来照着现成的配置抄一抄也能过但如果你想搞清楚 u-boot 的设备模型driver model后面统一简称 dm到底是怎么在启动流程里搭骨架的那board_init_r就是那个绕不开的总装车间。我先把结论摆在前面u-boot 的 dm 驱动骨架不是在某一个函数里一次性建好的而是在board_init_r里分阶段、按顺序、层层递进地扫描—绑定—探测出来的。board_init_r本身更像一个调度中心它不负责具体某个驱动的初始化而是负责在正确的时机把 dm 的各个扫描入口依次触发让设备树、平台数据、驱动列表这三路信息汇聚成一张完整的设备-驱动关系网。这篇文章适合三类人看第一类是做 BSP 移植、经常要改board_init_r附近代码的嵌入式工程师第二类是想深入理解 u-boot dm 机制、但被各种uclass、udevice、driver绕晕的驱动开发者第三类是准备面试、需要把 u-boot 启动流程讲清楚的同学。我会尽量用总装车间这个类比贯穿全文把抽象的 dm 概念落到具体的代码路径上让你看完之后能自己画出这张骨架图。在展开之前先明确一个前提不同版本的 u-bootboard_init_r的具体实现差异不小。我下面讲的内容以较新的 dm 完整版本大致 2018 年之后的主流版本为基准老版本可能没有dm_init_and_scan这种统一入口而是散落在各个init_sequence_r里。你对照自己手上的代码时先确认版本别硬套。2. board_init_r 里 dm 骨架的三次关键扫描2.1 为什么 dm 初始化要放在 board_init_r 而不是 board_init_f这个问题我当年也纠结过。board_init_f阶段连 DDR 都还没初始化完代码还在只读的 SRAM 或者临时栈上跑这时候去搞 dm 扫描内存分配、设备树解析全都没法正常进行。dm 的核心操作——分配udevice、解析ofnode、绑定driver——都需要可写的堆内存和完整的 malloc 机制而这些恰恰是board_init_f阶段给不了的。所以 u-boot 的设计是board_init_f只做最基础的、必须在重定位前完成的事情比如串口、时钟、部分 pinmux把 dm 的完整初始化推迟到board_init_r。到了board_init_r内存已经重定位完毕gd-malloc_base可用设备树也已经从存储介质里读进了内存这时候再搭 dm 骨架时机刚刚好。提示如果你在board_init_f阶段就想用某个 dm 设备通常会失败或者拿到一个未 probe 的空壳。正确做法是用DM_FLAG_PRE_RELOC标记那些确实需要提前初始化的驱动让它们在重定位前被特殊处理。2.2 dm_init_and_scan骨架搭建的总入口在board_init_r里dm 骨架搭建的核心入口就是dm_init_and_scan。这个函数名字起得很直白——init 加 scan。它内部大致做了这么几件事第一调用dm_init初始化 dm 的全局根设备gd-dm_root建立最顶层的uclass根节点。你可以理解为先把总装车间的总电闸合上让整个 dm 框架有一个可以挂载的根。第二调用dm_scan_platdata扫描平台数据platdata。这是给那些没有设备树、或者设备树信息不全的板子准备的兜底路径。很多老平台或者简单外设驱动信息是硬编码在U_BOOT_DEVICE宏里的这一步就是把这些静态定义的设备注册进 dm。第三调用dm_scan_fdt_devices在支持设备树的配置下扫描设备树里的设备节点。这是现代 u-boot 的主路径绝大多数设备信息都来自.dts文件。它会遍历设备树把每个有compatible属性的节点转换成对应的udevice并尝试匹配driver。第四调用dm_scan_other给板级代码留一个自定义扫描的钩子。有些板子有特殊的设备来源比如从 EEPROM 读出来的配置可以在这里补充。这四步走完dm 的设备清单和驱动清单基本就建立起来了但注意——这时候大部分设备只是被绑定bind还没有被探测probe。绑定只是建立了 udevice 和 driver 的关联真正的硬件初始化比如配置寄存器、申请 GPIO要等到 probe 阶段。2.3 从 bind 到 probe骨架上的设备什么时候真正活过来这是很多人容易混淆的地方。dm_init_and_scan跑完之后你用dm_dump_all去看会发现一大堆设备状态是not probed。这不是 bug而是 dm 的惰性初始化设计。probe 的触发有两种典型方式一种是按需 probe。当某个驱动调用uclass_get_device或者device_get_by_ofnode去获取一个设备时dm 会检查这个设备是否已经 probe如果没有就现场触发它的probe回调。这种方式的好处是启动快用不到的设备不初始化。另一种是主动 probe。在board_init_r的后半段会有一个init_sequence_r数组里面有一项initr_dm_devices或者类似名字视版本而定它会遍历所有标记了DM_FLAG_ACTIVE_DMA或者需要在启动阶段就绪的设备主动 probe 它们。串口、网口、MMC 这些启动关键路径上的设备通常就是在这里被拉起来的。我个人的经验是调试 dm 问题时先分清是 bind 阶段出错还是 probe 阶段出错。bind 出错通常是compatible字符串对不上、driver 没注册probe 出错则多半是硬件资源时钟、GPIO、电源域没准备好或者 probe 顺序有依赖。这两类问题的排查思路完全不同混在一起查会浪费大量时间。3. udevice、uclass、driver 三者的绑定关系是怎么建立的3.1 用车间—工位—工人类比理解 dm 三件套dm 里最核心的三个结构体是udevice、uclass、driver。官方文档讲得很抽象我用总装车间的类比给你翻译一下driver是工人它知道怎么干活probe、remove、ops但它本身不知道自己要装在哪条产线上。udevice是工位它代表一个具体的设备实例有地址、有资源、有状态。uclass是车间它把干同类活的工位组织在一起提供统一的调度接口比如所有 GPIO 工位归 GPIO 车间管。绑定关系是这样的一个udevice在 bind 时会通过compatible或者driver name找到对应的driver同时根据 driver 声明的uclass id挂到对应的uclass下面。这样当你通过 uclass 接口去操作设备时uclass 就能找到挂在自己名下的 udevice再通过 udevice 找到 driver 的 ops 去执行。3.2 bind 阶段compatible 匹配与 driver 查找的完整链路bind 的核心逻辑在device_bind_common里。当你从设备树扫描到一个节点时dm 会做这么几件事首先读取节点的compatible属性。这个属性可能是一个字符串列表dm 会逐个尝试匹配已注册的 driver。匹配的依据是 driver 的of_match表里面列出了这个 driver 支持的所有 compatible 字符串。其次如果 compatible 匹配成功dm 会分配一个udevice结构体把 driver 指针、设备树节点ofnode、父设备等信息填进去。如果这个 driver 属于某个 uclass还会把 udevice 挂到该 uclass 的设备链表上。然后如果 driver 定义了bind回调会在这里调用。bind 回调通常用来做一些轻量的初始化比如解析设备树里的私有属性、分配私有数据结构。注意 bind 阶段不应该去操作硬件寄存器因为这时候设备可能还没上电或者时钟还没开。最后如果设备有子节点dm 会递归地为子节点也执行 bind。这就是为什么设备树里的层级结构能直接映射成 dm 里的父子设备关系。注意compatible 匹配是大小写敏感的而且必须完全一致。我见过有人把vendor,device写成vendor,Device结果 bind 一直失败查了半天才发现是大小写问题。这种坑很隐蔽建议用dm_dump_all或者打开DEBUG级别的 dm 日志来确认匹配过程。3.3 uclass 的自动创建与 driver 的注册时机uclass 不是凭空出现的它也需要被创建。uclass 的创建有两种途径一种是自动创建。当第一个属于某个 uclass id 的设备被 bind 时如果这个 uclass 还不存在dm 会自动调用uclass_add创建一个。这依赖UCLASS_DRIVER宏注册的 uclass driver里面定义了 uclass 的名字、id、以及可选的post_bind、pre_probe等回调。另一种是显式创建。某些核心 uclass比如 root、sysreset会在dm_init阶段就被提前创建好因为它们是整个 dm 框架的基础。driver 的注册则依赖U_BOOT_DRIVER宏。这个宏会把 driver 结构体放到一个特殊的链接段.u_boot_list里启动时 dm 通过遍历这个段就能拿到所有已注册的 driver。这种链接段收集的手法在 u-boot 里很常见好处是不需要手动维护一张 driver 注册表新增驱动只要写宏就行。我踩过的一个坑是driver 的name字段必须全局唯一。如果你两个 driver 起了同一个名字链接时不会报错但运行时 dm 查找 driver 可能会拿到错误的那个导致 probe 行为诡异。这个问题的排查成本很高因为现象往往不是直接崩溃而是某个设备行为不对。建议在新增 driver 时用grep全局搜一下名字有没有重复。4. 设备树扫描在 dm 骨架里的具体作用路径4.1 dm_scan_fdt_devices 如何遍历设备树节点dm_scan_fdt_devices是设备树路径的核心。它的逻辑其实不复杂从设备树的根节点开始递归遍历所有子节点对每个节点调用dm_scan_fdt_node。dm_scan_fdt_node会做几个判断第一检查节点是否有compatible属性。没有 compatible 的节点通常只是容器节点比如soc { }这种纯层级节点不需要绑定设备但它的子节点还是要继续遍历。第二检查节点的status属性。如果是disabled或者fail就跳过这个节点及其子节点。这是设备树的标准机制用来在板级配置里裁剪不需要的设备。第三检查节点是否已经被绑定过。dm 会维护一个已绑定节点的记录避免重复绑定。这在有多个扫描入口platdata 和 fdt 同时存在时很重要。第四调用lists_bind_fdt尝试匹配并绑定。如果匹配到 driver就创建 udevice如果没匹配到默认情况下只是打印一条 debug 日志不会报错。这一点很关键——设备树里有节点但没驱动u-boot 不会因此启动失败只是那个设备用不了。4.2 ofnode 与 udevice 的映射关系维护设备树节点在 dm 里用ofnode表示它本质上是一个指向设备树节点的句柄。每个从设备树绑定来的udevice都会保存自己的ofnode这样驱动在 probe 时就能通过dev_ofnode(dev)拿到节点去读取自己需要的属性。这个映射关系是双向的从 udevice 可以拿到 ofnode从 ofnode 也可以通过ofnode_to_dev之类的接口反查 udevice。后者在中断控制器、时钟树这种需要从设备树节点找设备的场景里特别有用。我遇到过一个典型问题某个驱动在 probe 时用ofnode去读属性结果读出来是空。排查后发现是因为这个设备在设备树里被放在了错误的父节点下导致ofnode的路径不对。设备树的层级结构不是随便写的它会影响 dm 的父子关系和 probe 顺序。父设备先于子设备 probe是 dm 的一条基本规则如果你把有依赖关系的设备放错了层级probe 顺序就会乱。4.3 设备树里 status 属性对骨架裁剪的影响status属性是设备树裁剪的开关。在 dm 扫描时status disabled的节点会被整个跳过包括它的子节点。这个机制在多板共用一个 dtsi 的场景下非常有用——你可以把 SoC 通用的外设都写在 dtsi 里然后在具体板子的 dts 里用status开关来裁剪。但这里有个容易忽略的点u-boot 和 Linux 对 status 的处理基本一致但 u-boot 的 dm 扫描发生在很早的阶段。如果你的设备树里某个节点依赖另一个节点的初始化结果而那个节点被 disabled 了就会导致 probe 失败。我建议在调试阶段先用dm_dump_all确认实际被绑定的设备列表再对照设备树看哪些节点被裁掉了这样能快速定位设备树里明明有但 dm 里找不到的问题。另外u-boot,dm-pre-reloc这个属性值得单独提一下。它标记的设备会在重定位前就被绑定和 probe用于那些board_init_f阶段就要用的设备比如调试串口。这个属性用错了会导致启动早期崩溃用少了又会导致早期 log 出不来需要根据实际需求权衡。5. 驱动 probe 顺序与依赖处理的实战经验5.1 probe 顺序由什么决定probe 顺序是 dm 调试里最让人头疼的问题之一。它主要由三个因素决定第一设备树的层级。父设备总是先于子设备 probe。这是 dm 的硬性规则因为子设备可能需要访问父设备的资源。第二uclass 的pre_probe和post_probe回调。某些 uclass 会定义这些回调用来在设备 probe 前后做一些统一处理。比如 clock uclass 可能会在 pre_probe 里确保时钟源已经就绪。第三u-boot,dm-pre-reloc标记。带这个标记的设备会在早期阶段优先 probe不受常规顺序约束。除此之外dm 本身不保证同一层级、同一 uclass 下设备的 probe 顺序。如果你有两个设备有依赖关系不能指望它们按设备树里的书写顺序 probe。正确做法是用depends或者显式地在驱动里调用device_get_by_ofnode来触发依赖设备的 probe。5.2 用 depends 和 uclass 回调解决依赖depends是 u-boot dm 提供的一个依赖声明机制。在 driver 结构体里你可以用DM_DRIVER_ALIAS或者相关的宏来声明我这个驱动依赖某个 uclass 或某个具体设备。dm 在 probe 时会先确保依赖项已经 probe 完成。不过说实话depends在实际项目里用得不算多因为它的表达能力有限而且容易和 probe 顺序的其他规则冲突。更常见的做法是在 probe 函数里显式获取依赖设备static int my_driver_probe(struct udevice *dev) { struct udevice *clk; int ret; ret clk_get_by_index(dev, 0, clk); if (ret) { dev_err(dev, failed to get clock: %d\n, ret); return ret; } /* 此时 clk 已经被 probe 完成可以安全使用 */ ret clk_enable(clk); ... }这种写法的好处是依赖关系明确、可读性强而且clk_get_by_index内部会自动触发 clock 设备的 probe。缺点是如果依赖链很深可能会导致 probe 递归过深需要留意栈空间。5.3 probe 失败的常见原因与排查手法probe 失败的原因五花八门我按出现频率排个序失败原因典型现象排查手法时钟未就绪probe 返回 -ENODEV 或超时检查 clock 节点 status、确认 clk_get 返回值GPIO 申请失败probe 返回 -EBUSY用 gpio_request 的返回值定位检查 pinmux 冲突电源域未开寄存器读写全 0 或全 F确认 power domain 驱动是否 probe、regulator 是否 enable设备树属性缺失probe 里读属性返回空用 ofnode 逐属性打印对照 dts 确认compatible 不匹配设备根本没被 bind打开 dm debug 日志确认匹配过程驱动 name 冲突行为诡异、偶发失败grep 全局搜 driver name 是否重复我个人的排查习惯是先在 probe 函数入口加一条 dev_info确认它有没有被调用。如果没被调用问题在 bind 阶段如果被调用了但返回错误问题在 probe 内部。这一步能砍掉一半的无效排查。还有一个技巧是善用dm tree命令如果板子的 u-boot 支持命令行。它能以树状结构打印所有设备和 uclass直观展示父子关系和 probe 状态。比dm_dump_all的平铺列表好用得多。6. 从零搭建一个能被 board_init_r 正确识别的 dm 驱动6.1 驱动骨架的最小组成一个能被 dm 正确识别的最小驱动需要这么几个部分/* 1. 私有数据结构 */ struct my_dev_priv { void __iomem *base; struct clk clk; }; /* 2. 驱动 ops */ static int my_dev_of_to_plat(struct udevice *dev) { struct my_dev_priv *priv dev_get_priv(dev); /* 解析设备树填充 priv */ priv-base dev_read_addr_ptr(dev); return 0; } static int my_dev_probe(struct udevice *dev) { struct my_dev_priv *priv dev_get_priv(dev); /* 硬件初始化 */ return 0; } /* 3. of_match 表 */ static const struct udevice_id my_dev_ids[] { { .compatible vendor,my-device }, { } }; /* 4. driver 结构体 */ U_BOOT_DRIVER(my_dev) { .name my_dev, .id UCLASS_MISC, .of_match my_dev_ids, .of_to_plat my_dev_of_to_plat, .probe my_dev_probe, .priv_auto sizeof(struct my_dev_priv), };这里有几个关键点of_to_plat是较新版本 u-boot 引入的回调专门用来把设备树信息转换成平台数据。老版本可能直接在 probe 里做这件事。它的好处是职责分离——解析设备树和初始化硬件分开便于测试和复用。priv_auto让 dm 自动分配私有数据空间省去了手动 malloc 的麻烦。注意它的单位是字节dm 会在 bind 时自动分配。id字段指定这个驱动属于哪个 uclass。如果你不确定该用哪个UCLASS_MISC是个万能兜底但更好的做法是找到最贴切的 uclass这样能复用 uclass 提供的通用接口。6.2 设备树节点的正确写法驱动写好了设备树节点也得配对my_device: my-device10000000 { compatible vendor,my-device; reg 0x10000000 0x1000; clocks clk_my_dev; status okay; };compatible必须和驱动里的of_match完全一致。reg提供寄存器基地址和长度dev_read_addr_ptr会读这个属性。clocks是时钟引用驱动里用clk_get_by_index获取。如果你的设备有中断还要加interrupts和interrupt-parent有 GPIO 就加gpios。这些标准属性 dm 都有对应的读取接口不用自己解析。提示设备树节点名my-device10000000里的my-device部分建议和 compatible 的后半段保持一致这是设备树的标准约定便于阅读和维护。虽然 dm 不强制要求但养成好习惯能省很多事。6.3 验证驱动是否被正确 bind 和 probe驱动写完后怎么确认它被 dm 正确识别了我通常按这个顺序验证第一步编译后检查.u_boot_list段里有没有你的 driver。可以用nm或者objdump看符号确认U_BOOT_DRIVER宏生成的符号存在。第二步启动时打开 dm 的 debug 日志。在board_init_r之前设置gd-flags | GD_FLG_DM_DEBUG或者在配置里打开CONFIG_DM_DEBUG。这样能看到 bind 和 probe 的详细过程。第三步用dm_dump_all或dm tree确认设备出现在列表里且状态是 probed。第四步在 probe 函数里加dev_info确认它被调用了且返回值是 0。这四步走下来基本能覆盖 90% 的驱动不工作问题。剩下的 10% 通常是硬件层面的问题比如寄存器地址写错、时钟频率不对那就需要上示波器或者逻辑分析仪了。7. 几个我踩过的 dm 骨架相关坑7.1 设备树节点放错层级导致 probe 顺序错乱这个坑我在一个电源管理芯片的驱动上踩过。当时把 PMIC 节点放在了 I2C 控制器节点的同级而不是子级。结果 PMIC 的 probe 早于 I2C 控制器导致 I2C 通信全部失败。dm 的父子关系直接映射设备树的层级。有依赖关系的设备子设备必须放在父设备节点下面。PMIC 挂在 I2C 上就应该写成 I2C 节点的子节点。这样 dm 会先 probe I2C 控制器再 probe PMIC顺序天然正确。排查这个问题的关键是看dm tree的输出。如果发现某个设备的父设备不是预期的那个基本就能定位到设备树层级写错了。7.2 uclass id 冲突引发的诡异行为uclass id 是 dm 用来区分不同类别设备的标识。u-boot 预定义了一批标准 idUCLASS_GPIO、UCLASS_I2C等也允许自定义 id。自定义 id 必须从UCLASS_PRIVATE之后开始否则会和标准 id 冲突。我见过一个项目两个不同的驱动用了同一个自定义 uclass id结果 dm 把它们当成同一类设备管理导致uclass_get_device拿到的设备时对时错。这种问题的现象非常随机排查起来很痛苦。避免方法很简单自定义 uclass id 统一定义在一个头文件里用UCLASS_PRIVATE作为起始偏移并且加注释说明每个 id 的用途。新增时先 grep 确认没重复。7.3 probe 函数里做太重的事情导致启动变慢dm 的惰性 probe 设计本来是为了加快启动但如果你在 probe 里做了太多事情比如扫描整个 I2C 总线、读取大块 EEPROM启动时间就会明显变长。我的建议是probe 只做让设备可用的最小初始化把耗时的操作推迟到实际使用时。比如网口驱动probe 里只初始化 MAC 控制器和 PHY 的基本寄存器真正的链路协商放到start回调里。这样即使网口在启动阶段没被用到也不会拖慢启动。如果确实需要在启动阶段初始化某个耗时设备可以考虑用DM_FLAG_ACTIVE_DMA之类的标记控制它的 probe 时机或者干脆放到board_init_r之后的板级代码里手动触发。7.4 忽略 of_to_plat 和 probe 的职责边界of_to_plat和probe的职责边界官方文档讲得比较清楚of_to_plat负责把设备树信息转换成平台数据probe负责硬件初始化。但实际写代码时很容易混。我见过有人在of_to_plat里就去读写寄存器结果因为时钟还没开而失败。也见过有人在probe里重新解析设备树浪费了of_to_plat已经做好的工作。正确的做法是of_to_plat只做纯数据转换不碰硬件probe只做硬件初始化不再解析设备树。这样职责清晰也便于单元测试——你可以单独测试of_to_plat的转换逻辑不需要真实硬件。8. 把 dm 骨架图刻在脑子里回到最开始的那个类比board_init_r是总装车间dm_init_and_scan是总装指令udevice、uclass、driver是工位、车间和工人设备树是图纸probe 是真正开工。这套骨架搭好之后u-boot 的整个驱动体系就能有条不紊地运转起来。我个人的体会是理解 dm 骨架最好的方式不是死记函数调用顺序而是自己动手加一个驱动然后顺着 bind 和 probe 的路径走一遍。你会遇到 compatible 不匹配、probe 顺序不对、uclass id 冲突这些真实问题解决它们的过程比看十遍文档都管用。最后分享一个实用技巧在board_init_r里dm_init_and_scan调用前后各加一条dm_dump_all对比两次输出你就能直观看到 dm 骨架是怎么从无到有建立起来的。这个方法我在带新人的时候屡试不爽比任何架构图都直观。
返回列表