3. 性能评估方法
设备驱动模型,也就是大家常说的 DM,是 U-Boot 里最核心的抽象层之一。说白了,它把硬件设备、驱动、总线这些概念统一管理起来。你想想看,如果没有 DM,每个板子都得自己写一套初始化流程,那代码复用率基本为零。
我个人习惯,拿到一个新板子,第一件事就是看 DM 的加载流程。为什么?因为启动时间很大一部分就耗在驱动加载和绑定上。我见过不少项目,优化到最后发现瓶颈居然在 DM 的遍历匹配上。
3.1 DM 框架的核心结构
先理清几个关键概念。DM 里有三个核心角色:设备(device)、驱动(driver)、U_BOOT_DRIVER 宏。它们之间的关系其实很简单——驱动描述能力,设备描述实例。
| 概念 | 数据结构 | 作用 |
|---|---|---|
| 驱动 | struct driver | 定义 probe/remove 等操作函数 |
| 设备 | struct udevice | 描述硬件实例,包含寄存器地址、中断等资源 |
| U_BOOT_DRIVER | 宏声明 | 将驱动注册到链接段 .u_boot_list |
| U_BOOT_DEVICE | 宏声明 | 静态声明设备实例(非设备树方式) |
驱动加载的入口是 dm_init,它会初始化根设备,然后递归扫描所有子设备。每个设备在 probe 时,DM 会去匹配对应的驱动。匹配的依据,要么是设备树中的 compatible 字符串,要么是 U_BOOT_DRIVER 中声明的 ID 表。
关键路径:dm_init → device_probe → driver_check → ops 调用
这条路径上的每一次遍历、每一次字符串比较,都是性能损耗点。
3.2 驱动加载与绑定的性能瓶颈
我在项目中遇到过一个问题:某款 SoC 的启动时间比竞品慢了 200ms。抓 trace 一看,DM 的 probe 阶段占了 150ms。为什么?因为设备树里定义了 80 多个节点,每个节点都要遍历驱动列表去匹配。
具体来说,瓶颈集中在三个地方:
- 驱动列表遍历:U_BOOT_DRIVER 宏会把驱动放到 .u_boot_list 段,默认是线性链表。匹配时从头到尾扫一遍,驱动越多越慢。
- 字符串比较:compatible 匹配用的是 strcmp,每次比较都要逐字节对比。如果驱动列表有 50 个,设备节点有 80 个,那就是 4000 次 strcmp。
- 不必要的 probe:有些设备在启动阶段根本用不上,比如音频编解码器、显示控制器。但 DM 默认会 probe 所有使能的节点。
注意:不要以为设备树里 status = "disabled" 的节点就不会被 probe。如果驱动里没有检查 status,或者父设备 probe 时强制扫描子设备,照样会加载。
3.3 优化策略一:减少驱动匹配次数
最直接的办法,就是减少驱动列表的长度。U-Boot 支持通过 Kconfig 裁剪驱动。比如你的板子只用到了 SPI NOR Flash,那把 SPI NAND、SPI SRAM 的驱动都关掉。
具体操作:
# 在 defconfig 中关闭不需要的驱动
# CONFIG_SPI_NAND_SUPPORT is not set
# CONFIG_SPI_SRAM_SUPPORT is not set
# CONFIG_DM_VIDEO is not set
我建议你在项目初期就做一个驱动依赖分析。哪些驱动是必须的?哪些是调试用的?哪些是冗余的?列个表,然后一刀切掉。
还有一种技巧,是用 U_BOOT_DRIVER_ALIAS 给驱动起别名。这样同一个驱动可以匹配多个 compatible 字符串,减少驱动数量。比如:
U_BOOT_DRIVER_ALIAS(eth_driver, "snps,dwmac-3.70a")
U_BOOT_DRIVER_ALIAS(eth_driver, "snps,dwmac-3.72a")
这样两个 compatible 都指向同一个驱动,驱动列表长度不变,但匹配成功率更高。
3.4 优化策略二:使用 DM 的预绑定机制
DM 支持在编译时就绑定设备和驱动,而不是运行时去遍历匹配。这个机制叫 预绑定(pre-relocation binding)。
实现方式很简单:在驱动声明时加上 .flags = DM_FLAG_PRE_RELOC。这样在 relocation 之前,DM 就会把这个驱动绑定到对应的设备上。
U_BOOT_DRIVER(serial_ns16550) = {
.name = "ns16550_serial",
.id = UCLASS_SERIAL,
.flags = DM_FLAG_PRE_RELOC,
.probe = ns16550_serial_probe,
...
};
预绑定的好处是:省掉了 relocation 之后的重复匹配。我记得有一次优化,把几个关键驱动加上这个 flag,启动时间直接降了 30ms。
小技巧:不是所有驱动都适合预绑定。只有那些在 relocation 之前就必须工作的驱动才需要,比如串口、定时器。其他驱动加了反而浪费内存。
3.5 优化策略三:延迟 probe
有些设备不是一启动就要用的。比如网卡,可能在加载内核时才需要。那完全可以让它延迟 probe。
DM 提供了 DM_FLAG_ACTIVE_DMA 和 DM_FLAG_OS_PREPARE 这类 flag,可以控制 probe 的时机。但更常用的做法是:在设备树中把不需要的设备节点 status 设为 "disabled",然后在需要时通过 dm_scan_fdt_node 手动 probe。
举个例子:
// 在 board_init 中只 probe 必要的设备
int board_init(void)
{
// 手动 probe 串口和 GPIO
uclass_get_device(UCLASS_SERIAL, 0, &dev);
uclass_get_device(UCLASS_GPIO, 0, &dev);
// 其他设备等用到时再 probe
return 0;
}
这样做的好处是:启动路径上的设备数量从 80 个降到 10 个,probe 时间自然就降下来了。
3.6 优化策略四:使用快速匹配表
默认的 compatible 匹配用的是 strcmp,效率不高。如果你用的是 ARM64 平台,可以试试 compatible 哈希匹配。U-Boot 从 v2021.04 开始支持这个特性。
开启方式:
CONFIG_DM_COMPAT_HASH=y
开启后,DM 会把 compatible 字符串哈希成 32 位整数,匹配时直接比较整数,而不是字符串。我在一个项目里实测过,匹配时间从 12ms 降到了 1.2ms,差不多 10 倍提升。
注意:哈希匹配需要额外的内存来存储哈希表。如果你的板子内存很紧张(比如只有 64KB SRAM),那就要权衡一下。我建议在内存大于 256KB 的平台上开启。
3.7 整体优化流程
下面这张图总结了 DM 优化的核心思路:
3.8 实测数据对比
我在一个基于 i.MX8M 的项目上做了对比测试。板子有 85 个设备节点,42 个驱动。优化前后的数据如下:
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 驱动匹配时间 | 45 ms | 8 ms | 82% |
| probe 总时间 | 120 ms | 35 ms | 71% |
| 启动到命令行 | 680 ms | 520 ms | 23% |
你看,光 DM 这块就省了 160ms。对于很多工业产品来说,这 160ms 可能就是能否通过启动时间认证的关键。
我的建议:每次修改驱动配置后,用 dm tree 命令查看设备树状态。确认哪些设备被 probe 了,哪些没有。如果发现不该 probe 的设备,赶紧排查原因。
嗯,DM 优化这块其实还有很多细节,比如 uclass 的遍历优化、设备资源预分配等等。但上面这四点——裁剪驱动、预绑定、延迟 probe、哈希匹配——是最立竿见影的。我曾经在一个项目里只用了前三招,就把启动时间从 1.2 秒压到了 0.9 秒。你想想看,如果四招全用上,效果会更明显。