4. Bootloader优化
Bootloader 是嵌入式系统的第一段代码。它负责初始化硬件,加载内核。说白了,它就是系统启动的「守门人」。
很多人觉得 U-Boot 差不多能用就行。但我个人习惯,一定会花时间打磨它。为什么?因为 Bootloader 的启动速度,直接决定了产品的「第一印象」。你想想看,一个摄像头按下电源键,5 秒后才出画面,用户早就没耐心了。
这一章,我们就来聊聊怎么把 U-Boot 调教得又快又稳。
4.1 U-Boot 启动流程分析
先搞清楚 U-Boot 是怎么跑起来的。我刚开始接触 U-Boot 时,看着那堆汇编代码一头雾水。后来拆开来看,其实就三个阶段。
4.1.1 Stage 1:汇编阶段(SPL/TPL)
芯片上电后,CPU 从 ROM 里执行固化代码。然后跳转到 SPL(Secondary Program Loader)。SPL 是 U-Boot 的精简版,它只做一件事:初始化 DDR 和时钟,然后把完整的 U-Boot 从 Flash 搬到内存里。
嗯,这里要注意。如果你的芯片支持 TPL(Tertiary Program Loader),那流程会更复杂一些。TPL 负责最基础的初始化,比如电源管理。SPL 再接手 DDR 初始化。我曾在 i.MX8M 平台上遇到过 TPL 阶段 DDR 时序配置不对,导致系统随机死机。排查了整整两天,最后发现是 datasheet 里一个时序参数写错了。
4.1.2 Stage 2:C 语言阶段(U-Boot Proper)
SPL 跳转到 U-Boot Proper 后,代码就进入 C 语言世界了。入口函数是 board_init_f()。它会做这些事情:
- 初始化串口(让你能看到打印信息)
- 初始化定时器
- 初始化 GPIO、I2C、SPI 等外设
- 搬移自身到内存的高地址(relocation)
Relocation 这一步很有意思。U-Boot 先把自己从 Flash 加载到内存低地址,运行一段后,再把自己搬到内存高地址。为什么这么折腾?因为低地址可能被内核占用,U-Boot 得给内核腾地方。
4.1.3 Stage 3:命令行与启动内核
Relocation 完成后,U-Boot 进入主循环。它会检查环境变量 bootcmd,然后执行里面的命令。默认情况下,它会尝试从各种介质加载内核:
# 典型的 bootcmd 流程
=> printenv bootcmd
bootcmd=run distro_bootcmd
# distro_bootcmd 会依次尝试:
# 1. 从 MMC 启动
# 2. 从 USB 启动
# 3. 从 PXE 启动
# 4. 从 DHCP 启动
如果所有介质都失败了,U-Boot 就停在命令行,等待用户输入。这也是调试时最常用的状态。
核心要点:U-Boot 的启动流程可以概括为「SPL 初始化 DDR → 加载 U-Boot Proper → Relocation → 执行 bootcmd」。理解这个流程,是优化的基础。
4.2 U-Boot 配置裁剪
U-Boot 默认配置很臃肿。它支持几十种文件系统、网络协议、命令集。但你的产品可能只需要其中 10%。裁剪掉不需要的功能,能显著减少 U-Boot 的体积和启动时间。
4.2.1 使用 menuconfig 进行配置
U-Boot 使用 Kconfig 系统,和 Linux 内核一样。运行 make menuconfig 就能进入图形化配置界面。
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
我个人习惯,先加载默认配置,然后逐项检查:
- 命令集(Commands):只保留必要的命令。比如
bootm、load、mmc、fatload。调试命令如md、mw、bdinfo可以去掉。 - 文件系统(File systems):只用 FAT 和 ext4 的话,就把其他文件系统关掉。
- 网络协议(Networking):如果产品不需要网络启动,直接关掉整个网络栈。
- 设备驱动(Device drivers):只保留板子上实际用到的外设驱动。
4.2.2 裁剪实例:去掉不必要的命令
我曾经给一个工业控制板做优化。默认配置编译出来 800KB。裁剪后只有 280KB。怎么做到的?
# 在 .config 中关闭不需要的命令
# CONFIG_CMD_BOOTD is not set
# CONFIG_CMD_BDI is not set
# CONFIG_CMD_CONSOLE is not set
# CONFIG_CMD_ECHO is not set
# CONFIG_CMD_EDITENV is not set
# CONFIG_CMD_FAT is not set (如果不用 FAT)
# CONFIG_CMD_EXT4 is not set (如果不用 ext4)
# CONFIG_CMD_NET is not set (如果不用网络)
注意,有些命令之间有依赖关系。比如 CONFIG_CMD_BOOTM 依赖 CONFIG_CMD_BOOTD。关掉一个,另一个可能也会失效。所以裁剪后一定要编译测试。
警告:裁剪时不要一刀切。建议先编译一次,记录原始大小。然后每次关掉一组功能,编译一次,确认功能正常。我曾经因为关掉了 CONFIG_CMD_GPIO,导致产线测试脚本无法运行,被追着改了一整天。
4.3 减少启动时间的技巧
启动时间优化,是 Bootloader 优化的核心目标。我总结了几条实战经验。
4.3.1 减少串口打印
串口打印很慢。115200 波特率下,打印 1000 个字符需要将近 100ms。U-Boot 默认会打印大量调试信息。关掉它们:
# 在 .config 中设置
CONFIG_SILENT_CONSOLE=y
CONFIG_SILENT_U_BOOT_ONLY=y
这样 U-Boot 就不会输出任何打印信息。但调试时建议保留,方便定位问题。
4.3.2 优化 DDR 初始化
DDR 初始化是启动过程中最耗时的环节之一。有些芯片的 DDR 初始化代码会做多次校准,耗时几百毫秒。如果你的硬件设计很成熟,可以跳过部分校准:
// 在 board 文件中设置
#define CONFIG_SYS_SDRAM_SIZE (512 * 1024 * 1024)
#define CONFIG_SYS_INIT_SP_ADDR 0x40000000
// 跳过 DDR 训练(仅适用于已知稳定的硬件)
// 在 SPL 中设置
#define CONFIG_SPL_SKIP_DDR_TRAINING
嗯,这里要特别小心。跳过 DDR 训练意味着你信任硬件设计。如果 PCB 走线有差异,可能会导致内存不稳定。我建议量产前做一次完整的 DDR 压力测试。
4.3.3 使用 Falcon Mode
Falcon Mode 是 U-Boot 的一个特性。它允许 SPL 直接加载内核,跳过 U-Boot Proper。这样能省掉 U-Boot Proper 的加载和初始化时间,大约 200-500ms。
# 启用 Falcon Mode
CONFIG_SPL_OS_BOOT=y
# 在 SPL 中指定内核和设备树的加载地址
#define CONFIG_SYS_SPL_ARGS_ADDR 0x42000000
#define CONFIG_SYS_MMCSD_RAW_MODE_KERNEL_SECTOR 0x1000
#define CONFIG_SYS_MMCSD_RAW_MODE_ARGS_SECTOR 0x2000
Falcon Mode 的缺点是灵活性差。你不能在启动时修改内核参数。所以它适合那些配置固定的产品。
4.3.4 优化 Flash 读取速度
从 Flash 读取数据是另一个瓶颈。如果你的 Flash 支持 DMA,一定要开启:
# 启用 DMA 支持
CONFIG_DMA=y
CONFIG_SPL_DMA=y
# 设置 DMA 传输大小(根据 Flash 性能调整)
#define CONFIG_SYS_MMC_MAX_BLK_COUNT 256
我曾在 NAND Flash 上遇到过读取速度只有 2MB/s 的情况。开启 DMA 后,速度提升到 15MB/s。启动时间直接减半。
小技巧:使用 bootstage 命令可以查看 U-Boot 各阶段的耗时。运行 bootstage report 就能看到每个阶段的精确时间。这是定位启动瓶颈的利器。
4.4 FIT image 的使用
FIT image(Flattened Image Tree)是 U-Boot 推荐的内核打包格式。它把内核、设备树、ramdisk 打包成一个文件。好处是:
- 统一管理:一个文件包含所有启动所需的数据
- 支持签名:可以验证内核的完整性
- 支持多配置:一个 FIT 文件可以包含多个内核版本
4.4.1 创建 FIT image
首先,创建一个 .its 文件(Image Tree Source):
// kernel.its
/dts-v1/;
/ {
description = "Linux kernel with device tree";
#address-cells = <1>;
images {
kernel {
description = "Linux kernel";
data = /incbin/("zImage");
type = "kernel";
arch = "arm";
os = "linux";
compression = "none";
load = <0x80008000>;
entry = <0x80008000>;
hash {
algo = "sha256";
};
};
fdt {
description = "Device tree";
data = /incbin/("board.dtb");
type = "flat_dt";
arch = "arm";
compression = "none";
hash {
algo = "sha256";
};
};
};
configurations {
default = "config1";
config1 {
description = "Boot with kernel and device tree";
kernel = "kernel";
fdt = "fdt";
};
};
};
然后使用 mkimage 工具生成 FIT 文件:
$ mkimage -f kernel.its kernel.itb
4.4.2 使用 FIT image 启动
在 U-Boot 命令行中,加载并启动 FIT 文件:
=> fatload mmc 0:1 0x42000000 kernel.itb
=> bootm 0x42000000
U-Boot 会自动解析 FIT 文件,提取内核和设备树,然后启动。
4.4.3 签名与验证
安全启动场景下,FIT image 支持签名验证。先生成密钥对:
$ openssl genrsa -F4 -out key.pem 2048
$ openssl req -batch -new -x509 -key key.pem -out cert.pem
然后在 .its 文件中添加签名信息:
// 在 images 节点中添加
kernel {
// ... 其他属性
signature {
algo = "sha256,rsa2048";
key-name-hint = "dev-key";
};
};
生成 FIT 文件时,指定私钥:
$ mkimage -f kernel.its -k ./keys -r kernel.itb
在 U-Boot 中启用验证:
CONFIG_FIT_SIGNATURE=y
CONFIG_RSA=y
这样,U-Boot 在加载 FIT 文件时会自动验证签名。签名不匹配,直接拒绝启动。
实战建议:FIT image 的签名验证是防止固件被篡改的有效手段。我在一个物联网网关项目中使用过。当时客户要求 OTA 更新必须安全。我们用了 FIT 签名 + 硬件安全单元(HSM)存储私钥。上线两年,没出过一次安全问题。
4.5 本章小结
Bootloader 优化,说白了就是「减负」和「加速」。减负是裁剪不需要的功能,加速是优化每个阶段的耗时。
我个人的经验是:先分析启动流程,找到瓶颈。然后针对性地裁剪和优化。最后用 FIT image 统一管理内核和设备树。这套组合拳打下来,启动时间减少 50% 以上不是问题。
嗯,最后提醒一句。优化 Bootloader 时,一定要保留调试接口。否则出了问题,你连打印信息都看不到,那才叫抓瞎。