ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

u-boot设备模型骨架:board_init_r阶段DM初始化详解

u-boot设备模型骨架:board_init_r阶段DM初始化详解

1. 这不是玄学,是u-boot设备模型的“筑基仪式”

“屠龙刀在手,速通u-boot设备模型”——这标题听着像武侠小说,但放在嵌入式开发圈里,它精准戳中了无数工程师的痛点。我干了十多年底层驱动和Bootloader定制,从ARM9到RISC-V,从单片机Bootloader到车规级SOC启动栈,u-boot的board_init_r函数就像一道分水岭:跨过去,你才算真正摸到了硬件初始化的命脉;卡在这儿,哪怕你把Linux内核源码倒背如流,面对一块新板子也得抓瞎。而标题里说的“dm驱动骨架”,就是这道门背后的地基工程——不是什么花哨的新功能,而是u-boot自2013年引入Device Model(DM)机制后,整个驱动架构翻天覆地的底层重构。它不直接控制LED亮灭,却决定了UART能不能收发、EMMC能不能识别、网卡能不能ping通;它不写一行业务逻辑,却让成百上千个驱动模块能像乐高积木一样插拔、复用、分层管理。所谓“骨架”,就是指board_init_r阶段那一段看似平淡无奇、实则暗流汹涌的初始化序列:从dm_init_and_scan()开始,到drivers_register()结束,中间穿插着uclass_init()、device_bind_driver()、device_probe()这一连串调用链。很多人以为只要把驱动代码放对位置、加个U_BOOT_DRIVER宏就完事了,结果烧进去发现串口没输出、SD卡识别失败,debug半天才发现——骨架根本没立起来,所有驱动都悬在半空。这背后没有魔法,只有三件事:内存布局是否预留了足够大的gd->dm_root空间、设备树节点是否正确标注了compatible并挂载到正确parent、驱动注册顺序是否满足依赖关系。我见过太多项目,因为一个#address-cells少写了个1,或者reg属性地址算错4字节,导致整个DM树构建失败,最后靠打印gd->dm_root指针值为NULL才定位到问题。所以,“屠龙刀”不是某款神秘工具,而是你对board_init_r里这几百行C代码的透彻理解;“速通”也不是跳过原理,而是用结构化思维,把设备模型从抽象概念,变成可调试、可验证、可复用的具体内存结构。

2. 为什么非得在board_init_r里搭这个骨架?绕不开的启动时序铁律

2.1 启动流程中的“不可逆临界点”

u-boot的启动过程像一条精密流水线,board_init_r不是随便选的一个函数名,它是整个初始化流程中一个明确的、不可逆的分界点。在此之前,比如board_init_f阶段,系统还处于“裸奔”状态:内存控制器可能刚初始化,DRAM还没完全稳定,全局数据区gd(global data)甚至还在SRAM里临时存放,连基本的malloc都不支持。这时候做设备模型初始化?等于在地基都没打牢的工地上盖摩天楼。board_init_r之所以被设计成DM骨架的唯一落点,核心在于它具备三个硬性前提:第一,gd结构体已完整迁移到DRAM中,拥有稳定、可长期使用的内存空间;第二,malloc和calloc等动态内存分配函数已可用,而DM树的构建极度依赖堆内存来分配struct udevice、struct uclass等对象;第三,最关键的——设备树(Device Tree)已完成解析并加载到内存,所有硬件描述信息(/soc/uart@12300000这类节点)都已转化为可遍历的数据结构。我曾在一个Rockchip RK3399项目上刻意把dm_init_and_scan()提前到board_init_f末尾,结果malloc返回NULL,gd->dm_root初始化失败,后续所有probe调用直接跳过,串口驱动压根没执行。这不是bug,是设计使然。u-boot开发者早就把时序约束写进了注释里:“This must be called after malloc() is available and before any drivers are probed.”——这句话不是建议,是铁律。

2.2 DM骨架与传统驱动模型的本质差异

