1. U-Boot启动流程全景:从ROM Code到Main U-Boot的完整链路

做嵌入式开发的朋友,十有八九都跟U-Boot打过交道。但说实话,很多人对它的启动过程是一知半解的——只知道上电后U-Boot会跑起来,至于中间经历了什么,为什么有时候快有时候慢,往往说不清楚。

我个人习惯,在优化任何系统之前,先把它的完整生命周期画出来。今天我们就从芯片上电那一刻开始,把U-Boot的启动链路从头到尾捋一遍。

核心观点:U-Boot启动不是单一阶段,而是由ROM Code、SPL、TPL(可选)、Main U-Boot四个阶段组成的接力赛。每个阶段都有明确的职责和优化空间。

1.1 启动的第一棒:ROM Code

芯片上电后,CPU会从一个固定的地址取指令。这个地址通常固化在芯片内部ROM里,所以叫ROM Code。它是芯片厂商写死的,你改不了。

ROM Code干的事情其实很简单:

  • 初始化最基本的硬件(时钟、SRAM、看门狗)
  • 从启动介质(SD卡、eMMC、NAND Flash、SPI NOR等)读取下一级代码
  • 验证签名(如果使能了安全启动)
  • 跳转到下一级代码的入口

这里有个关键点:ROM Code能用的内存非常有限。大多数芯片内部SRAM只有几十KB到几百KB。所以它只能加载一个很小的程序——这就是SPL(Secondary Program Loader)诞生的原因。

经验之谈:我在调试某款i.MX平台时,发现ROM Code读取启动介质的速度其实可以调整。默认的读速往往偏保守,适当提高时钟频率能省下几十毫秒。不过要注意,这个改动有风险,搞不好就起不来了。

1.2 第二棒:SPL(Secondary Program Loader)

SPL是U-Boot源码编译出来的一个小镜像,大小通常被限制在几十KB以内。它的任务很明确:初始化DDR内存,然后把Main U-Boot从存储介质加载到DDR里。

SPL的执行流程大致如下:

  1. 设置CPU为SVC模式,关中断
  2. 初始化串口(方便打印调试信息)
  3. 初始化时钟和DDR控制器
  4. 初始化存储介质驱动(MMC、NAND、SPI等)
  5. 从存储介质读取Main U-Boot到DDR
  6. 跳转到Main U-Boot入口

你想想看,SPL其实就是一个微缩版的U-Boot。它也有设备树,也有驱动模型,只不过功能被裁剪到极致。

注意:SPL阶段的DDR初始化是启动速度的关键瓶颈。DDR训练(Training)如果做得不好,要么启动失败,要么花掉几百毫秒。我曾经遇到过一块板子,DDR训练花了400多ms,优化后降到80ms——差距就在这里。

1.3 可选阶段:TPL(Tertiary Program Loader)

有些芯片的SRAM实在太小,连SPL都塞不下。这时候就需要引入TPL。TPL比SPL更小,只做一件事:初始化外部RAM(通常是DDR),然后加载SPL到DDR里运行。

TPL不是必须的。我个人的经验是,除非SRAM小于64KB,否则没必要引入TPL。多一个阶段就多一次加载和跳转,时间上不划算。

阶段 典型大小 运行位置 主要职责
ROM Code 固化在芯片内 内部ROM 加载SPL/TPL,安全验证
TPL 8~16 KB 内部SRAM 初始化DDR,加载SPL
SPL 32~128 KB 内部SRAM或DDR 初始化DDR/存储,加载Main U-Boot
Main U-Boot 200 KB~2 MB DDR 完整引导流程,加载内核

1.4 最后一棒:Main U-Boot

Main U-Boot就是我们通常说的U-Boot。它被加载到DDR后,会做一系列初始化工作:

  • 完整的板级初始化(board_init_f / board_init_r)
  • 设备树解析(FDT)
  • 驱动模型初始化(DM)
  • 环境变量加载(env)
  • 网络协议栈初始化(如果使能)
  • 命令行界面(CLI)准备
  • 自动启动脚本执行(bootcmd)

