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 还是会把所有设备都扫一遍。

避坑指南:我曾经遇到过一块芯片,ROM Code 在检测 DDR 时用了默认的保守时序,导致初始化时间翻倍。如果你的芯片支持 fuse 配置,记得把 DDR 时序参数固化进去,能省下不少时间。

2.2 第二站:SPL——轻量级的“开瓶器”

SPL 是 ROM Code 加载进来的第一个用户程序。它很小,通常只有几十 KB。为什么需要它?因为 U-Boot 太大了,ROM Code 那点 SRAM 装不下。

SPL 的主要工作:

  • 初始化 DDR(如果 ROM Code 没做的话)
  • 初始化存储设备(eMMC、NAND 等)
  • 从存储设备加载 U-Boot 到 DDR
  • 跳转到 U-Boot 执行

说白了,SPL 就是个“搬运工”。但你别小看它,很多启动时间优化都从这里下手。

个人经验:我习惯在 SPL 里只做最必要的初始化。比如,如果你的 U-Boot 放在 eMMC 的固定分区里,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 图把整个启动流程串起来了。我建议你把它存下来,后面优化的时候对照着看。

Bootloader 启动流程与时间分布 Bootloader 启动流程与时间分布 ROM Code 50-150 ms (10%-20%) SPL 100-300 ms (20%-30%) U-Boot 300-800 ms (50%-70%) Linux Kernel 各阶段关键操作 • ROM Code:初始化时钟、DDR、存储接口;扫描启动设备 • SPL:初始化 DDR(如未完成)、加载 U-Boot 到 DDR • U-Boot:板级初始化、设备树解析、环境变量加载、启动内核 • 优化重点:U-Boot 阶段占时最长,可裁剪驱动、缩短延时、并行初始化

2.6 各阶段的关键操作与优化切入点

我们一个一个来看。

ROM Code 阶段

  • 时钟初始化:芯片上电后默认跑在低速时钟上,ROM Code 会把它切换到高速模式。这个时间基本固定。
  • DDR 初始化:这是大头。DDR 控制器需要训练时序,尤其是 DDR3/DDR4,训练过程可能持续几十毫秒。
  • 启动设备扫描:ROM Code 会按优先级列表依次尝试每个启动设备。如果你的板子只用一个设备,但 ROM Code 还是会扫一遍其他的——这就是浪费。
优化思路:ROM Code 虽然改不了,但你可以通过芯片的 fuse 或 eFuse 配置来跳过不必要的步骤。比如,有些芯片允许你指定启动设备,跳过扫描过程。

SPL 阶段

  • DDR 初始化:如果 ROM Code 没做,SPL 就得自己做。这里要注意,DDR 训练参数可以提前固化,避免每次都重新训练。
  • 存储设备初始化:eMMC 的初始化时间通常在 50-100ms 左右,NAND 可能更长。
  • U-Boot 加载:从存储设备读取 U-Boot 镜像到 DDR。这里的关键是读取速度——用 DMA 还是 PIO?块大小是多少?
个人经验:我习惯在 SPL 里用 DMA 方式读取 U-Boot,并且把块大小调到存储设备的最佳性能点。比如 eMMC 通常 512KB 的块大小效率最高。另外,如果 U-Boot 镜像压缩过,SPL 还得解压——这个时间也要算进去。

U-Boot 阶段

  • 板级初始化:各种外设的初始化,比如 GPIO、I2C、SPI、UART 等。很多驱动默认是全部初始化的,但你的板子可能只用其中几个。
  • 设备树解析:U-Boot 会解析设备树(DTB),这个时间跟设备树的大小成正比。
  • 环境变量加载:从存储设备读取环境变量。如果环境变量很大(比如存了很多网络配置),加载时间也会增加。
  • 启动延时:U-Boot 默认会等待几秒钟,让你按任意键进入命令行。这个延时在生产环境中完全可以去掉。
避坑指南:我曾经遇到过一个问题,U-Boot 在初始化网络协议栈时卡了 200ms,原因是 PHY 芯片的自动协商超时了。后来我把 PHY 的自动协商模式改成了强制模式,问题就解决了。所以,不要盲目相信默认配置,每个外设的初始化时间都要实测。

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 是重头戏,占时最长,也是我们后续优化的主战场。