4. Bootloader裁剪:U-Boot启动流程分析、U-Boot配置裁剪、驱动与命令裁剪、启动时间优化
Bootloader 是系统启动的第一段代码。说白了,它负责把内核从闪存里捞出来,放到内存里,然后跳过去执行。U-Boot 是目前嵌入式领域用得最多的 Bootloader,功能强大,但也很臃肿。我见过不少项目,U-Boot 启动要花两三秒,甚至更久。对于消费类产品,这个时间用户是能感知到的。
所以,裁剪 U-Boot 是系统裁剪的第一步。你想想看,如果 Bootloader 本身就慢吞吞的,后面再怎么优化内核也是白搭。这一章,我们就来拆解 U-Boot 的启动流程,看看哪些地方可以动刀子。
核心观点:U-Boot 裁剪的目标不是删代码,而是删掉那些「你用不到」的功能。每一条被裁剪的命令、每一个被移除的驱动,都在为启动时间做贡献。
4.1 U-Boot 启动流程分析
要裁剪,先得知道它干了什么。U-Boot 的启动大致分为两个阶段:SPL(Secondary Program Loader)和 U-Boot proper。SPL 是精简版,负责初始化最小硬件,然后加载 U-Boot proper。U-Boot proper 才是我们通常说的那个命令行交互环境。
我个人习惯把启动流程画成一张图,这样思路更清晰。下面这张图展示了从芯片上电到内核启动的完整路径。
SPL 阶段通常只做三件事:初始化时钟、DDR 和存储控制器,然后从闪存读取 U-Boot proper 到 DDR。这个阶段代码量很小,但却是启动时间的关键。我记得有一次,SPL 里因为多了一个 GPIO 初始化,导致启动慢了 50 毫秒。对于量产产品,这个时间就是成本。
4.2 U-Boot 配置裁剪
U-Boot 的配置系统基于 Kconfig,和 Linux 内核类似。你可以在 make menuconfig 里勾选或取消功能。但很多人不知道,U-Boot 的配置项比内核还琐碎。我建议你从 include/configs/ 目录下的板级头文件入手,那里定义了很多宏开关。
举个例子,如果你的产品只用到了网络启动,那文件系统相关的命令就可以全部关掉。下面这张表列出了常见的可裁剪功能。
| 功能模块 | 配置宏 | 裁剪建议 | 节省空间 |
|---|---|---|---|
| 文件系统支持 | CONFIG_CMD_EXT4 / CONFIG_CMD_FAT | 如果只用 TFTP 或 NFS,可以关掉 | ~50KB |
| 网络协议栈 | CONFIG_CMD_NET / CONFIG_CMD_PING | 保留 TFTP 即可,PING 和 DHCP 按需 | ~30KB |
| USB 支持 | CONFIG_USB_STORAGE / CONFIG_CMD_USB | 量产阶段通常不需要 USB 烧录 | ~80KB |
| 命令行编辑 | CONFIG_CMDLINE_EDITING | 调试阶段保留,量产可关 | ~10KB |
| 环境变量保存 | CONFIG_ENV_IS_IN_MMC / CONFIG_ENV_IS_IN_SPI_FLASH | 只保留一种存储介质 | ~5KB |
我的经验:裁剪配置时,不要一次性关掉太多功能。每关一个,就编译一次,看看能不能正常启动。我曾经一次性关掉了所有文件系统支持,结果 U-Boot 连环境变量都读不出来了——因为环境变量是存在 FAT 分区里的。
4.3 驱动与命令裁剪
U-Boot 的驱动模型(Driver Model)在 2016 年之后逐渐成为主流。它让驱动管理更规范,但也带来了额外的开销。如果你用的是旧版 U-Boot,可以考虑升级到支持 Driver Model 的版本,因为它的裁剪粒度更细。
命令裁剪是最直接的。U-Boot 默认编译了上百条命令,但你的产品可能只用到了 bootm、tftp、setenv 和 saveenv。其他的,比如 md、mw、cmp 这些内存调试命令,量产阶段完全可以去掉。
具体怎么做?在 include/configs/your_board.h 里,找到 #define CONFIG_CMD_XXX 这样的宏,把它注释掉就行。或者,在 defconfig 文件里设置 # CONFIG_CMD_XXX is not set。
# 示例:裁剪掉不必要的命令
# 在 defconfig 中添加:
# CONFIG_CMD_BOOTD is not set
# CONFIG_CMD_ECHO is not set
# CONFIG_CMD_ITEST is not set
# CONFIG_CMD_LOADB is not set
# CONFIG_CMD_LOADS is not set
# CONFIG_CMD_MEMORY is not set
# CONFIG_CMD_MISC is not set
# CONFIG_CMD_NFS is not set
# CONFIG_CMD_PING is not set
# CONFIG_CMD_SOURCE is not set
# CONFIG_CMD_UNZIP is not set
注意:有些命令之间有依赖关系。比如 CONFIG_CMD_BOOTM 依赖于 CONFIG_CMD_BOOTD 或 CONFIG_CMD_BOOTZ。关掉之前,最好先看看 Kconfig 里的依赖链。我踩过这个坑,关掉 CONFIG_CMD_BOOTM 后,U-Boot 直接无法启动内核。
4.4 启动时间优化
启动时间优化是裁剪的最终目的。我一般把优化分为两类:软件优化和硬件优化。软件优化是改代码和配置,硬件优化是调整时钟和时序。
先说说软件优化。U-Boot 启动时,会做很多不必要的延时。比如,有些板子在初始化网卡时,会等待 PHY 自协商完成,这个等待时间可能长达 2 秒。如果你的产品不需要网络启动,完全可以把网卡初始化放到内核里去做。
另一个常见的优化点是环境变量。U-Boot 默认会从存储介质读取环境变量,如果存储介质响应慢,这个时间就很可观。我建议把环境变量编译进 U-Boot 镜像里,省去读取时间。
// 示例:将环境变量编译进镜像
// 在 include/configs/your_board.h 中:
#define CONFIG_ENV_IS_NOWHERE
#define CONFIG_EXTRA_ENV_SETTINGS \
"bootcmd=tftp 0x82000000 zImage; bootz 0x82000000\0" \
"bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rw\0"
硬件优化方面,我建议检查 SPL 阶段的时钟配置。很多开发板为了兼容性,把时钟频率设得很保守。如果你的硬件是定制的,可以适当提高 CPU 和 DDR 的频率。但要注意,频率提高后,DDR 的时序参数也要相应调整,否则会不稳定。
下面这张表总结了常见的启动时间优化手段和效果。
| 优化手段 | 优化对象 | 预期效果 | 风险等级 |
|---|---|---|---|
| 裁剪不必要的命令 | U-Boot proper | 减少 100-300ms | 低 |
| 移除 PHY 自协商等待 | 网卡初始化 | 减少 1-2s | 中 |
| 环境变量编译进镜像 | 环境加载 | 减少 50-200ms | 低 |
| 提高 SPL 时钟频率 | SPL 阶段 | 减少 30-100ms | 高 |
| 使用快速启动模式 | U-Boot proper | 减少 200-500ms | 中 |
我的建议:优化启动时间时,先用示波器或逻辑分析仪测量各个阶段的实际耗时。不要凭感觉优化。我曾经以为网卡初始化是瓶颈,结果一测,发现是环境变量读取花了 800ms。换了存储介质后,问题就解决了。
最后,我想强调一点:U-Boot 裁剪不是一锤子买卖。产品开发的不同阶段,对 U-Boot 的需求是不一样的。调试阶段,你需要保留所有命令和驱动;量产阶段,才需要极致裁剪。所以,我建议维护两套配置:一套是开发配置,一套是量产配置。这样既能保证开发效率,又能保证产品性能。