Main U-Boot的启动流程可以分成两个大阶段:

第一阶段(board_init_f):此时U-Boot还在只读存储区(比如SPL把它加载到的DDR地址),不能随意写数据。这个阶段主要做:初始化串口、时钟、DRAM控制器、堆栈等。

第二阶段(board_init_r):U-Boot把自己重定位到DDR的末尾地址,然后开始完整的初始化。这时候驱动模型、设备树、环境变量、网络协议栈等才会真正跑起来。

为什么分两个阶段?说白了,就是U-Boot一开始不知道自己占用的内存会不会被覆盖。先在一个安全的地方把基本硬件初始化好,再把自己挪到安全位置,然后放开手脚干。

1.5 完整链路图

下面这张图把整个启动链路画出来了。我建议你保存下来,后面每一章的优化点都可以在这张图上找到对应位置。

U-Boot 启动完整链路图 ROM Code 芯片固化,不可修改 TPL(可选) 初始化DDR 仅SRAM极小时使用 SPL 初始化DDR/存储 加载Main U-Boot Main U-Boot ROM Code 内部流程 ① 初始化CPU/SRAM ② 读取启动介质 ③ 安全验证(可选) ④ 跳转到SPL/TPL SPL 内部流程 ① 初始化DDR控制器 ② DDR Training ③ 初始化存储驱动 ④ 加载Main U-Boot Main内部 ① board_init_f ② 重定位 ③ board_init_r ④ 加载内核 上电 ~10ms ~50ms ~200ms ~500ms 各阶段耗时分布(典型值) • ROM Code:5~20ms(芯片固定,基本无法优化) • TPL(如有):10~30ms(DDR初始化+加载SPL) • SPL:30~150ms(DDR Training是最大头) • Main U-Boot:100~500ms(驱动初始化+环境加载+脚本执行) 优化重点:SPL的DDR Training + Main U-Boot的驱动初始化

1.6 各阶段耗时分布

上面这张图里我标了典型耗时。实际项目中,这些数字可能差很多。比如我调过的一块全志H3板子,SPL阶段花了200多ms,其中DDR Training就占了150ms。后来换了更快的Training算法,直接砍到40ms。

Main U-Boot的耗时浮动更大。如果使能了网络协议栈、USB、文件系统,光驱动初始化就能吃掉300ms。这也是我们后面要重点优化的地方。

一个小技巧:在SPL和Main U-Boot的入口处加上时间戳打印,用GPIO或者串口输出。这样你能精确知道每个阶段花了多少时间。我习惯在board_init_f、board_init_r、以及加载内核之前各打一个时间戳。

1.7 避坑指南

做启动优化这么多年,有些坑我踩过不止一次。这里列几个最常见的:

  • DDR Training参数照搬参考设计:不同PCB布局、不同批次的内存颗粒,Training参数可能有差异。我曾经因为照搬了开发板的配置,导致量产时部分板子启动失败。
  • SPL镜像超出SRAM大小:加功能时没注意SPL的大小限制,结果编译出来的镜像放不进SRAM。嗯,这种问题通常只能在链接阶段发现。
  • 忽略ROM Code的启动介质读取速度:有些芯片的ROM Code支持配置读取速度,默认值往往很保守。如果你用的是高速SD卡或eMMC,适当提高读取速度能省下不少时间。
  • Main U-Boot重定位后的地址冲突:如果设备树或者内核的加载地址跟U-Boot重定位后的地址重叠,就会出现诡异的问题——有时候能启动,有时候不行。
重要提醒:优化启动速度时,一定要先测量,再优化。不要凭感觉去改代码。用示波器抓GPIO电平变化,或者用串口打印时间戳,拿到真实数据再动手。我见过太多人花了一周优化了一个根本不存在的瓶颈。

好了,这一章我们把U-Boot的完整启动链路梳理清楚了。从ROM Code到Main U-Boot,每个阶段做什么、花多少时间、哪里是优化重点,心里应该有个底了。