老派u-boot驱动(pre-DM时代)像一盘散沙:每个驱动自己管自己,UART驱动直接操作寄存器,EMMC驱动自己初始化控制器,彼此之间没有统一视图。board_init_f里一堆uart_init()、i2c_init()、mmc_init()平铺直叙地调用,谁先谁后全靠开发者手动排布,一旦某个驱动依赖另一个驱动的资源(比如I2C总线要先于连接在其上的RTC芯片初始化),就必须在代码里硬编码调用顺序。这种模式在简单板子上还能应付,但到了多核SoC、带PCIe/NVMe/USB3.0的复杂平台,维护成本指数级上升。而DM模型则像建立了一个中央调度室:board_init_r里先调用dm_init_and_scan(),它会扫描设备树,为每个兼容节点创建一个struct udevice实例,并按parent关系构建成一棵树;接着uclass_init()为每类设备(UART、I2C、MMC)创建一个struct uclass容器,负责管理该类下所有设备的生命周期;最后drivers_register()将所有已编译进镜像的驱动(通过U_BOOT_DRIVER宏注册)挂载到对应uclass上。整个过程是声明式的——你只需在设备树里写compatible = "snps,dw-apb-uart",在驱动里写U_BOOT_DRIVER(dw_apb_uart_drv),框架自动完成匹配、绑定、探测。这带来的直接好处是解耦:UART驱动不再关心自己在哪块SoC上,只专注实现ops->putc、ops->getc;板级代码也不再需要硬编码调用dw_apb_uart_init(),只需确保设备树节点存在且路径正确。我在做高通SM8150平台移植时,同一套UART驱动代码,仅通过修改设备树compatible字符串,就无缝切换了高通原生驱动和Synopsys IP驱动,省去了数天的适配工作。这种灵活性,正是board_init_r阶段构建DM骨架所赋予的核心价值。

2.3 “骨架”二字的物理含义:内存布局与数据结构真相

很多人把“DM骨架”想象成一个虚的概念,其实它在内存里有非常具体的、可触摸的形态。当你在board_init_r里执行完dm_init_and_scan(),内存里至少会生成三类关键结构:首先是gd->dm_root,这是一个指向struct udevice的指针,它就是整棵树的根节点,通常对应设备树的/根节点;其次是每个uclass的struct uclass实例,存放在gd->uclass_root数组里,索引由enum uclass_id决定(比如UCLASS_SERIAL对应索引12);最后是海量的struct udevice节点,它们分散在堆内存中,每个节点包含name、seq、parent、uclass、driver等字段,parent指针形成父子关系,uclass指针指向所属uclass容器。我习惯用print_info命令在u-boot shell里直接打印这些结构:bdinfo能看到gd->dm_root地址,dm tree命令则递归打印整棵树的层级和状态。有一次客户反馈网卡无法初始化,我让他执行dm tree -P eth,发现eth0节点state显示PROBE_ERR,再查dm uclass看到UCLASS_ETH的dev_count为0,立刻断定是网卡驱动没注册成功,而不是硬件问题。这种可观察、可调试的特性,正是“骨架”区别于黑盒初始化的关键。它不是一次性执行完就消失的函数调用,而是一个持续存在的、贯穿整个u-boot生命周期的内存实体。后续所有device_find_by_name()、uclass_get_device_by_seq()等API,都是在这个骨架上进行查询和操作。所以,理解board_init_r里的DM初始化,本质上就是理解这三类结构如何被分配、如何被链接、如何被索引——这才是“屠龙刀”的真正锋刃。

3. 拆解board_init_r:从dm_init_and_scan()到drivers_register()的逐帧解析

3.1 dm_init_and_scan():设备树到内存树的第一次映射

