u-boot的DM框架圈里人都叫它“屠龙刀”无他因为从这块代码过去的人太多能用好的人太少。各种总线、外设、驱动像乱麻一样缠在启动流程里而DMDriver Model把所有设备节点、驱动、uclass抽象成一套可遍历、可匹配、可热插拔的模型让你在密密麻麻的代码里也能快速找到“这棵设备树是怎么长出来的”。这篇文章不绕弯子直接瞄准board_init_r这个入口把DM的驱动骨架从头搭到尾——从dm_init到设备树扫描从bind到probe再到调试和实战接入一套流程捋完心里基本就有谱了。适合正在做u-boot移植、被设备树和驱动绑定搞到头皮发麻的嵌入式开发同学也适合刚接触DM想搞懂内部原理的人。刀在手咱们开始。1. 从board_init_r说起DM为什么偏偏在这里自举1.1 先认清board_init_r在启动链路里的位置很多新人第一次看u-boot源码都会被一大串“init_sequence_r”直接劝退。这个函数列表摆在那里看着像流水账实际上每一条都对应一个子系统从无到有的过程。u-boot的启动顺序大体是arch级别的start.S先把栈、异常向量、内存基本初始化跑一遍然后进入板级C代码的board_init_f做早期的RAM、时钟、串口调试之类的准备工作。接着代码会重定位自己把u-boot镜像搬到内存的合适位置再进入board_init_r——这才是“完整世界”的开始。DM框架的搭设就发生在board_init_r里具体动作是initcall列表中的initr_dm。这一行看起来不起眼但它决定了后续所有uclass、driver、device的生死。你去看board_r.c里的init_sequence_rinitr_dm出现的位置非常靠前比console_init_r、stdio_init_tables都要早。为什么因为console、stdio、flash、env这些子系统的初始化统统依赖设备模型去枚举硬件、绑定驱动、分配资源。如果DM没有先立起来后面全是空中楼阁。1.2 为什么不在board_init_f阶段就搭完整DM这是很多人没想通的一层。board_init_f阶段代码还在一个相对“残缺”的世界里内存可能只初始化了一部分malloc池还没准备好重定位也没完成设备树能不能完整访问都是问题。DM框架虽然本身不复杂但它需要malloc做动态分配需要建立链表结构需要把FDT里的节点转化成可遍历的设备树——这全都依赖一个稳定可用的运行时环境。所以u-boot的设计很务实board_init_f阶段能做的事情极度精简初始化RAM、时钟、调dram大小、准备堆栈最多做一点早期调试输出。而真正的“世界构建”放到board_init_r里等重定位完成、内存空间充裕、malloc池可用之后再用initr_dm把DM这棵树的根扎下去。理解了这一层你就知道为什么DM的初始化时机这么靠前却又不能在更早的裸环境中出现——它不是被遗忘在后面的而是刻意安排。1.3 initr_dm内部到底做了什么打开看到initr_dm的实现其实就是一句话的事static int initr_dm(void) { return dm_init_and_scan(); }但这一句话背后是一长串连锁反应。dm_init_and_scan()做的事情第一步是dm_init()负责把全局的root device和root uclass建立起来接着是dm_scan_plat()扫描platform data从编译期定义的platdata生成设备再是dm_scan_fdt(),把设备树里的节点逐层扫描出来并绑定对应驱动最后dm_scan_other()留给板级代码补充扫描其他非标准设备。这四个阶段合起来就是DM骨架从零到一的完整自举过程。主线代码里这个函数还会有一些平台差异的包装比如某些架构要预先处理pci、ahci之类的总线扫描但核心骨架是这四个。后面我按这个路径慢慢拆一个环节一个环节地讲透。2. DM的三件套driver、uclass_driver、udevice是怎么咬合的2.1 三组结构体的职责划分要理解DM骨架必须先把三个核心概念刻在脑子里struct driver、struct uclass_driver、struct udevice。这三者各有分工我用大白话给你捋一遍。struct driver描述的是“一个具体的驱动”它知道自己支持哪些compatible字符串of_match知道它属于哪个uclassid知道bind和probe的回调函数在哪以及它期望多大的私有数据区priv_auto。换句话说它是“一张驱动卡片”记录了硬件型号与实现代码的对应关系。struct uclass_driver描述的是“一类设备的公共行为”。比如所有LED设备都有一个“点亮/熄灭”或“设置亮度”的接口所有串口设备都有“读一个字符/写一个字符”的接口。这些公共操作函数、公共数据的大小、设备probe前后需要执行的公共回调全都放在uclass_driver里。它是“一张类目卡片”定义了这类设备的外在行为。struct udevice则是“一个具体的设备实例”。它包含了这个设备属于哪个驱动、挂在哪个父设备下面、归属哪个uclass、设备的私有数据和平台数据在哪、当前生命周期状态是否已probe、是否已bind等等。它是设备模型里的“实体卡片”对应当前硬件上真实存在的一个节点。三者的关系是设备树里来了一个节点DM拿着节点里的compatible去匹配driver匹配成功后分配出一个udevice实例把driver、uclass都挂到实例上之后再用这个实例去跟uclass打交道统一走公共接口。2.2 注册宏的“魔法”一般的驱动代码里很少会直接操作上面三个结构体而是通过宏来声明。比如U_BOOT_DRIVER(gpio_led) { .name gpio_led, .id UCLASS_LED, .of_match gpio_led_ids, .probe gpio_led_probe, .priv_auto_alloc_size sizeof(struct gpio_led_priv), };U_BOOT_DRIVER这个宏做的事本质上就是把这个变量放到一个特殊的段里链接器会把所有驱动变量集中到一段连续的地址空间。也就是说DM框架“知道的”驱动并不是运行时一个个注册进去的而是编译链接时就全部静态登记在册的运行时的匹配动作只是在“已知全集”里找出符合条件的那个。同理UCLASS_DRIVER(led)会把uclass_driver也放到专门的段里。u-boot里还有U_BOOT_DRIVER_ALIAS这样的宏用来把某个驱动的别名对应到另一个compatible上。这些宏的设计让驱动模型在无动态库、无模块加载的裸机环境里也能做到“按需查找”又不会像纯动态注册那样在ram里耗费额外的构建复杂度。这套思路你在Linux内核里也能看到类似影子但在u-boot这种极简环境下它更加紧凑。2.3 链表拓扑从root到叶子DM的骨架本质上是一棵多叉树树的根就是第一个udevice——root device。dm_init()里会把root device创建出来同时创建root uclass。每个设备实例里都有parent、sibling、child三个指针字段分别指向父设备、兄弟设备链表头和第一个子设备。而同一类设备又会通过uclass里的dev_head链表串起来形成“按类聚族”的第二维索引。这棵树的实体存在就是你平时在u-boot命令行下看到的dm tree输出——每一行是一个设备缩进表示层级关系冒号前是这个设备在同类设备中的序号冒号后是设备树路径或driver名。有了这棵树任何总线下的子设备都能顺着parent/child关系被遍历到任何uclass下的设备都能顺着dev_head被遍历到。两套索引各有各的用途一个管拓扑一个管行为。2.4 bind和probe为什么要拆开这是DM设计里最容易被忽略、却又最精髓的一个地方。bind的含义是“登记在册”只是把设备节点和driver建立关联分配udevice结构体把链表挂好probe的含义是“真正上电开工”调用驱动里的初始化函数分配私有数据操作寄存器让设备可用。两者拆开的理由很简单设备树扫描的时候可能只是想把硬件的拓扑结构摸清楚并不需要立刻把所有外设都初始化一遍。试想几百个设备节点如果扫描到哪个就初始化哪个有的外设电源没上有的外设时钟没配有的驱动初始化顺序不对整个启动流程瞬间就乱了。所以bind阶段只是“建目录”probe阶段才是“雇人上班”。和Linux的设备模型一对比你会发现u-boot的DM几乎是同构的只是裁剪得更狠。这也解释了为什么dm tree里能看到设备但wait列表里没有对应驱动工作后续内容时排查的第一步常常是“设备有没有probe”。一个设备可以已经bind但没有probe这种情况下它只是个“挂在册子上的名字”不能用于实际IO操作。3. 核心链路拆解dm_init_and_scan如何一步步把骨架搭起来3.1 dm_init先立起root设备这面大旗一切开始于dm_init()。它做的事情很单纯把root设备绑定到root_driver上同时创建root uclass。root设备是整个设备树的根它没有任何硬件意义纯粹是用来“挂东西”的虚拟节点。所有的顶层设备比如某个i2c控制器、某个gpio控制器、某个串口控制器都会挂在root设备下面。这里有一步很容易被忽略dm_init()里会调用device_bind_common()传进去的parent是NULLdriver是root_driver。也就是说root device是这个模型里唯一没有父节点的设备。同时root device也会挂到root uclass的链表上。这个uclass是UCLASS_ROOT在枚举uclass时你会经常见到它。dm_init()一旦成功意味着malloc池、链表、根节点都已经可用。以后所有设备的bind、probe都有了一个“悬挂点”。如果这一步失败后面基本全完所以u-boot在这边的错误处理也比较干脆直接返回错误启动流程卡死在早期。3.2 dm_scan_fdt设备树上的节点是怎么变成设备的dm_init()做完“地基”后紧接着就是dm_scan_fdt()。这是整个骨架搭建里工作量最大的一环。dm_scan_fdt()会从/根节点开始调用dm_scan_fdt_node()逐层遍历设备树。对每一个节点它都会做这么几件事第一读取该节点的compatible属性第二拿compatible去驱动列表里搜索匹配的driver第三如果匹配到了driver就调用device_bind_common()创建一个udevice实例并挂到parent下面第四如果这个节点本身也是一个“总线”设备比如i2c控制器、spi控制器、pci控制器那还得继续往它的子节点递归扫描。这里有非常关键的一点compatible匹配用的是lists_driver_lookup_node()它遍历的是编译时静态链接好的驱动数组。如果设备树里写了一个compatible但你的u-boot二进制里压根没有对应驱动结果就是——没有报错只是这个节点被静默忽略掉。这正是新手经常遇到的问题为什么我的节点在设备树里好好的dm tree里却看不到多半是compatible没有驱动去接。3.3 bind阶段内部到底干了哪些活device_bind_common()虽然不是特别长但每一步都有讲究。它首先会找driver然后分配一个struct udevice实例。分配完dev之后还要单独分配一个struct driver_info或platform data空间这块空间在扫描设备树时通常只起占位作用比如记录设备树节点的偏移量of_offset。接下来是链表挂接把新设备挂到父设备的子链表上同时挂到所属uclass的dev_head链表上。这两步做完设备在“拓扑索引”和“行为索引”里都有位置了。然后是uclass的回调和driver的bind回调。许多总线驱动会在自己的bind回调里做一点准备工作比如初始化bus的私有数据、解析bus的配置项。这里有一个容易混淆的概念of_offset和ofnode。在设备树扫描阶段新设备暂时没有读写设备树内容的接口它只知道自己的节点偏移。真正把设备树里的各种属性翻译成可用数据发生在ofdata_to_platdata()阶段。简单说bind阶段主要建立结构关系ofdata_to_platdata阶段把静态配置消化掉probe阶段才是硬件动作。3.4 probe时机bus设备如何带着子设备一起开工bind只建骨架probe才真正上电。但是偏偏很多设备树节点是层层嵌套的一个i2c控制器下面挂着触摸屏、传感器一个spi控制器下面挂着flash、屏幕。这些子设备什么时候probe答案藏在uclass_driver和device_probe的配合里。当一个总线设备比如i2c控制器完成probe后u-boot会自动去扫描它的子节点。具体机制是总线设备的uclass_driver里通常会设置.post_bind dm_scan_fdt_dev或者bus驱动在ofdata阶段调用相关扫描函数。正是这个递归扫描机制让一棵设备树能按层级逐步“苏醒”先probe根下的总线控制器总线控制器probe后扫描出下面的子设备子设备再probe再扫描孙设备。这也是为什么u-boot的DM看起来像“懒加载”子设备不会在dm_init_and_scan()阶段全部probe完而是以parent为序一层一层往下推。实际操作中如果某个父设备probe失败它的所有子设备都不会被处理这在调试时尤其值得注意。3.5 谁先谁后probe顺序为什么是门玄学DM框架并不保证所有设备按设备树里的物理顺序依次probe。它的probe顺序主要由这几方面决定uclass的初始化顺序、各驱动在链接段里的先后、以及设备在链表中的挂接顺序。实际操作中u-boot会使用dm_scan_plat()先扫一遍平台数据再扫设备树因此有些设备可能在设备树扫描之前就已经bind并probe了典型的就是某些启动早期必须要用的timer、serial设备。u-boot还引进了bootstage的概念本质上是给每个启动阶段打时间戳方便分析“花时间去哪了”。如果你在调probe顺序问题可以在init_sequence_r里观察initr_dm之后哪些子系统被拉起来、它们的依赖关系是什么。经验是尽量别依赖隐式的probe顺序必要的顺序依赖应该通过uclass的机制或者显式的设备查找API来解决比如在某个驱动probe里主动uclass_get_device()去找它依赖的设备。4. 实战让一个自研LED设备挂到DM树上4.1 设备树怎么加节点光看理论容易头晕咱们直接上手拉一个真实例子。假设板子上有一颗LED引脚接在gpio0的第5脚我想让它在u-boot阶段就能被DM框架识别成UCLASS_LED设备。设备树里先定义一个leds节点放在根节点或者某个gpio总线节点下。为了让例子清晰我放在根节点下leds { compatible gpio-leds; led_user { label user-led; gpios gpio0 5 GPIO_ACTIVE_HIGH; default-state off; }; };注意了compatible gpio-leds是给整个leds容器用的真正需要驱动匹配的其实是“容器”节点而不是led_user子节点。gpio-leds这类设备在u-boot里有现成的驱动这里我拿它当例子是因为足够直观。如果你要接一个完全自研的设备compatible换成你自己的字符串就行比如vendor,mywidget然后写一套自己的driver。4.2 驱动文件的最小骨架接下来写驱动放到drivers/led或drivers/misc下。核心就是定义好of_match表、probe回调和私有数据结构static const struct udevice_id gpio_led_ids[] { { .compatible gpio-leds }, { } }; struct gpio_led_priv { struct gpio_desc gpio; }; static int gpio_led_probe(struct udevice *dev) { struct gpio_led_priv *priv dev_get_priv(dev); /* 解析设备树里的gpios属性拿到gpio编号和有效电平 */ gpio_request_by_name(dev, gpios, 0, priv-gpio, GPIOD_IS_OUT); return 0; } U_BOOT_DRIVER(gpio_led) { .name gpio_led, .id UCLASS_LED, .of_match gpio_led_ids, .probe gpio_led_probe, .priv_auto_alloc_size sizeof(struct gpio_led_priv), };这段代码里有一个容易踩坑的地方priv_auto_alloc_size和per_device_auto的区别。前者是驱动自己的私有数据后者是uclass公共数据。如果你在probe里拿到的是dev_get_priv()那么数据大小必须定义成priv_auto_alloc_size如果你要用uclass公共数据得用dev_get_uclass_priv()并在uclass_driver里设置per_device_auto_alloc_size。两个分不清轻则数据错位重则内存踩掉程序直接崩。4.3 uclass_driver也要注册光有driver还不够还要注册一个uclass_driver。如果LED类在u-boot里已经有了那就复用如果是你全新的设备类就得手动加上UCLASS_DRIVER(led) { .name led, .id UCLASS_LED, .post_bind dm_scan_fdt_dev, .per_device_auto_alloc_size sizeof(struct led_uclass_priv), };.post_bind dm_scan_fdt_dev这行非常重要它就是让这个类下的设备在完成bind后继续递归扫描子节点的钩子。如果你忘了这行子设备永远绑不上。这是很多总线驱动写完之后子设备不出现的热门原因。你还需要在include/dm/uclass-id.h里增加UCLASS_LED这样一个枚举值如果它还不存在的话。4.4 编译和验证用dm tree看到你的设备编译烧录之后在u-boot命令行里敲dm tree如果一切正常你会看到自己的leds节点出现在设备树层级里driver名是gpio_led状态列显示如果probe过会是probed。再敲dm uclass可以看到LED这个uclass下的设备列表。如果节点出现但状态不是probed说明bind成功了但probe还没跑最常见的原因是驱动里的probe返回了错误比如gpio_request_by_name失败了。我在实际调试里还会在probe函数里加一个printf(%s: probed\n, __func__)用串口确认驱动到底有没有执行到。别嫌这种土办法低级嵌入式调试很多时候最直接的方式就是加打印看路径走到哪一步。dm tree只能告诉你结果不能告诉你probe里的哪一行出错。5. 调试与避坑实录骨架搭好后最容易卡住的地方5.1 设备树根本没生效先查CONFIG和加载路径很多人改了设备树、加了驱动代码结果dm tree里空空如也第一时间怀疑自己的驱动写错了。其实很多时候是设备树本身就没有被u-boot使用。要确认三件事第一板级config里有没有定义CONFIG_OF_CONTROL第二使用的设备树有没有被编译进u-boot镜像并正确传递第三CONFIG_DEFAULT_DEVICE_TREE指向的dts文件是不是你改的那个。u-boot里设备树可以以独立dtb形式由bootloader加载也可以链接进u-boot二进制本身。如果你是后者记得改完dts之后要重新编译dtb并确保镜像更新了。最简单的方法是在命令行敲fdt addr看FDT地址再fdt list /看根节点下的子节点是否包含你加的内容。FDT都没加载DM再怎么扫描也是空中捉影。5.2 compatible不匹配为什么dm tree里看不到你的节点如果FDT正常、节点确实存在但dm tree里看不到九成是compatible匹配失败。dm扫描时拿节点上的compatible去驱动表里一个个比对比对规则是字符串完全相等或者通过U_BOOT_DRIVER_ALIAS定义的别名。有一个极隐蔽的问题设备树上compatible写的是vendor,mywidget驱动里of_match表写的是vendor,my-widget或者大小写了一丁点不一样就匹配不上而且不会有任何提示。我的排查建议是直接在dm_scan_fdt_node或lists_driver_lookup_node里临时加打印把当前节点的compatible打印出来再去看驱动表里都有哪些compatible。当然更省事的办法是在dm tree确认设备是否存在如果不存在再查驱动注册段里的符号。平台相关的dts目录和驱动文件目录有时候在仓库里离得很远命名不一样的情况特别常见。5.3 probe失败却不见报错设备绑上了、也进probe了但probe返回了错误dm tree里设备状态不是probed。麻烦的是有些错误被上层吞掉你不一定能看到明显日志。我遇到过一个案例某spi flash的probe里调用spi_get_bus_and_cs()返回值没检查导致后续操作全部走偏但板子启动表面还正常直到你访问flash才崩。处理这种问题标准做法是打开CONFIG_DM_DEBUG或者CONFIG_DEBUG_UART把DM内部日志打开它会打印怀疑点的详细信息。还有一个笨但有效的办法在probe里用ret逐段检查一旦ret 0就马上printf出来并返回。不要相信任何“看起来正常”的启动错误值没检查就是埋雷。5.4 priv_auto和per_device_auto混淆我曾经被这个问题坑过整整一下午。给一个自研的pinctrl驱动设置了priv_auto_alloc_size又在uclass_driver里设置了per_device_auto_alloc_size代码里一会儿用dev_get_priv一会儿用dev_get_uclass_priv数据总是对不上。其实这两个空间是完全独立的前者是driver私有后者是uclass公共。同类设备也许希望共享部分数据所以uclass公共区通常是给uclass的回调函数用的。你自定义的驱动代码里只碰priv区就好uclass区留给框架层的回调去用。调试时最直观的办法是打印两个指针的地址值dev_get_priv(dev)和dev_get_uclass_priv(dev)能看到它们差多少。它们可能紧挨着分配也可能中间夹着其他数据。如果发现某个字段被莫名篡改优先怀疑是不是越界访问了这两块区域。malloc池里的越界不像操作系统里有段错误它只是悄悄地把数据搞脏极其难抓。5.5 在板级代码里如何拿到你自己的设备调起来之后下一步自然是想在任意代码里使用这个设备。DM没有提供一个“谁能用一个设备名就能查到”的万能接口但提供了按uclass和序号查询的API。比如LED设备遍历方式就是struct udevice *dev; uclass_first_device(UCLASS_LED, dev); for (; dev; uclass_next_device(dev)) { /* 拿到每个LED设备 */ }如果是按设备树路径或节点找设备可以用struct udevice *dev; uclass_get_device_by_ofnode(UCLASS_LED, ofnode, dev);按序号用uclass_get_device(UCLASS_LED, 0, dev)就能拿到第一个LED设备。但要记住uclass_get_device会触发probe如果设备probe不了它返回的就不是成功的udevice。所以在代码里一定要检查返回值哪怕只是打印一行错误也好过静默失败。我个人的习惯是在板级初始化代码里写一个小的helper函数封装“按label查LED设备”的流程。因为设备树里的label字符串和设备编号不是一回事直接按序号找容易串。把label比对做成一个函数后面不管在bootloader命令里还是后续的fastboot、android boot阶段调用起来都清爽得多。再分享一个真实踩坑我在一个项目里把设备树里gpio-leds节点加到了某个i2c总线的子节点下面结果DM扫描顺序变成“等i2c控制器probe之后才去扫描子节点”而i2c控制器probe又依赖某个gpio那个gpio恰恰又挂在leds节点下面管理的引脚上——死锁了。这种交叉依赖在设备树里特别容易发生排查半天才发现是层级放错了。设备树层级不只是给人看的它直接决定了DM的扫描顺序和依赖关系节点该挂在哪一层必须在画dts之前想清楚。如果说这篇文章能给你留下一个记忆点那就是DM的骨架看起来是几段代码本质是一个有序的构建过程。先从init_sequence_r里找到initr_dm看着dm_init把根立起来再顺着dm_scan_fdt看设备树节点一个个变成udevicebind的时候挂链表probe的时候才摸硬件。你不需要背下所有函数名但需要掌握这条主线的节奏谁来创建root谁来扫描节点谁来匹配driver谁来分配私有区谁触发子节点递归。这套节奏吃透了后面再看pinctrl、clk、reset、syscon这些框架基本都是一脉相承的套路无非是uclass变多了、总线种类变多了、回调钩子变勤了而已。我实际用下来最大的感受就是u-boot的DM不是一套摆设它是整个后续开发的地基。调试任何一个外设问题第一步永远是先看dm tree第二步看节点在不在、probe没probe第三步打开打印看probe走到哪。顺着这个流程来很多问题会自己浮出来。反过来如果你连dm tree都没看过上来就翻寄存器手册那你很可能在跟一个根本不存在的设备做斗争。