
1. 从 board_init_r 切入搞懂 u-boot 设备模型到底在干什么玩过 u-boot 移植的人都有一个共同的体感SPL 和 board_init_f 阶段像是在“搭毛坯房”而到了board_init_r才真正开始“精装修”。这个精装修的核心骨架就是 u-boot 的设备模型也就是大家常说的driver model简称dm。很多人第一次看board_init_r的源码时是懵的一堆init_sequence_r[]函数指针排着队执行initr_xxx一个接一个中间还穿插着dm_init_and_scan、dm_scan_platdata、dm_scan_fdt这些调用。它们到底谁先谁后设备是什么时候被“发现”的驱动是什么时候被“绑定”的uclass又是在哪一步被创建出来的这些问题如果不搞清楚移植新板子时遇到“设备树里写了节点但驱动没 probe”这种问题就只能靠猜。这篇内容就是把这套骨架拆开给你看。我会从board_init_r的执行顺序讲起把dm 驱动骨架的搭建过程一步步还原出来包括gd-dm_root是怎么建立的、uclass是怎么注册的、设备是怎么从设备树里被扫描出来的、驱动是怎么和设备匹配上的、probe又是在什么时机被触发的。中间会穿插我在实际移植中踩过的坑以及一些看源码时容易忽略但非常关键的细节。适合的读者是已经能编译 u-boot、能跑起来、但想深入理解 dm 机制的人正在移植新板子、设备树写了但驱动不工作的人以及想给 u-boot 写自己驱动但不知道从哪下手的人。不需要你精通 C 语言的高级技巧但至少要能看懂结构体、函数指针和链表操作。下面这张表先给你一个全局印象board_init_r里和 dm 相关的关键调用大致是这个顺序阶段关键函数主要动作早期initr_dm初始化 dm 根节点建立gd-dm_root早期dm_init_and_scan扫描 platdata 和设备树绑定设备与驱动中期initr_xxx系列各子系统初始化触发对应 uclass 的 probe后期dm_scan_other扫描非设备树来源的设备这个顺序不是随便排的每一步都有它存在的理由。接下来我逐个拆。2. board_init_r 的执行脉络与 dm 的插入点2.1 init_sequence_r 数组到底排了什么board_init_r的主体非常简洁核心就是遍历init_sequence_r[]这个函数指针数组。这个数组定义在common/board_r.c里不同版本略有差异但 dm 相关的几个关键节点基本固定。我拿一个比较典型的版本给你看static init_fnc_t init_sequence_r[] { initr_trace, initr_reloc, initr_caches, initr_reloc_global_data, initr_barrier, initr_malloc, initr_bootstage, initr_console_record, initr_bootstage, initr_dm, /* dm 根节点初始化 */ initr_of_live, initr_sandbox, initr_board_init_r, initr_serial, initr_announce, initr_ethaddr, initr_net, ... };注意initr_dm的位置它在initr_malloc之后在initr_serial之前。这个位置非常讲究。initr_malloc先把堆准备好因为 dm 初始化过程中要分配大量结构体内存而initr_serial需要用到 dm 里的串口设备所以 dm 必须在它之前就绪。提示如果你在移植时发现串口在board_init_r早期就不可用先检查initr_dm是否被正确加入序列以及它前面的initr_malloc是否成功。2.2 initr_dm 做了什么initr_dm本身很短核心就是调用dm_init_and_scan。但在这之前它会先设置gd-dm_root相关的东西。我把它简化一下static int initr_dm(void) { int ret; ret dm_init_and_scan(false); if (ret) return ret; return 0; }看起来简单但dm_init_and_scan里面干的事可不少。它先调用dm_init再调用dm_scan_platdata然后dm_scan_fdt最后dm_scan_other。这几个函数的顺序和职责就是整个 dm 骨架的核心。2.3 为什么 dm 初始化要放在这个位置有人可能会问为什么不在board_init_f阶段就把 dm 建好答案是内存。board_init_f阶段运行在只读的早期环境里堆还没准备好而 dm 需要动态分配大量结构体。board_init_r阶段initr_malloc已经把堆初始化完毕这时候才能安全地 malloc。另一个原因是设备树。board_init_f阶段设备树可能还没完全解析好而 dm 扫描设备树需要gd-fdt_blob已经就位。到了board_init_r设备树已经通过initr_reloc_global_data重定位完成可以安全访问。3. dm_init把根节点和 uclass 链表立起来3.1 gd-dm_root 的建立过程dm_init是整个 dm 骨架的第一根桩。它的核心任务是创建 dm 的根设备也就是gd-dm_root。这个根设备不是一个真实的硬件设备而是一个逻辑上的“根”所有其他设备都挂在它下面。int dm_init(bool of_live) { int ret; if (gd-dm_root) { dm_warn(Virtual root driver already exists!\n); return -EINVAL; } INIT_LIST_HEAD(DM_UCLASS_ROOT_NON_CONST); INIT_LIST_HEAD(DM_ROOT_NON_CONST-dev_head); ... ret device_bind_by_name(NULL, false, root_info, DM_ROOT_NON_CONST); ... }这里有几个关键点。第一DM_UCLASS_ROOT_NON_CONST是一个全局的 uclass 链表头所有 uclass 都会挂到这个链表上。第二DM_ROOT_NON_CONST-dev_head是根设备的子设备链表头所有顶级设备都挂在这里。第三device_bind_by_name把根驱动绑定到根设备上。3.2 uclass 链表与 uclass 驱动uclass 是 dm 里非常核心的概念。你可以把它理解成“设备类别”比如所有串口设备属于UCLASS_SERIAL所有 GPIO 控制器属于UCLASS_GPIO。每个 uclass 有一个uclass_driver定义了这类设备的通用操作比如probe、remove、post_probe等。uclass 的注册有两种方式。一种是静态注册通过UCLASS_DRIVER宏把uclass_driver结构体放到特定的段里链接时自动收集。另一种是动态创建当第一个属于某个 uclass 的设备被绑定时如果对应的 uclass 还不存在就会自动创建。struct uclass_driver { const char *name; enum uclass_id id; int (*post_bind)(struct udevice *dev); int (*pre_unbind)(struct udevice *dev); int (*post_probe)(struct udevice *dev); int (*pre_remove)(struct udevice *dev); int (*child_post_bind)(struct udevice *dev); int (*child_pre_probe)(struct udevice *dev); int (*init)(struct uclass *class); int (*destroy)(struct uclass *class); ... };这个结构体里的回调函数就是 uclass 层面的“钩子”。比如post_probe会在该 uclass 下任何一个设备 probe 成功后调用适合做一些全局性的初始化。3.3 根驱动 root_driver 的特殊性根驱动定义在drivers/core/root.c里它的id是UCLASS_ROOT。这个驱动本身不做任何硬件操作它的存在只是为了给整个设备树提供一个挂载点。设备树里的根节点/对应的就是它。U_BOOT_DRIVER(root_driver) { .name root_driver, .id UCLASS_ROOT, .priv_auto sizeof(struct root_priv), };注意priv_auto这是 dm 里自动分配私有数据的机制。每个设备可以有自己私有的数据结构priv_auto指定大小dm 在绑定设备时自动 malloc。这个机制在写驱动时非常有用后面会详细讲。4. dm_scan_platdata 与 dm_scan_fdt设备从哪里来4.1 platdata 扫描老式板级信息的兼容路径dm_scan_platdata扫描的是通过U_BOOT_DEVICE宏静态定义的设备。这些设备信息不在设备树里而是直接写在 C 代码里。这种方式主要用于那些还没完全切换到设备树的板子或者一些特殊的、不适合放在设备树里的设备。U_BOOT_DEVICE(serial_plat) { .name serial_pl01x, .platdata serial_platdata, };dm_scan_platdata会遍历这些静态定义逐个调用device_bind_by_name把设备绑定到对应的驱动上。绑定成功后设备就挂在根设备下面等待后续 probe。注意platdata 方式定义的设备其platdata是静态分配的不需要 dm 再 malloc。但priv数据仍然会按priv_auto分配。4.2 设备树扫描dm_scan_fdt 的主流程dm_scan_fdt是重头戏。它遍历设备树把每个有compatible属性的节点转换成 dm 设备。核心流程是这样的从根节点开始遍历所有子节点。对每个子节点检查是否有compatible属性。如果有调用device_bind_driver_to_node或lists_bind_fdt尝试绑定。绑定成功后递归处理该节点的子节点。int dm_scan_fdt(const void *blob, bool pre_reloc_only) { return dm_scan_fdt_node(gd-dm_root, blob, 0, pre_reloc_only); }dm_scan_fdt_node里会调用lists_bind_fdt后者会遍历driver_info列表找到compatible匹配的驱动然后调用device_bind_with_driver_data。4.3 compatible 匹配的细节驱动和设备树的匹配靠的是compatible字符串。驱动里通过of_match数组声明自己支持哪些 compatiblestatic const struct udevice_id pl01x_serial_id[] { { .compatible arm,pl011, .data PORT_0 }, { .compatible arm,pl010 }, { } }; U_BOOT_DRIVER(serial_pl01x) { .name serial_pl01x, .id UCLASS_SERIAL, .of_match pl01x_serial_id, ... };匹配时dm 会拿设备树节点的compatible属性逐个和驱动of_match里的字符串比较。匹配上了就绑定匹配不上就跳过。这里有个容易踩的坑compatible属性可以是一个字符串列表dm 会按顺序尝试匹配。如果你的设备树里写了多个 compatible但驱动只匹配了其中一个那也能绑定成功。但如果一个都没匹配上设备就不会被创建后续自然也不会 probe。提示调试时可以用dm tree命令查看当前 dm 里的设备树结构用dm uclass查看 uclass 列表。这两个命令在CONFIG_CMD_DM打开后可用是排查 dm 问题的利器。5. 设备绑定与驱动匹配device_bind 的完整链路5.1 device_bind_common 做了什么所有绑定最终都会走到device_bind_common。这个函数是 dm 骨架里最核心的函数之一它完成了设备结构体的分配、初始化、挂载、以及和驱动的关联。static int device_bind_common(struct udevice *parent, const struct driver *drv, const char *name, void *platdata, ulong driver_data, ofnode node, uint of_platdata_size, struct udevice **devp) { struct udevice *dev; struct uclass *uc; int size, ret 0; if (devp) *devp NULL; if (!drv) return -EINVAL; ret uclass_get(drv-id, uc); if (ret) return ret; dev calloc(1, sizeof(struct udevice)); if (!dev) return -ENOMEM; dev-platdata platdata; dev-driver_data driver_data; dev-name name; dev-node node; dev-parent parent; dev-driver drv; dev-uclass uc; ... }这里有几个关键动作。第一uclass_get确保该驱动所属的 uclass 已经存在不存在就创建。第二calloc分配设备结构体。第三填充platdata、driver_data、node、parent、driver、uclass等字段。第四如果priv_auto不为零分配私有数据。第五把设备挂到父设备的dev_head链表和 uclass 的dev_head链表上。5.2 uclass_get 的自动创建逻辑uclass_get的逻辑是先在全局 uclass 链表里找对应 id 的 uclass找到就返回找不到就调用uclass_add创建。uclass_add会从uclass_driver段里找到对应 id 的驱动分配uclass结构体初始化链表然后调用uclass_driver-init回调。static int uclass_add(enum uclass_id id, struct uclass **ucp) { struct uclass_driver *uc_drv; struct uclass *uc; int ret; *ucp NULL; uc_drv lists_uclass_lookup(id); if (!uc_drv) return -ENOENT; uc calloc(1, sizeof(*uc)); if (!uc) return -ENOMEM; INIT_LIST_HEAD(uc-dev_head); INIT_LIST_HEAD(uc-sibling_node); uc-uc_drv uc_drv; list_add_tail(uc-sibling_node, DM_UCLASS_ROOT_NON_CONST); if (uc_drv-init) { ret uc_drv-init(uc); if (ret) goto fail; } *ucp uc; return 0; }这个自动创建机制的好处是你不需要手动注册 uclass只要定义了UCLASS_DRIVER第一个设备绑定时它就会自动出现。5.3 绑定时的父子关系与链表操作设备绑定后会同时挂在两个链表上一个是父设备的dev_head一个是 uclass 的dev_head。这两个链表分别用于不同的遍历场景。父设备链表用于递归遍历设备树结构uclass 链表用于按类别遍历设备。list_add_tail(dev-sibling_node, parent-child_head); list_add_tail(dev-uclass_node, uc-dev_head);这种双链表设计是 dm 的一个巧妙之处。比如uclass_foreach_dev宏就是遍历 uclass 链表而device_foreach_child是遍历父设备链表。6. probe 时机设备什么时候真正工作6.1 probe 的触发条件设备被绑定后并不会立即 probe。probe 的触发有两种方式主动触发和被动触发。主动触发是调用device_probe(dev)这会直接执行驱动的probe函数。被动触发是在访问设备时如果设备还没 probedm 会自动先 probe 再执行操作。比如uclass_get_device就会在返回设备前确保它已经 probe。int device_probe(struct udevice *dev) { const struct driver *drv; int ret; int seq; if (!dev) return -EINVAL; if (dev-flags DM_FLAG_ACTIVATED) return 0; drv dev-driver; ... if (drv-probe) { ret drv-probe(dev); if (ret) goto fail; } ... dev-flags | DM_FLAG_ACTIVATED; return 0; }注意DM_FLAG_ACTIVATED标志它保证同一个设备不会被 probe 两次。这个标志在排查“probe 没执行”或“probe 执行了两次”这类问题时非常关键。6.2 uclass 层面的 probe 钩子除了驱动自己的probeuclass 也有post_probe钩子。当某个设备 probe 成功后如果它所属的 uclass 定义了post_probe就会被调用。这个钩子适合做一些跨设备的初始化比如某个 uclass 下所有设备都 probe 完后统一配置一下。if (uc-uc_drv-post_probe) { ret uc-uc_drv-post_probe(dev); if (ret) goto fail; }6.3 各子系统初始化时的 probe 触发回到board_init_r的init_sequence_r[]initr_serial、initr_net、initr_mmc这些函数内部都会调用uclass_get_device或uclass_first_device从而触发对应设备的 probe。比如initr_serial会调用serial_init后者通过uclass_get_device_by_seq获取串口设备并 probe。static int initr_serial(void) { serial_initialize(); return 0; }serial_initialize里会遍历UCLASS_SERIAL下的所有设备逐个 probe。这就是为什么串口驱动必须在initr_serial之前完成绑定否则这里找不到设备。7. 实操给一块新板子搭 dm 驱动骨架7.1 设备树节点的编写要点假设你要给一块新板子加一个自定义的 GPIO 控制器设备树节点大概是这样my_gpio: gpio10000000 { compatible myvendor,my-gpio; reg 0x10000000 0x1000; gpio-controller; #gpio-cells 2; clocks clk 10; status okay; };几个关键点。compatible必须和驱动of_match里的字符串完全一致大小写敏感。reg指定寄存器基址和长度驱动 probe 时可以通过dev_read_addr获取。status必须是okay否则 dm 扫描时会跳过。7.2 驱动代码的骨架驱动代码的骨架大致如下struct my_gpio_priv { void __iomem *base; struct clk clk; }; static int my_gpio_probe(struct udevice *dev) { struct my_gpio_priv *priv dev_get_priv(dev); int ret; priv-base dev_read_addr_ptr(dev); if (!priv-base) return -EINVAL; ret clk_get_by_index(dev, 0, priv-clk); if (ret) return ret; ret clk_enable(priv-clk); if (ret) return ret; return 0; } static int my_gpio_of_to_plat(struct udevice *dev) { struct my_gpio_plat *plat dev_get_plat(dev); plat-base dev_read_addr(dev); return 0; } static const struct udevice_id my_gpio_ids[] { { .compatible myvendor,my-gpio }, { } }; U_BOOT_DRIVER(my_gpio) { .name my_gpio, .id UCLASS_GPIO, .of_match my_gpio_ids, .of_to_plat my_gpio_of_to_plat, .probe my_gpio_probe, .priv_auto sizeof(struct my_gpio_priv), .plat_auto sizeof(struct my_gpio_plat), .ops my_gpio_ops, };这里有几个关键字段。of_match是匹配表。of_to_plat在绑定后、probe 前调用用于把设备树信息转换成 platdata。probe是真正的初始化。priv_auto和plat_auto指定自动分配的大小。ops是操作函数集定义该驱动对外提供的接口。7.3 编译配置的检查清单驱动写完后编译配置要检查这几项CONFIG_DM必须打开这是 dm 的总开关。CONFIG_DM_GPIO必须打开这是 GPIO uclass 的开关。CONFIG_OF_CONTROL必须打开否则设备树不会被解析。CONFIG_SPL_DM如果 SPL 阶段也要用 dm这个也要打开。注意有些 uclass 有独立的配置项比如CONFIG_DM_SERIAL、CONFIG_DM_MMC。如果你的驱动属于某个 uclass记得把对应的配置打开否则 uclass 驱动不会被编译进去。8. 常见问题与排查技巧实录8.1 设备树节点写了但设备没出现这是最常见的问题。排查顺序是这样的用dm tree看设备是否在树里。如果不在说明绑定失败。检查compatible是否和驱动of_match完全一致。检查status是否为okay。检查驱动是否被编译进去可以用nm u-boot | grep my_gpio看符号是否存在。检查CONFIG_OF_CONTROL是否打开。如果设备在树里但没 probe那就是 probe 阶段的问题。检查probe函数是否返回了错误可以在probe里加printf或者用log_debug输出。8.2 probe 顺序不对导致依赖失败dm 里设备 probe 的顺序不是完全按设备树顺序来的而是按依赖关系。如果一个设备依赖另一个设备比如 GPIO 控制器依赖时钟控制器那时钟控制器必须先 probe。dm 通过uclass_get_device_by_phandle这类接口自动处理依赖但前提是驱动里正确声明了依赖。ret clk_get_by_index(dev, 0, priv-clk);这行代码会触发时钟设备的 probe。如果时钟设备还没绑定这里会失败。所以时钟设备的绑定必须在 GPIO 设备 probe 之前完成。绑定顺序由设备树扫描顺序决定一般父节点先于子节点但同级节点的顺序不保证。提示如果依赖关系复杂可以在驱动里用device_probe显式 probe 依赖设备或者调整设备树节点顺序。8.3 priv 数据被覆盖或为空dev_get_priv返回的指针如果为空通常是priv_auto没设置或者设置为零。检查驱动结构体里的priv_auto字段。另一个可能是probe被调用了两次第二次时 priv 被重新分配导致第一次的指针失效。用DM_FLAG_ACTIVATED可以避免重复 probe。8.4 常见问题速查表现象可能原因排查方法设备不在 dm tree 里compatible 不匹配对比设备树和 of_match设备在树里但没 probeprobe 返回错误在 probe 里加日志priv 为空priv_auto 未设置检查驱动结构体probe 执行两次重复调用 device_probe检查 DM_FLAG_ACTIVATEDuclass 不存在uclass_driver 未编译检查 CONFIG_DM_XXX设备树解析失败CONFIG_OF_CONTROL 未开检查配置9. 几个容易被忽略但很关键的细节9.1 of_to_plat 和 probe 的分工of_to_plat在设备绑定后、probe 前调用它的职责是把设备树里的信息解析到 platdata 里。probe则是真正的硬件初始化。这个分工的好处是platdata 可以在 probe 之前就被访问比如在child_pre_probe里。很多人把设备树解析放在probe里这也能工作但不符合 dm 的设计意图。如果父设备需要在子设备 probe 前访问子设备的 platdata就必须用of_to_plat。9.2 uclass 的 post_bind 和 child_post_bindpost_bind在设备绑定到 uclass 后调用child_post_bind在子设备绑定后调用。这两个钩子适合做一些绑定后的自动配置比如自动分配序列号、自动设置设备名称等。static int my_uclass_post_bind(struct udevice *dev) { if (dev-node) { /* 从设备树读取额外属性 */ } return 0; }9.3 dm 的内存分配策略dm 在绑定设备时会分配三类内存udevice结构体本身、platdata如果plat_auto非零、priv如果priv_auto非零。这些内存都是用calloc分配的在device_unbind时释放。如果设备很多内存占用会比较可观在内存紧张的板子上要注意。提示可以用dm mem命令查看 dm 的内存占用情况这个命令在调试内存问题时很有用。9.4 设备树的 live tree 与 flat treeu-boot 支持两种设备树访问方式flat tree 和 live tree。flat tree 直接访问gd-fdt_bloblive tree 则把设备树转换成结构体链表。initr_of_live负责 live tree 的初始化。live tree 访问更快但占用更多内存。在board_init_r里initr_dm在initr_of_live之前所以 dm 初始化时用的是 flat tree。这个细节在调试时很重要。如果你在probe里用ofnode相关接口要确认 live tree 是否已经初始化。如果没初始化ofnode会回退到 flat tree 访问。10. 我在实际移植中的几点体会第一次移植新板子时我习惯先把dm tree命令打开编译进去。这样板子一跑起来就能看到 dm 里到底有哪些设备、哪些 uclass、哪些驱动绑定成功了。这个命令帮我省了无数次的“盲猜”。另一个体会是设备树的compatible字符串一定要和驱动里的完全一致包括大小写和连字符。我曾经因为把my-gpio写成my_gpio排查了两个小时。后来养成了一个习惯设备树和驱动代码放在两个窗口里逐字符对比。还有一点probe函数里尽量不要做太重的初始化。dm 的 probe 可能会在系统启动的早期被调用这时候很多资源还没准备好。如果确实需要延迟初始化可以用DM_FLAG_PRE_RELOC或者把初始化拆成两步在probe里只做最基本的剩下的在后续的initr_xxx里做。最后分享一个小技巧如果某个设备 probe 失败但错误信息不明显可以在device_probe里加一行printf输出设备名称和返回值。这个改动很小但能快速定位是哪个设备、哪一步出了问题。调试完后记得删掉不然会影响启动速度。