dm_init_and_scan()是整个骨架搭建的起点,它的任务很纯粹:读取已解析的设备树,为每一个有效节点创建对应的struct udevice实例,并建立初步的父子关系。这个函数内部其实做了两件大事:第一是dm_init(),它分配并初始化gd->dm_root,同时为所有预定义的uclass(UART、I2C等)分配struct uclass内存,但此时uclass还是空的,没有设备挂载;第二是dm_scan_fdt(),这才是真正的“扫图”动作。dm_scan_fdt()会递归遍历设备树的每个节点,对每个节点执行device_bind_by_ofnode()。这里有个关键细节:并非所有节点都会被绑定。device_bind_by_ofnode()会检查节点是否有compatible属性,且该compatible字符串必须能在当前u-boot镜像中找到匹配的驱动(即驱动里U_BOOT_DRIVER的.of_match数组里有对应项)。如果匹配成功,就调用device_bind()为该节点分配一个struct udevice,填充name(取自label或name属性)、seq(设备序号)、parent(根据设备树层级设置)等字段,并将其加入父节点的child_head链表。我曾经在一个Allwinner H6项目里,设备树里写了compatible = "allwinner,sun50i-h6-usb-phy",但u-boot配置里没打开CONFIG_USB_PHY_SUNXI,结果dm_scan_fdt()扫到这个节点时,device_bind_by_ofnode()返回-ENOENT,usb_phy节点根本没生成udevice,后续USB Host初始化自然失败。所以,dm_init_and_scan()不是万能的“自动发现”,它严格遵循“有驱动才建设备”的原则。这也是为什么我们常说“设备树是硬件描述,驱动是软件能力,两者必须精确匹配”。调试时,如果dm tree看不到预期设备,第一反应不是设备树写错了,而是先确认对应驱动是否已启用并编译进镜像。

3.2 uclass_init():为每一类设备建立专属管理办公室

当dm_init_and_scan()完成后,内存里已经有了gd->dm_root和一堆零散的struct udevice节点,但它们还只是“户口本”上的名字,没有组织、没有管理、没有统一接口。uclass_init()就是为它们建立“居委会”或“行业协会”的过程。这个函数会遍历所有预定义的uclass ID(从UCLASS_ROOT到UCLASS_COUNT),对每个ID调用uclass_get(),后者会检查gd->uclass_root数组中该ID位置是否为空,为空则分配一个struct uclass结构体,并初始化其uc_drv(uclass驱动,定义了该类设备的通用操作,如post_bind、pre_probe)、dev_head(该类下所有设备的链表头)等字段。举个具体例子:UCLASS_SERIAL的uclass驱动定义在drivers/serial/serial-uclass.c里,它实现了serial_post_bind()——这个函数会在每个UART设备绑定后自动为其分配seq序号,并设置platdata。这意味着,你不需要在每个UART驱动里重复写dev->seq = get_next_seq();,uclass层已经帮你做好了标准化处理。uclass_init()的精妙之处在于它的延迟性:它不会为所有uclass一次性初始化,而是按需触发。比如,当dm_scan_fdt()遇到第一个compatible = "ns16550a"的节点时,才会去初始化UCLASS_SERIALuclass。这种设计节省了内存,尤其对于那些板子上根本不存在的设备类型(比如没有PCIe设备的板子,UCLASS_PCIuclass就不会被初始化)。我在做NXP i.MX8MQ项目时,板子上没有SPI NOR Flash,但设备树里误留了&spi0节点,结果uclass_init()为UCLASS_SPI分配了内存,虽然没设备挂载,但gd->uclass_root[UCLASS_SPI]不为NULL,dm uclass命令里依然能看到它。这提醒我们:uclass的存在不等于设备存在,dm uclass和dm tree要结合看,才能准确定位问题。

3.3 drivers_register():驱动与设备的“媒妁之约”

