ARTICLE DETAIL

资讯详情

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

u-boot设备模型dm框架解析:从board_init_r到device_probe

u-boot设备模型dm框架解析:从board_init_r到device_probe 搞过板级移植的都知道u-boot这两年最绕不开的变化就是设备模型Driver Model简称dm。不管你是调网卡、调GPIO还是调串口最后都会撞到board_init_r里那两条初始化函数initr_dm和initr_dm_devices。我刚接触dm时也懵过一阵子一堆结构体来回指UCLASS_DRIVER和U_BOOT_DRIVER傻傻分不清楚更搞不明白bind和probe之间到底差几步。后来把源码从drivers/core一路啃下去又实际在板子上折腾了几个月才算把这条线捋顺。这篇东西与其说是教程不如说是我把board_init_r到device_probe之间那根“驱动骨架”拆开揉碎的记录适合正在看u-boot驱动、想搞懂dm初始化链路的同学。1. 为什么u-boot要把驱动全部收编进dm从“到处手动init”到“一条链子管到底”1.1 旧式驱动模型的痛点到底是什么在dm引入之前u-boot的驱动写法很“原始”。你想用某个外设就得在board_init_r里的初始化序列中手动注册一堆init函数比如i2c_init()、serial_init()、spi_init()而且调用顺序必须严格按照硬件时序来。这种写法有几个问题每次移植新板卡板级文件就成了一个巨型杂物间所有外设初始化堆在一起维护成本极高。驱动和具体硬件强耦合。换个地址、换个引脚就得改驱动代码有时候甚至要复制整个文件再改。设备的依赖关系比如I2C需要先有时钟、GPIO脚要先配好方向根本表达不出来只能靠“排序”硬撑。设备树device tree在u-boot里逐渐普及后代码里写死的初始化逻辑和设备树描述的硬件信息完全脱节两边经常对不上。dm的核心思路就是把这些散落的初始化逻辑统一成一个“框架”驱动只负责描述自己支持什么、怎么操作硬件设备树节点负责描述硬件长什么样、挂在哪个总线上框架负责把它们匹配起来并在合适的时候自动probe。这样多个平台复用一套驱动板级代码里不再堆初始化函数而是靠dts描述硬件。1.2 dm三个核心结构体uclass、driver、udevice各管哪一摊理解dm骨架最关键的就是三个结构体。你可以把uclass想象成“部门”把driver想象成“员工的技能模板”把udevice想象成“具体上岗的那个人”。uclassinclude/dm/uclass.h同类设备的集合。GPIO是UCLASS_GPIO串口是UCLASS_SERIALI2C是UCLASS_I2C。所有同类设备的udevice都会挂到同一个uclass的链表上。uclass还定义了一套统一的接口操作表ops上层代码通过它访问设备不用关心底层是哪个厂商的芯片。driverinclude/dm/driver.h就是驱动本身。它描述驱动名字、属于哪个uclass、支持哪些设备树compatible匹配项、bind回调、probe回调、需要多大私有数据等。每个驱动在编译链接时会被一个宏放到特殊的段里运行时就能被框架找到。udeviceinclude/dm/device.h一个具体的设备实例。它对应设备树里的一个节点或者平台数据里的一个条目内部保存父设备指针、所属uclass、绑定的driver、平台数据、私有数据、seq编号等信息。你可以把udevice理解为“这个硬件节点在u-boot里的身份档案”。这三者的关系是一对一组合的一个udevice必须绑定一个driver同时归属一个uclass。driver描述能力uclass描述归属类别udevice描述具体实例。初始化骨架的本质就是把设备树节点变成udevice再把udevice和合适的driver、uclass关联起来。1.3 UCLASS_DRIVER和U_BOOT_DRIVER这对“双黄蛋”怎么区分很多人分不清这两个宏。简单说U_BOOT_DRIVER定义一个具体设备驱动。比如“这个I2C控制器是designware的”它就是针对某个硬件IP的驱动。UCLASS_DRIVER定义一个uclass的“类驱动”。它不直接操作硬件而是定义这一类设备的共有框架行为例如所有GPIO uclass都有一个post_probe回调用于在探测之后做统一的初始化。举个例子你在SDK里定义一个U_BOOT_DRIVER(gpio_sandbox)这是具体驱动同时你还会看到UCLASS_DRIVER(gpio)这是uclass骨架。框架在绑定驱动时会根据驱动的id字段去找对应的uclass_driver然后把udevice挂到那个uclass下。两个宏都用ll_entry_declare把一个结构体放进链接段只是放进的段不同。前者进.u_boot_driver段后者进.u_boot_uclass段。2. board_init_r里的dm骨架搭建流程从initr_dm到dm_init_and_scan2.1 初始化序列中的两个关键函数u-boot在重定位之后会进入漫长而繁琐的板级初始化序列init_sequence_r[]这个数组在common/board_r.c里定义。dm相关的初始化在序列中至少有两个关键位置static init_fnc_t init_sequence_r[] { initr_trace, initr_reloc, ... initr_dm, // 第一阶段dm核心初始化 ... initr_dm_devices, // 第二阶段扫描设备树并绑定设备 ... };这两个函数分别做了什么看代码更清楚static int initr_dm(void) { int ret; if (IS_ENABLED(CONFIG_DM)) { ret dm_init(); if (ret) { debug(Error initializing DM: %d\n, ret); return ret; } } return 0; } static int initr_dm_devices(void) { int ret; if (IS_ENABLED(CONFIG_DM)) { ret dm_init_and_scan(); if (ret) { debug(Error scanning devices: %d\n, ret); return ret; } } return 0; }注意版本差异在新一点的内核中initr_dm_devices()里实际调的是dm_extended_scan_fdt(true)这个过程会先补调dm_init()再做扫描。不同版本函数名有差异但骨架思路一致——先搭核心框架再扫描设备。2.2 dm_init()内部root driver和root设备是怎么来的dm_init()做的事情说白了就是“初始化根框架”。它位于drivers/core/root.c相关代码链路中核心调用是device_bind_common()int dm_init(void) { int ret; struct udevice *root NULL; if (gd-dm_root) return -EINVAL; ret device_bind_common(NULL, root_driver, root_driver, NULL, NULL, 0, root); if (ret) return ret; gd-dm_root root; if (IS_ENABLED(CONFIG_DM_DEVICE_REMOVE)) root-flags | DM_FLAG_ACTIVATED; return 0; }流程很清晰先把一个名为root_driver的驱动绑定成一个udevice这个udevice就是“根设备”它的parent是NULL是整个设备树的根部。之后所有设备都是它的子孙节点。root_driver定义在drivers/core/root.c里非常精简U_BOOT_DRIVER(root_driver) { .name root_driver, .id UCLASS_ROOT, }; UCLASS_DRIVER(root) { .name root, .id UCLASS_ROOT, };这一步相当于给整个dm世界创建了“上帝视角的0号设备”。有了gd-dm_root后续扫描出来的所有udevice就能通过parent指针形成一棵树这棵树就是调试时看到的设备树拓扑。2.3 dm_scan从设备树到udevice的递归旅程真正把设备树节点变成udevice是在dm_init_and_scan()里完成的。这个函数先确保dm_init()已执行然后调用dm_scan()int dm_init_and_scan(bool pre_reloc_only) { int ret; ret dm_init(); if (ret) return ret; ret dm_scan(pre_reloc_only); if (ret) return ret; return 0; }dm_scan()的职责是决定从哪些来源创建设备。在现代u-boot里最重要来源就是设备树int dm_scan(bool pre_reloc_only) { int ret; if (CONFIG_IS_ENABLED(OF_CONTROL)) { ret dm_scan_fdt_node(gd-dm_root, gd-fdt_blob, 0, pre_reloc_only); if (ret) return ret; } return dm_scan_platdata(pre_reloc_only); }dm_scan_fdt_node()是递归函数它会遍历设备树的每个子节点。对每个节点先尝试查找一个匹配的driver如果找到就调用lists_bind_fdt()完成绑定绑定成功后继续处理该节点的子节点。整个过程相当于把DTB里的树形结构在内存里映射成一颗udevice树。这里要注意一个前提dm扫描依赖gd-fdt_blob已经指向有效DTB。如果你看到dm扫描为空或者驱动没绑上先怀疑一下fdt才指针到底是不是有效地址这是个很常见的低级坑。3. 绑核机制拆解bind、probe、uclass的运作原理3.1 绑定阶段设备树节点怎么找到自己的driver绑定是“设备树节点 ↔ driver”之间的配对。核心函数是lists_bind_fdt()它的大致逻辑读取节点compatible属性字符串。遍历.u_boot_driver段里所有已注册driver。对每个driver用它的of_match表和节点compatible逐一比对。匹配成功就调用device_bind_common()创建udevice。device_bind_common()是绑定阶段真正的“心脏”。它做的事情包括分配struct udevice内存。按driver的priv_auto、platdata_auto、ops_auto为设备预留私有数据、平台数据、操作表。初始化设备的链表节点。调用driver的.bind回调让驱动自己有机会做预处理比如解析reg地址。把udevice挂到父设备的child_head链表。把udevice挂到uclass的dev_head链表。设置设备的seq编号。绑定完成后我们手里有一个“已经分配好内存、挂好链、和driver配对成功”的udevice但它还没有真正初始化硬件。注意bind阶段不会访问硬件寄存器这是它和probe最大的区别。3.2 探测阶段probe为什么是递归的device_probe()是驱动真正上电、配置寄存器的地方。它的调用链大体是int device_probe(struct udevice *dev) { ... /* 如果父设备还没probe先递归probe父设备 */ if (dev-parent !(dev-parent-flags DM_FLAG_ACTIVATED)) { ret device_probe(dev-parent); ... } /* 分配平台数据 */ if (dev-driver-platdata_auto) { ... } /* 调用驱动自身的probe */ if (dev-driver-probe) { ret dev-driver-probe(dev); ... } /* 调用uclass的post_probe */ if (uc-uc_drv-post_probe) { ret uc-uc_drv-post_probe(dev); ... } dev-flags | DM_FLAG_ACTIVATED; ... }为什么probe必须是递归的因为很多设备有父子依赖关系。比如一个I2C控制器要先probe挂在它下面的温度传感器才能probe。如果先probe子设备寄存器和时钟可能根本没准备好。所以框架里强制要求子设备probe前父设备必须先被probe成功。还有一个细节经常被人忽略同一个驱动可能probe多个实例。比如两颗一样的LED芯片设备树里出现两个节点driver只写一份但会有两个udevice每个都调用同一份probe代码靠dev指针区分硬件实例。这就是驱动和实例分离的思想。3.3 uclass如何统一管理同类型设备当udevice被绑定到driver后框架会通过driver的id字段找到对应的uclass然后把udevice挂到uclass-dev_head链表。uclass的价值在于提供“统一访问接口”。以GPIO为例所有GPIO驱动都提供一套struct dm_gpio_ops操作表如set_value、get_value等。上层代码先用uclass_get_device(UCLASS_GPIO, ...)拿到某个GPIO设备的udevice。然后通过gpio_set_value()这类封装函数调用dev_get_ops(dev)-set_value(dev, offset, value)。这样上层代码完全不需要知道具体厂商芯片的寄存器地址和操作方式只要uclass接口定好底层驱动遵循同一套ops就行了。它和Linux设备模型里的subsystem/class概念几乎一模一样只是u-boot做得更精简。还有一点值得提uclass_driver的post_probe回调可以在这个uclass下每个设备probe完成后统一做一些事情。比如motor uclass会在每个电机驱动probe后自动启用PWM输出这种“类级别的初始化钩子”非常实用。4. 实操手写一个设备树驱动的完整步骤4.1 设备树节点与compatible设计我们以“给一块自定义板加一个假的LED控制器”为例。其实硬件可以非常简单关键是走通dm那条链。先在设备树里加节点my_fake_led: my-fake-led1000 { compatible vendor,my-fake-led; reg 0x1000 0x4; label core-led; status okay; };这里有几个硬性要求compatible字符串必须和驱动里的of_match完全一致字符串模糊或者大小写不一致都会导致匹配失败。status okay一定要确认。很多板子节点写着status disabled驱动再怎么对也绑不上。reg属性按需定义驱动里可以通过dev_read_addr()解析。4.2 驱动代码结构与U_BOOT_DRIVER宏驱动本体放哪都行常见位置是drivers/misc/或者你自己加的板级驱动目录。核心代码如下#include dm.h #include asm/io.h struct my_fake_led_priv { ulong base; /* 寄存器基址 */ int off; /* 当前开关状态 */ }; static int my_fake_led_probe(struct udevice *dev) { struct my_fake_led_priv *priv dev_get_priv(dev); /* 从设备树读reg地址 */ priv-base dev_read_addr(dev); if (priv-base FDT_ADDR_T_NONE) return -EINVAL; priv-off 0; printf(my_fake_led: probed at 0x%lx\n, priv-base); return 0; } static const struct udevice_id my_fake_led_ids[] { { .compatible vendor,my-fake-led }, { } }; U_BOOT_DRIVER(my_fake_led) { .name my_fake_led, .id UCLASS_MISC, .of_match my_fake_led_ids, .probe my_fake_led_probe, .priv_auto sizeof(struct my_fake_led_priv), };要点.id UCLASS_MISC把驱动归入misc这个uclass省得自定义一个uclass。如果你想把设备归入某个具体类别比如UCLASS_GPIO就得实现对应ops。.priv_auto框架会自动为每个udevice分配私有内存probe里通过dev_get_priv(dev)拿到。千万不要自己用malloc去存“驱动私有状态”框架的分配会随设备生命周期自动释放。如果驱动依赖probe前的预处理可以在.bind回调里做大多数简单驱动不用bind。4.3 编译进固件并用dm tree验证把驱动文件加入Makefile。比如放在drivers/misc/Makefile就加一行obj-$(CONFIG_MY_FAKE_LED) my_fake_led.o在defconfig里打开CONFIG_MY_FAKE_LEDy重新编译烧录后进入u-boot命令行敲最管用的诊断命令 dm tree my_fake_led1000 misc my_fake_led [ ] my_fake_led1000输出里从左到右依次是设备名、uclass、driver、probe状态、平台数据描述。[ ]表示设备probe成功如果是[ - ]则说明驱动绑上了但probe失败或者还没有被要求probe。再看uclass列表 dm uclass misc my_fake_led1000如果这两条命令输出正常就说明从设备树到udevice再到driver的整条骨架已经通了。5. 常见问题与排查技巧实录5.1 用dm命令快速判断骨架是否搭对u-boot dm命令是排查的第一利器。除了dm tree和dm uclass还有几个高频用法dm tree看整棵设备树的绑定和probe状态。这是最直观的骨架体检。dm uclass按uclass分组看设备适合排查同类设备都挂在谁下面。dm devres看设备动态内存使用情况排查泄漏或资源未释放。dm dump某些版本支持把当前设备的platdata、priv指针信息打出来。如果你在u-boot启动日志里加CONFIG_DM_DEBUGy还能看到更详细的bind/probe调用流。实测下来板子外设起不来时先跑dm tree比盲改驱动效率高太多。5.2 三个高频踩坑案例与解决思路设备树里节点有dm tree里就是看不到。大概率是fdt_blob有问题先确认dtb有没有正确加载并被fdt_chosen等机制处理。或者你的驱动没有编译进去直接在编译时检查.u_boot_driver段符号有没有出现。compatible明明写对了但驱动没绑上。检查驱动文件里of_match表的结尾是否有空结构体很多驱动漏写这个不可见的终止项static const struct udevice_id my_fake_led_ids[] { { .compatible vendor,my-fake-led }, { } /* 这行不能省 */ };设备绑上了probe却返回错误dm tree里对应状态是[ - ]。最常见的probe失败原因是资源没就绪比如父设备没probe、下游时钟没开。检查boot日志里的错误返回值例如-EINVAL说明参数问题-ENODEV说明资源不存在-EPERM常用于权限或标志位冲突。另外注意priv_auto是否足够大如果priv里存了超长数组而框架分配的小了也会出现诡异现象。5.3 调试u-boot设备模型的三板斧第一板斧打印匹配过程。在lists_bind_fdt()里临时加debug()打印当前遍历到的driver名称和节点compatible。这个方法能直接看出匹配到谁、卡在哪一步。调完记得把debug去掉。第二板斧手动调用device_probe。有时候我怀疑probe时序就临时在board_init_r之后调用uclass_get_device_by_name(UCLASS_MISC, my_fake_led1000, dev)。如果这样能正确probe说明问题是自动probe时机太早考虑调整DM_FLAG_PRE_RELOC标志或者确保父总线就绪。第三板斧查op操作表。如果设备能probe但上层操作无效问题多半是ops没有正确赋值。比如实际用的是GPIO接口但驱动里没给.ops字段安装dm_gpio_ops那么dev_get_ops(dev)拿到的就是NULL调用即崩溃。这种问题从dm tree上看不出来只能返回代码核对ops指针。最后说一个个人习惯我在移植新板子时第一步永远是让dm tree输出正常再谈其他功能。因为这个骨架一旦通了后面所有驱动调试都建立了坐标系。如果是老平台同时存在旧式代码和dm驱动转换期间还要小心两者打架——别让旧初始化函数和dm框架重复操作同一份寄存器。我在实际项目里光“设备树节点写了但驱动没编进去”这个问题就碰到过三次每次都是重新编译后忘加CONFIG选项。所以现在写驱动我必定先确认符号段里有我的driver名字再花时间查probe逻辑。这套经验虽然不起眼却真能省掉几个小时的头秃时光。
返回列表