ARTICLE DETAIL

资讯详情

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

U-Boot DM驱动骨架:从board_init_r到设备树匹配解析

U-Boot DM驱动骨架:从board_init_r到设备树匹配解析 前两天帮人调一块新板子MIPI屏点不亮串口日志倒是正常设备树节点目测也对驱动文件也在但就是probe不进去。查了半天最后问题出在compatible没匹配上驱动压根没被bind。这让我又一次体会到在U-Boot里如果你不理解DMDriver Model这套驱动骨架遇到问题基本只能瞎猜。很多从Linux转过来的朋友对设备模型那一套理论很熟一进U-Boot就发懵因为U-Boot把整个设备模型的骨架搭建压缩到了board_init_r里短短几步初始化中。这篇就把board_init_r里面DM驱动骨架是怎么一步步搭起来的拆开讲清楚看完你也能拿着这把屠龙刀快速定位和解决U-Boot驱动相关的问题。1. 先看大局board_init_r里那串init序列到底在跑什么1.1 定位init_sequence_rU-Boot的启动分两大阶段board_init_f和board_init_r。前者英文叫“before relocation”后者叫“after relocation”。代码重定位到RAM之后board_init_r就开始执行一套长长的初始化序列这个序列的核心就是一个函数指针数组定义在common/board_r.cinit_fnc_t init_sequence_r[] { initr_malloc, initr_dm, initr_sys_info, initr_of_live, initr_bdinfo, initr_reloc, ... initr_dm_devices, ... };说白了这个数组就是一个“待办清单”U-Boot按顺序挨个调用里面的初始化函数。只要哪个函数返回非0整个启动就认为失败直接halt。你不需要背这个数组的全部内容只需要抓到几个关键点initr_malloc先把内存堆准备好initr_dm紧接着把设备模型初始化掉后面initr_of_live如果开了CONFIG_OF_LIVE会把扁平设备树转成live tree运行时树再往下才是各种外设的正式初始化比如initr_serial、initr_net之类的。initr_dm在不同版本里的实现稍有差别新版本里它的实现非常直白static int initr_dm(void) { return dm_init_and_scan(false); }就这么一句话。甚至老版本里你看到的是static int initr_dm(void) { dm_scan_platdata(false); dm_scan_fdt(gd-fdt_blob, false); return 0; }看到这里你可能会说这不就是扫一下平台数据和设备树吗对本质上就是这件事但“扫一下”这三个字背后的工作量远超表面。整个U-Boot的所有驱动能不能用几乎都取决于这一步之前你的U_BOOT_DRIVER、UCLASS_DRIVER有没有被正确编译进固件设备树节点有没有和驱动匹配上。后面所有驱动probe都是建立在这棵扫描出来的设备树之上的。1.2 dm_init_and_scan的前置依赖dm_init_and_scan不是想调就能调的它有几个前置依赖这也是我建议你理解board_init_r时要连起来看的原因。第一个依赖是内存堆heap已就绪。board_init_f阶段时代码还在Flash或SRAM里跑那时候的malloc空间往往非常小只够早期串口这类必需设备用。到了initr_mallocU-Boot会在RAM里划出一块完整的malloc区域代码里能看到类似malloc_simple或mem_malloc_init之类的调用。DM初始化过程中会创建uclass实例、udevice实例这些struct都要动态分配内存堆没起来之前你就算想初始化DM也分不出内存。第二个依赖是gdglobal data结构体已经准备到位且重定位后的地址正确。gd-dm_root这个指针专门用来保存DM的根设备从dm_init开始后续所有DM操作都能通过DM_ROOT()这个宏找到根节点。如果gd本身乱了后面所有东西都会崩而且崩得毫无规律。第三个依赖是早期DM初始化已经做过一轮。在board_init_f阶段你会看到一个initf_dm它调用的是dm_init_and_scan(true)注意参数是true。这个参数的含义是pre_reloc_only也就是只扫描绑定了DM_FLAG_PRE_RELOC标志的驱动和设备树节点。为什么这么做因为在Flash里跑的早期代码资源和时间都极其有限不可能把所有驱动都扫一遍只需要把串口、时钟这些早期必须要用的设备先拉起来就行。等进入board_init_r代码已经在RAM里舒舒服服跑着这时候再调用dm_init_and_scan(false)把整个设备树里所有设备完整扫描、绑定一遍。如果你只盯着board_init_r看不看board_init_f里的那次early scan很容易误解为什么有些设备在日志里出现两次。搞懂了前置条件我们再钻进DM骨架本身。2. dm骨架的三个底座driver、uclass、udevice与那段链接表魔法2.1 三个核心结构体U-Boot的设备模型核心抽象有三个结构体struct driver、struct uclass_driver和struct udevice。光看名字你可能觉得它们差不多其实分工完全不同我习惯用一个公司的结构去类比。struct driver是一份“岗位说明书”描述一个具体的驱动实现。它里面有name、id归属哪个uclass、of_match设备树compatible匹配表、probe、remove、bind这些回调函数。比如你写一个GPIO驱动probe函数就是初始化寄存器、设置引脚方向这些东西。struct uclass_driver是一个“部门章程”描述某一类设备的通用行为管着一堆driver。同样是GPIO不同厂商的GPIO控制器驱动千差万别但上层用GPIO时只关心gpio_request、gpio_direction_output这些通用接口这些通用接口的定义和行为就在uclass_driver里。struct udevice则是一个“具体员工”代表设备树里或平台数据中某个实际存在的设备实例它既有指向driver的指针也有指向所属uclass的指针还有挂到父设备下的链表节点。举个例子设备树里有个gpioff010000节点它对应的udevice就是一个具体实例这个实例的driver是某个厂商的GPIO驱动它所属的uclass是UCLASS_GPIO。上层的GPIO操作函数拿到这个udevice后先通过uclass找到通用层接口再通过driver调用具体的硬件操作。三层各司其职缺一层都跑不起来。还有一个隐藏的结构struct uclass它是uclass_driver的运行实例。你可以把uclass_driver看成静态的“章程”而struct uclass是运行时创建的“部门实体”它里面有一个链表头dev_head把属于这个uclass的所有udevice串起来。这样你要遍历某个uclass下所有设备时直接顺着这个链表走就行。2.2 U_BOOT_DRIVER和UCLASS_DRIVER宏与链接表通常我们写U-Boot驱动不会直接定义一个struct driver变量然后到处传指针而是用U_BOOT_DRIVER这个宏注册U_BOOT_DRIVER(gpio_sunxi) { .name sunxi_gpio, .id UCLASS_GPIO, .of_match sunxi_gpio_ids, .probe sunxi_gpio_probe, };这个宏展开后是什么它本质上定义了一个名为_u_boot_list_2_driver_2_gpio_sunxi的struct driver变量并且用__attribute__((section(.u_boot_list_2_driver_2_gpio_sunxi)))把它放到了指定的段里#define U_BOOT_DRIVER(__name) \ ll_entry_declare(struct driver, __name, driver)这就要提到U-Boot设备模型最精妙、也最让新手看不懂的设计链接表linker list。U-Boot的驱动分散在几百个.c文件里你怎么在编译期收集出一个完整的驱动数组用全局数组当然不行因为你没法事先知道要写多少个驱动。链接表的思路是把每个驱动声明成独立符号放到同名的section里然后在链接时让链接脚本把这些同section的符号连续排列自动形成一张表。链接脚本里你通常会发现类似这样的描述.u_boot_list : { KEEP(*(SORT(.u_boot_list_2_driver_1))); KEEP(*(SORT(.u_boot_list_2_driver_2))); KEEP(*(SORT(.u_boot_list_2_driver_3))); }配合include/u-boot/symbols.h里生成的起始结束符号你就能用ll_entry_start拿到表的首地址用ll_entry_count算出有多少项。uclass_driver也是同一个套路UCLASS_DRIVER(root)定义的其实也是这种链接表项只是放在了uclass这个名字对应的section里。这里有个最容易踩的坑链接表里的符号如果没人引用很可能会被链接器的--gc-sections优化掉。所以链接脚本里必须用KEEP()把它保住。有时候你明明写了U_BOOT_DRIVER但dm tree里就是看不到第一反应就该去检查有没有被gc掉或者对应的.o文件有没有真的编进u-boot这个目标里。理解了链接表你就知道dm到底从哪里拿驱动列表了。它不是靠什么配置文件去“发现”驱动而是靠编译器把分散的驱动打包成一个二进制里的静态数组运行时直接遍历这个数组。这套机制的另外一个好处是不需要任何动态注册步骤也不依赖初始化顺序天然适合嵌入式这种资源紧张的环境。3. dm_init_and_scan到底推进了什么3.1 dm_init造出root设备dm_init_and_scan的第一步是dm_init()。这个函数做了一件很小但至关重要的事创建根设备root device。代码上看非常简洁int dm_init(void) { return device_bind_common(NULL, root_driver, root, NULL, 0, NULL, 0, DM_ROOT_NON_CONST); }root_driver是一个特殊的驱动定义在drivers/core/root.c里它对应的uclass是UCLASS_ROOT。device_bind_common会创建根设备的udevice实例并把它的指针存到gd-dm_root。为什么要造一个根设备因为DM里的所有设备都要求有父设备整个设备树要从一个根节点开始组织。后续所有通过设备树扫描出来的设备都是这个root的child或者child的child。你可以把root理解成一个虚拟的“总装车间”所有实际硬件设备都挂在这个车间下面。dm_init还会初始化一些全局链表比如uclass实例链表。这里有一点要记住dm_init只创建root设备不创建任何真实硬件设备。真实设备全是后面scan阶段绑出来的。3.2 dm_scan_platdataboard文件里的固化设备接下来是dm_scan_platdata。这个函数遍历的是另一张链接表这张表里的元素是struct driver_info使用U_BOOT_DEVICE宏注册出来的U_BOOT_DEVICE(sunxi_gpio_bank0) { .name sunxi_gpio, .platdata gpio_bank0_platdata, };这种注册方式在纯设备树平台上用得越来越少但在一些不开设备树、或者固定外设的场景下很常见。它的工作方式是通过driver_info里的name字段在driver链接表里查找同名驱动找到之后调用device_bind_by_name把设备绑定到该驱动上。这个阶段需要注意设备树平台下driver_info表往往是空的因为设备信息全在设备树里。但U-Boot不关心你有没有它每次都会先遍历一下这张表。如果某项驱动没有设置DM_FLAG_PRE_RELOC而当前是early scan参数为true这段信息就会被跳过。3.3 dm_scan_fdt设备树节点与驱动匹配dm_scan_fdt才是大部分现代平台真正的主角。这个函数会从设备树根节点开始深度优先遍历所有子节点对每个节点尝试找到匹配的driver并bind。匹配方式不是看driver的name而是看driver里的of_match数组static const struct udevice_id sunxi_gpio_ids[] { { .compatible allwinner,sunxi-gpio }, { } };对于设备树里的一个节点U-Boot会取出它的compatible属性然后去driver的of_match数组里逐一比较字符串。匹配上了就认为这个节点应该由这个驱动负责调用device_bind_common创建一个udevice实例把节点信息offset、compatible等塞进设备结构体。匹配不上就跳过这个节点继续遍历下一个节点。这一步的遍历顺序也很有意思是先父后子递归进行。比如“i2c控制器”节点先被绑定然后它下面的“i2c设备”子节点再被绑定。这样天然保证了父子关系在udevice的链表中是被正确建立的。dm_scan_fdt还会配合pre_reloc_only参数做判断early阶段只绑定带有u-boot,dm-pre-reloc属性或驱动带DM_FLAG_PRE_RELOC标志的节点其他节点留到重定位后的完整扫描。3.4 bind不是probe从bind到probe的完整路径很多人刚接触DM时分不清bind和probe的差别这里我多说两句。bind只是把设备树节点/平台数据和driver关联起来创建udevice建立uclass下的链表关系这个阶段不会碰硬件寄存器。probe才是真正初始化硬件调用驱动的probe回调做一些寄存器配置、中断初始化之类的活。scan阶段只做bind不做probe。所以你在dm tree里看到一个设备处于actactive状态那是它已经被probe过了如果处于---之类的状态说明只bind了还没probe。真正的probe动作是“按需”的当某个代码调用uclass_get_device、dm_get_device或者直接device_probe(dev)时才会触发int device_probe(struct udevice *dev) { ... if (dev-flags DM_FLAG_ACTIVATED) return 0; if (dev-parent) device_probe(dev-parent); ... dev-flags | DM_FLAG_ACTIVATED; ret uclass_pre_probe(dev); if (ret) goto fail; ret drv-probe(dev); if (ret) goto fail; ret uclass_post_probe(dev); ... }device_probe的执行路径里有个非常关键的行为它会先probe父设备。父设备不成功子设备直接失败。比如你调一个挂在I2C总线上的触摸芯片驱动结果I2C控制器本身probe失败那么触摸芯片的probe就会因为这个父设备失败而被阻断而且你光看触摸芯片的日志往往只能看到一串暧昧的错误。probe顺序还有uclass层的介入uclass_pre_probe和uclass_post_probe分别在驱动probe前后调用。它们通常用来做uclass级别的通用初始化。比如某个uclass要求所有设备在probe之前先分配一块公共数据区或者probe之后统一设置某个标志就是在这两个回调里实现的。理解了bind和probe的区别再看启动日志里“DM: dev ...”那些打印就知道哪一步是哪一步了。4. 实战从启动日志反推骨架与排查问题4.1 验证骨架的几个命令和手段讲完原理我们回到文章开头那个屏点不亮的场景。在调试DM时我最常用的手段就是U-Boot命令行里的dm命令。开机进入U-Boot命令行敲 dm tree你会看到系统把整个设备树扫描后的设备列出来包括device的地址、所属uclass、父设备、名字和状态。如果某个节点没出现在列表里说明bind就没发生问题大概率出在compatible字符串匹配或驱动没被编译进来。如果出现在列表里但状态不是active或probe报错那就是probe阶段的问题可以顺着probe回调去找。还有一个命令是dm uclass它列出所有已注册的uclass实例可以帮你确认某个uclass_driver有没有被链接进来。如果缺失这里就不会显示。dm dump或dm drv在部分版本里也能用分别看设备和driver列表。如果想看更细的匹配日志可以打开CONFIG_DEBUG_DM和CONFIG_DM_WARN。打开之后device_bind_common和device_probe会打印较多调试信息比如某个节点“no match”之类。配合串口输出排起查来效率高很多。4.2 常见问题速查表与排障思路下面这几种情况是我在实际项目里遇到最多、也最有代表性的DM骨架问题我整理成一张速查表现象大概率原因排查方向dm tree里看不到设备节点compatible不匹配或驱动未被链接检查of_match数组和设备树节点的compatible确认U_BOOT_DRIVER的of_match已定义检查对应.o是否编进固件设备在dm tree里能看到但一直不是active没有代码主动probe它或probe被跳过找到上层使用者确认用户代码是否调用了uclass_get_device/device_probeprobe时报“missing uclass”或找不到uclass idUCLASS_DRIVER没注册或driver.id写错检查enum uclass_id枚举定义确认uclass链接表里有对应条目多个设备共用一个驱动只有第一个device正常platform data或uclass platdata分配问题检查per_device_auto、per_device_platdata_auto等宏确认每个设备的数据区独立父设备probe失败子设备莫名其妙起不来父设备依赖的前置资源没就绪先单测父设备单独调device_probe父设备看它具体死在哪个返回值开了CONFIG_DEBUG_DM还是没日志某些log被优化或者串口本身是早期设备在驱动probe函数里手动加printf缩进配合DM_DEBUG宏在必要位置打点这里面最隐蔽的是compatible匹配问题。很多Linux下的驱动已经写好了复制到U-Boot里of_match数组却在驱动文件里没有正确导出或者设备树里的compatible写成了allwinner,sunxi-gpio而驱动里的of_match只写了allwinner,sun8i-gpio。字符串匹配没有任何缓存错一个字符就是整个节点消失且没有任何主动报错。所以我建议你现在就养成习惯写完U_BOOT_DRIVER第一时间用dm tree验证节点在不在不在先比对compatible字符串用二分法缩小范围。还有一个值得说的坑是uclass id。U-Boot的uclass id定义在一个大的枚举里类似UCLASS_ROOT、UCLASS_SERIAL、UCLASS_GPIO这些。如果你在自定义驱动里把.id不小心写成一个不存在的枚举值或者改成别人的uclass id那么bind阶段找uclass实例时就会失败。这种问题看起来像“系统崩溃”其实是骨架没搭对。5. 写在最后换个角度看这块骨架老话说功夫在诗外对嵌入式工程师来说源码就是最好的老师。每次我调U-Boot驱动调不动都会重新回到drivers/core/root.c、device.c、uclass.c这几个核心文件里过一遍。这三个文件加起来也没几千行但把U-Boot设备模型的骨架讲得明明白白。我自己最深的体会是不要去背那些回调函数的调用顺序而是要记住“链接表收集驱动、scan阶段绑定设备、probe按需初始化硬件”这个总纲。只要这个总纲在脑子里遇到dm tree里看不到节点、probe失败、uclass缺失这类问题你第一时间就知道该往哪个方向查。屠龙刀不是指某个具体函数而是这套“由静到动、先结构后行为”的骨架思维。另外一个小技巧调试U-Boot设备模型时别只看代码加打印多利用命令行命令。U-Boot本身就是个交互环境dm tree一下能省掉很多无意义的插桩日志。如果你调的板子串口还没起来那就要从early scan阶段开始排查看看board_init_f里的initf_dm之后早期设备到底有没有被正确bind和probe这一步起不来后面board_init_r里的一切都是空中楼阁。最后再分享一个我个人的土办法我习惯在驱动probe函数入口加一行printf(probe %s\n, dev-name)先确认它到底有没有被调用然后才去分析外部问题。代码里加了这行不减出问题时永远快人一步。
返回列表