如果说dm_init_and_scan()是画出设备地图,uclass_init()是设立管理机构,那么drivers_register()就是促成驱动与设备“联姻”的红娘。这个函数遍历所有通过U_BOOT_DRIVER宏注册的驱动列表(这个列表在链接时由__u_boot_driver_section_start和__u_boot_driver_section_end符号界定),对每个驱动调用driver_bind()。driver_bind()的核心逻辑是:遍历所有已存在的struct udevice节点,查找那些driver字段为空、且of_match能匹配当前驱动of_match表的节点,然后调用device_bind_driver()将驱动指针填入设备的driver字段,并设置driver_data等。注意,这里的关键是“已存在”的设备——也就是说,drivers_register()必须在dm_init_and_scan()之后执行,否则设备节点都不存在,绑定无从谈起。这个过程看似简单,实则暗藏玄机。device_bind_driver()在绑定前会调用驱动的.bind回调函数(如果定义了的话),这是一个绝佳的钩子点。比如,在drivers/mmc/mmc-uclass.c里,mmc_bind()函数会根据设备树max-frequency属性,为MMC设备预设时钟频率,这个设置在probe之前就完成了。我曾利用这个机制,在自定义的ADC驱动里,通过.bind回调读取设备树vref-supply属性,提前获取参考电压值,避免probe时再去解析,提升了初始化速度。drivers_register()完成后,每个udevice节点都拥有了自己的driver,但此时设备还处于UDPROBE_UNKNOWN状态,尚未真正工作。真正的“激活”要等到后续的device_probe()调用,那才是驱动probe函数被执行的时刻。所以,drivers_register()只是完成了静态绑定,是骨架成型的最后一块拼图,但还不是血肉丰满的完整体。

3.4 device_probe():骨架上的第一次心跳

device_probe()通常不会在board_init_r的初始序列里被显式调用,但它紧随其后,是骨架获得生命的第一步。在标准u-boot流程中,board_init_r末尾会调用initr_dm(),而initr_dm()内部就会执行dm_init_and_scan()、uclass_init()、drivers_register(),然后紧接着调用dm_init_and_scan()返回后的一系列device_probe()调用。device_probe()的逻辑很清晰:它遍历所有udevice节点,对每个state == UDPROBE_UNKNOWN的节点,调用其driver->probe函数。probe函数是驱动的灵魂,它负责真正的硬件初始化:申请内存、映射寄存器、配置时钟、使能中断、读取ID等。probe成功后,设备state变为UDPROBE_DONE,表示已就绪;失败则变为UDPROBE_FAIL。这里有个重要原则:probe的执行顺序不是随机的,而是严格遵循设备树的层级顺序。父节点(如/soc)会先于子节点(如/soc/uart@12300000)被probe,确保子设备能安全访问父总线资源。我在调试一个PCIe设备时,发现probe失败,日志显示pci_read_config_dword返回错误。后来发现是PCIe Root Complex的probe函数里,pci_setup_bus()没执行完,总线配置还没生效,而子设备的probe就已经被触发了。解决方案就是在Root Complex驱动的probe函数末尾,显式调用pci_bus_assign_resources(),确保资源分配完成后再返回。这再次印证了DM骨架的价值:它不仅提供了结构,更通过严格的层级probe顺序,强制保证了硬件初始化的逻辑正确性。device_probe()不是一次性的,后续在运行时动态加载设备(如USB热插拔)时,也会再次触发device_probe(),这就是骨架的延展性——它支撑的不只是启动那一刻,而是整个u-boot生命周期。

4. 实操避坑指南:从设备树编写到驱动注册的全流程陷阱排查

4.1 设备树层面的致命陷阱:compatible、reg与phandle的三角关系

设备树是DM骨架的蓝图,任何一处笔误都可能导致整棵树构建失败。最常见的三类错误,我称之为“三角陷阱”:

