2. Bootloader 启动流程分析:ROM Code、SPL、U-Boot 的启动阶段与时间分布
做嵌入式启动优化,第一件事就是搞清楚你的芯片上电后到底干了什么。很多人一上来就盯着 U-Boot 的代码改,结果发现优化了半天,启动时间只少了十几毫秒——其实大头都在前面呢。
今天我们就来拆解一下,从按下复位键到 U-Boot 交棒给内核,中间到底经历了哪些阶段。我个人习惯把整个流程分成三段:ROM Code、SPL(Secondary Program Loader)和 U-Boot 本身。
2.1 第一站:ROM Code——芯片出厂就写死的“铁饭碗”
ROM Code 是芯片内部固化的程序,你改不了它。它负责最底层的硬件初始化,比如设置时钟、配置 DDR 控制器、初始化存储接口(NAND、eMMC、SD 卡等)。
它的核心任务只有一个:找到并加载下一级启动程序。
我记得有一次调试一块 i.MX6 的板子,启动时间总是比预期多 200ms。查了半天,发现 ROM Code 在枚举启动设备时花了大量时间——它先尝试了 SPI NOR,没找到;再试 NAND,还是没找到;最后才轮到 SD 卡。嗯,这里有个坑:ROM Code 的启动设备优先级是固定的,如果你的板子只用 SD 卡启动,但 ROM Code 还是会把所有设备都扫一遍。
2.2 第二站:SPL——轻量级的“开瓶器”
SPL 是 ROM Code 加载进来的第一个用户程序。它很小,通常只有几十 KB。为什么需要它?因为 U-Boot 太大了,ROM Code 那点 SRAM 装不下。
SPL 的主要工作:
- 初始化 DDR(如果 ROM Code 没做的话)
- 初始化存储设备(eMMC、NAND 等)
- 从存储设备加载 U-Boot 到 DDR
- 跳转到 U-Boot 执行
说白了,SPL 就是个“搬运工”。但你别小看它,很多启动时间优化都从这里下手。
2.3 第三站:U-Boot——真正的“大管家”
U-Boot 被 SPL 加载到 DDR 后,开始执行完整的初始化流程。它比 SPL 复杂得多,包括:
- 完整的板级初始化(时钟、GPIO、外设等)
- 设备树解析
- 环境变量加载
- 网络协议栈初始化(如果需要)
- 启动内核
U-Boot 的启动时间通常占整个 Bootloader 流程的 60%-70%。但好消息是,这里也是我们最能发挥优化能力的地方。
2.4 时间分布:到底慢在哪里?
下面这张表是我基于一个典型 ARM Cortex-A7 平台(主频 1GHz,eMMC 启动)测出来的数据。你感受一下:
| 启动阶段 | 典型耗时 | 占比 | 优化空间 |
|---|---|---|---|
| ROM Code | 50-150 ms | 10%-20% | 小(芯片固定) |
| SPL 加载与执行 | 100-300 ms | 20%-30% | 中(可裁剪) |
| U-Boot 初始化 | 300-800 ms | 50%-70% | 大(可深度优化) |
为什么会这样?说白了,ROM Code 是硬编码的,你动不了它。SPL 虽然能改,但受限于 SRAM 大小,能做的事情有限。U-Boot 就不一样了——它跑在 DDR 里,空间大、功能多,但也意味着“臃肿”。
2.5 核心流程:一张图看懂
下面这张 SVG 图把整个启动流程串起来了。我建议你把它存下来,后面优化的时候对照着看。
2.6 各阶段的关键操作与优化切入点
我们一个一个来看。
ROM Code 阶段
- 时钟初始化:芯片上电后默认跑在低速时钟上,ROM Code 会把它切换到高速模式。这个时间基本固定。
- DDR 初始化:这是大头。DDR 控制器需要训练时序,尤其是 DDR3/DDR4,训练过程可能持续几十毫秒。
- 启动设备扫描:ROM Code 会按优先级列表依次尝试每个启动设备。如果你的板子只用一个设备,但 ROM Code 还是会扫一遍其他的——这就是浪费。
SPL 阶段
- DDR 初始化:如果 ROM Code 没做,SPL 就得自己做。这里要注意,DDR 训练参数可以提前固化,避免每次都重新训练。
- 存储设备初始化:eMMC 的初始化时间通常在 50-100ms 左右,NAND 可能更长。
- U-Boot 加载:从存储设备读取 U-Boot 镜像到 DDR。这里的关键是读取速度——用 DMA 还是 PIO?块大小是多少?
U-Boot 阶段
- 板级初始化:各种外设的初始化,比如 GPIO、I2C、SPI、UART 等。很多驱动默认是全部初始化的,但你的板子可能只用其中几个。
- 设备树解析:U-Boot 会解析设备树(DTB),这个时间跟设备树的大小成正比。
- 环境变量加载:从存储设备读取环境变量。如果环境变量很大(比如存了很多网络配置),加载时间也会增加。
- 启动延时:U-Boot 默认会等待几秒钟,让你按任意键进入命令行。这个延时在生产环境中完全可以去掉。
2.7 时间测量:没有数据就没有优化
做优化之前,先得知道时间花在哪里。我常用的方法有两种:
- GPIO 脉冲法:在关键节点翻转 GPIO,用示波器或逻辑分析仪测量脉冲宽度。简单粗暴,但很有效。
- 串口打印法:在代码里加串口打印,记录时间戳。注意串口本身有延迟,但用来做相对比较还是可以的。
下面是一个简单的 GPIO 脉冲示例:
// 在 SPL 入口处拉高 GPIO
gpio_set_value(DEBUG_GPIO, 1);
// ... 执行初始化 ...
// 在跳转到 U-Boot 前拉低 GPIO
gpio_set_value(DEBUG_GPIO, 0);
用示波器量一下高电平的宽度,就是 SPL 的执行时间。这个方法我在项目里用了无数次,简单可靠。
2.8 小结
好了,这一章我们把 Bootloader 的启动流程拆成了三段:ROM Code、SPL 和 U-Boot。每段都有它的特点和优化空间。ROM Code 你动不了,但可以绕;SPL 是轻量级的搬运工,优化空间中等;U-Boot 是重头戏,占时最长,也是我们后续优化的主战场。