第一,compatible字符串不匹配。这是最直观也最容易忽略的。驱动里U_BOOT_DRIVER的.of_match数组,必须与设备树节点的compatible属性完全一致(包括大小写和空格)。例如,驱动定义{ .compatible = "rockchip,rk3399-pcie" },设备树里写成compatible = "rockchip,rk3399-pci",哪怕只差一个字母,device_bind_by_ofnode()就找不到匹配,设备节点永远不会被创建。调试方法很简单:在dm_init_and_scan()前后加printf("Scanning node %s\n", ofnode_get_name(node));,再在device_bind_by_ofnode()里加printf("Match %s with %s? %d\n", ofnode_get_name(node), match->compatible, ret);,一眼就能看出哪个节点匹配失败。

第二,reg属性地址/长度计算错误。reg定义了设备的寄存器基址和范围,DM在device_bind()时会用它来设置dev->regs。如果地址算错,probe函数里readl(dev->regs)就会读到错误地址,轻则功能异常,重则系统崩溃。典型错误是混淆了物理地址和虚拟地址,或者忽略了#address-cells和#size-cells的值。比如,#address-cells = <2>意味着reg的第一个cell是高位地址,第二个是低位地址,必须拼接起来。我在一个TI AM65x项目里,reg = <0x0 0x48000000 0x0 0x1000>,但驱动里直接用0x48000000当基址,忘了高位是0,结果访问了错误的内存区域。正确做法是用ofnode_get_addr_size()函数安全获取。

第三,phandle引用失效。很多设备依赖其他设备,比如UART需要clock,I2C设备需要I2C控制器。这时用clocks = <&clk_uart0>或i2c-parent = <&i2c0>。如果&clk_uart0这个label在设备树里根本不存在,或者&i2c0节点没启用(status = "disabled"),ofnode_parse_phandle()就会失败,device_bind()返回错误,设备无法绑定。我习惯在board_init_r开头加一段调试代码:for_each_node((node = ofnode_first_node(gd->fdt_blob)); node; node = ofnode_next_node(node)) { printf("Node: %s, status: %s\n", ofnode_get_name(node), ofnode_read_string(node, "status")); },快速扫一遍所有节点状态,揪出被disable的父节点。

4.2 驱动注册环节的隐形雷区:U_BOOT_DRIVER宏的隐藏参数

U_BOOT_DRIVER宏看着简单,但里面藏着几个关键参数,填错一个,驱动就“隐身”了。标准写法是:

U_BOOT_DRIVER(rockchip_pcie_drv) = { .name = "rockchip-pcie", .id = UCLASS_PCI, .of_match = rockchip_pcie_ids, .probe = rockchip_pcie_probe, .ops = &rockchip_pcie_ops, .priv_auto_alloc_size = sizeof(struct rockchip_pcie), };

其中.id必须与uclass ID严格对应,UCLASS_PCI不能写成UCLASS_PCIE(后者不存在)。.of_match必须是指向struct of_device_id数组的指针,且数组最后一个元素必须是{}(空结构体)作为终止符,否则driver_bind()会越界扫描。.priv_auto_alloc_size指定了为该设备自动分配的私有数据区大小,如果驱动probe函数里需要大量本地变量,必须在这里申请,而不是在probe里malloc——因为probe时malloc可能还不稳定。最隐蔽的雷区是.flags字段。默认是0,但如果驱动需要在probe前执行一些预处理,可以设DM_FLAG_PRE_RELOC,这样driver_bind()会在board_init_f阶段就执行,但这要求驱动代码必须是relocation-safe的(不能用全局变量、不能调用malloc)。我曾在一个需要早期初始化DDR控制器的驱动里误用了DM_FLAG_PRE_RELOC,结果probe函数里调用了printf,导致启动失败,因为printf依赖重定位后的gd指针。解决方法是去掉该flag,或者重写probe函数,只用汇编指令和寄存器操作。

4.3 内存与链接脚本的底层制约:gd->dm_root的空间争夺战

gd->dm_root指向的是一块连续内存,用于存放struct udevice、struct uclass等对象。这块内存的大小由CONFIG_DM_MAX_DEVICES和CONFIG_DM_MAX_UCLASS等配置项决定,默认值往往偏小。当板子上设备太多(比如带多个PCIe设备、USB Hub、SATA控制器),dm_init_and_scan()就会因内存不足而失败,gd->dm_root保持为NULL。现象是dm tree命令报错“DM not initialized”,所有设备都不可见。解决方案不是盲目增大配置,而是精准计算。struct udevice大小约80字节,struct uclass约120字节,假设你有50个设备、10个uclass,理论需要(50*80 + 10*120) = 5200字节,再加30%冗余,设CONFIG_DM_MAX_DEVICES=60、CONFIG_DM_MAX_UCLASS=12即可。更大的陷阱在链接脚本。gd结构体本身在arch/arm/lib/bss.o里定义,其大小由CONFIG_SYS_GBL_DATA_SIZE控制。如果这个值太小,gd结构体后面没有足够空间容纳dm_root指针及其关联数据,dm_init()分配内存时就会踩到其他变量。我在一个ARM64项目里,CONFIG_SYS_GBL_DATA_SIZE设为0x1000(4KB),但实际gd加上DM所需内存超过4KB,结果gd->dm_root被覆盖,dm tree显示乱码。最终解决方案是增大CONFIG_SYS_GBL_DATA_SIZE到0x2000,并在链接脚本里确认.bss段有足够的空间。这提醒我们:DM骨架不是孤立的,它深深嵌入u-boot的内存布局之中,任何改动都需全局审视。

4.4 调试工具链的实战组合:从dm命令到自定义printf

u-boot自带的dm命令是调试骨架的利器,但光靠它不够,必须组合使用。我的标准调试流程是:

  1. 启动初期加printf:在board_init_r开头,dm_init_and_scan()前后各加一句printf("Before dm_init_and_scan, gd->dm_root=0x%p\n", gd->dm_root);和printf("After dm_init_and_scan, gd->dm_root=0x%p\n", gd->dm_root);。如果gd->dm_root始终为NULL,问题一定出在dm_init()阶段,检查CONFIG_DM是否启用、CONFIG_SYS_MALLOC_LEN是否足够。

  2. dm tree与dm uclass交叉验证:dm tree显示设备树结构,dm uclass显示uclass统计。如果dm tree里有uart@12300000,但dm uclass里UCLASS_SERIAL的dev_count为0,说明设备节点创建了,但没绑定驱动,重点查compatible和驱动编译。

  3. dm dev与dm driver深挖细节:dm dev -i <dev-id>可以查看指定设备的详细信息,包括state、driver、parent。dm driver列出所有已注册驱动。如果某个设备state是PROBE_ERR,用dm dev -i <dev-id>看driver字段是否为空,为空则绑定失败,不为空则probe函数出错。

  4. 自定义dump_dm_tree()函数:对于复杂问题,我常在代码里加一个函数,遍历gd->dm_root,用printf逐级打印每个udevice的name、seq、parent->name、driver->name,比dm tree更直观。例如:

void dump_dm_tree(struct udevice *dev, int depth) { for (int i = 0; i < depth; i++) printf(" "); printf("%s (seq=%d, state=%d, drv=%s)\n", dev->name, dev->seq, dev->state, dev->driver ? dev->driver->name : "none"); struct udevice *child; list_for_each_entry(child, &dev->child_head, sibling_node) dump_dm_tree(child, depth + 1); }

调用dump_dm_tree(gd->dm_root, 0),就能看到一棵清晰的文本树,连state状态都一目了然。

5. 常见问题速查表与独家经验技巧

问题现象可能原因排查步骤解决方案
dm tree命令报错“DM not initialized”gd->dm_root为NULL1. 检查CONFIG_DM=y是否启用
2. 检查board_init_r里是否调用了dm_init_and_scan()
3. 检查CONFIG_SYS_MALLOC_LEN是否足够,malloc是否返回NULL
确保配置开启,确认函数调用,增大CONFIG_SYS_MALLOC_LEN
dm tree能看到设备节点,但state为PROBE_ERRprobe函数返回非0值1. 在probe函数开头加printf("Probing %s...\n", dev->name)
2. 检查probe里readl/writel操作的寄存器地址是否正确
3. 检查时钟、复位等前置条件是否满足
修复寄存器地址,添加clk_enable()、reset_deassert()等前置操作
dm tree里设备节点缺失设备树节点未被扫描或绑定1. 执行fdt print /soc/xxx确认节点存在且status="okay"
2. 检查compatible字符串是否与驱动匹配
3. 检查驱动是否已编译进镜像(grep "rockchip_pcie_drv" u-boot.map)
修正设备树status,核对compatible,确认CONFIG_XXX=y
多个相同设备(如多个UART)seq序号混乱uclass的post_bind未正确执行1. 检查UCLASS_SERIALuclass驱动是否启用
2. 在serial_post_bind()里加printf确认是否被调用
3. 检查设备树节点是否有label属性,label优先级高于name
确保uclass驱动启用,为每个UART节点添加唯一label,如&uart0 { label = "uart0"; };
dm uclass显示dev_count为0,但dm tree有设备设备未绑定到uclass1. 检查设备节点的compatible是否指向正确的uclass驱动
2. 检查驱动的.id字段是否与uclass ID一致
3. 检查drivers_register()是否执行
核对compatible与驱动of_match,确认驱动.id正确,检查board_init_r流程

提示:dm tree -p命令会显示每个设备的platdata内容,这是调试驱动私有数据的黄金入口。如果platdata里字段全是0,说明driver->bind或driver->ofdata_to_platdata没执行,重点检查这两个回调函数。

注意:不要迷信CONFIG_DM_DEBUG。开启它会产生海量日志,淹没关键信息。我的习惯是,在怀疑某个特定节点时,只在device_bind_by_ofnode()和device_probe()里加针对性printf,日志量可控,定位精准。

实操心得:在board_init_r里,我习惯把dm_init_and_scan()、uclass_init()、drivers_register()这三步拆成独立函数调用,并在每步后加printf("DM step X done\n")。这样,如果启动卡住,一眼就能看出停在哪一步,极大缩短debug时间。这比看CONFIG_DM_DEBUG的滚动日志高效十倍。

经验技巧:对于新板子,我第一件事不是写驱动,而是先写一个“空驱动”:U_BOOT_DRIVER(dummy_drv) = { .name = "dummy", .id = UCLASS_MISC, .of_match = dummy_ids, };,dummy_ids[] = { { .compatible = "my,vendor-my-device" }, {} };。然后在设备树里加一个my-device { compatible = "my,vendor-my-device"; };。如果dm tree里能看到这个节点,证明DM骨架已通,后续再逐步替换为真实驱动。这是一种极低成本的“骨架验证法”。

独家避坑:device_probe()的执行顺序受uclass的.per_device_platdata_auto_alloc_size影响。如果某个uclass设置了这个值,它会为每个设备自动分配platdata,但分配时机在uclass_init()时,早于drivers_register()。如果驱动probe函数里又malloc了一次platdata,就会造成内存泄漏。务必检查uclass驱动源码,确认是否已自动分配,避免重复。

我做过最“野”的一次调试,是在一个没有串口输出的加密SoC上。printf全被禁用,只能靠LED闪烁编码。我把dm_init_and_scan()的返回值,用LED的闪烁次数来表示:1次闪代表dm_init()成功,2次代表dm_scan_fdt()成功,3次代表某个关键节点绑定成功……靠着这个“摩尔斯电码”,硬是把DM骨架的每一步都走通了。这件事让我深刻体会到:所谓“屠龙刀”,从来不是某个现成的工具,而是你对底层机制的理解深度,以及把这种理解转化为可执行、可验证、可调试的行动力。board_init_r里的这几行代码,就是嵌入式世界的“道”,读懂它,你就拿到了开启所有u-boot设备模型大门的钥匙。

返回列表