4. U-Boot启动流程回顾:SPL、TPL、ATF与主U-Boot
做嵌入式启动优化,绕不开U-Boot。很多工程师觉得U-Boot就是个黑盒子,上电就跑,跑完就跳内核。但真正做异常复位快速恢复时,你会发现——不懂启动流程,根本不知道时间花在哪。
这一节,我带你把U-Boot的启动链条拆开看。从SPL到主U-Boot,再到ATF,每个阶段干什么、怎么衔接、哪里容易出问题,咱们一次说清楚。
4.1 为什么需要多阶段启动?
早期U-Boot是单阶段启动。上电后,U-Boot直接初始化DDR、外设、文件系统,然后加载内核。听起来简单,但有个致命问题——DDR初始化代码必须在SRAM里跑。
SRAM容量有限,一般只有几十KB到几百KB。单阶段U-Boot镜像动辄几百KB,根本塞不进SRAM。怎么办?拆成两段:
- 第一阶段(SPL):在SRAM里运行,只做最基础的初始化,比如时钟、DDR、串口。然后加载第二阶段。
- 第二阶段(主U-Boot):在DDR里运行,做完整的板级初始化、设备驱动、环境变量、启动命令。
后来有些平台更复杂,比如TI的OMAP系列,SRAM小到连SPL都塞不下,于是又加了TPL(Tertiary Program Loader)。TPL比SPL更小,只做一件事:初始化足够大的SRAM或DDR,然后加载SPL。
一句话总结:SPL是"加载器",主U-Boot是"执行器"。TPL是"加载器的加载器"。
4.2 SPL:最小加载器
SPL(Secondary Program Loader)是U-Boot的精简版。它只保留最核心的功能:
- 初始化时钟、PLL、DDR
- 初始化串口(用于调试输出)
- 从存储介质(NAND、eMMC、SD卡、SPI Flash)读取主U-Boot
- 跳转到主U-Boot入口
SPL的代码量很小,一般控制在32KB以内。我见过有些项目为了塞进16KB的SRAM,连串口初始化都砍了——调试全靠LED闪烁,那叫一个痛苦。
个人经验:SPL阶段最容易翻车的是DDR初始化。DDR时序参数稍微不对,SPL能跑,但主U-Boot加载到DDR后直接挂。我曾经在一个项目里,SPL打印"OK"后跳转,主U-Boot死活不跑。查了两天,发现是DDR的ODT配置不对,主U-Boot代码段刚好被DDR的反射信号冲坏了。
4.3 TPL:极简加载器
TPL(Tertiary Program Loader)不是所有平台都有。它出现在SRAM极度紧张的场景下,比如:
- SRAM只有4KB-8KB
- DDR初始化代码本身就超过SRAM容量
- 需要先初始化外部SRAM或DDR才能放下SPL
TPL的代码量通常在2KB-4KB。它只做:
- 初始化最小硬件(时钟、部分SRAM)
- 从存储介质加载SPL到SRAM
- 跳转到SPL
TPL和SPL的关系,有点像BIOS的"微码加载"和"POST"阶段。TPL是"先让CPU能跑起来",SPL是"让系统能初始化DDR"。
注意:TPL不是U-Boot官方标配。它由芯片厂商提供,或者由BootROM直接加载。你在U-Boot源码里看到的spl/tpl目录,其实是U-Boot为了兼容这些平台做的适配层。
4.4 ATF:安全世界的入口
ATF(ARM Trusted Firmware)是ARMv8架构引入的安全固件层。它运行在EL3(异常级别3),是系统最高权限层。
在启动流程中,ATF通常位于SPL和主U-Boot之间:
- SPL初始化DDR后,加载ATF到DDR
- SPL跳转到ATF入口
- ATF初始化安全世界(Secure World)
- ATF跳转到主U-Boot(运行在非安全世界EL2)
ATF的主要职责:
- 初始化GIC(中断控制器)
- 设置PSCI(电源状态协调接口)
- 管理安全启动链
- 提供SMC(安全监控调用)服务
我刚开始接触ATF时,觉得它就是个"多余的中转站"。直到有一次做CPU热插拔,发现没有ATF的PSCI支持,主U-Boot根本没法控制CPU的电源状态。嗯,ATF不是多余,它是ARMv8架构的"看门人"。
4.5 主U-Boot:完整的Bootloader
主U-Boot是完整的U-Boot镜像。它运行在DDR里,拥有完整的驱动、命令、环境变量支持。
主U-Boot的启动流程大致如下:
| 阶段 | 主要工作 | 典型耗时 |
|---|---|---|
| board_init_f | 初始化CPU、定时器、串口、DDR控制器 | 10-50ms |
| relocate_code | 将U-Boot自身从Flash拷贝到DDR | 5-20ms |
| board_init_r | 初始化所有外设驱动、环境变量、网络、文件系统 | 50-200ms |
| main_loop | 进入命令行或自动启动内核 | 取决于配置 |
这里有个关键点:relocate_code。U-Boot在Flash里运行时,代码是位置无关的(PIC)。但为了性能,它会把自身拷贝到DDR的高地址,然后跳过去执行。这个拷贝过程,如果Flash读取速度慢(比如SPI NOR),会吃掉不少时间。
避坑指南:我曾经在一个项目里,主U-Boot启动花了800ms。查了半天,发现是board_init_r里初始化了所有GPIO——包括那些根本没用到的。每个GPIO的配置都要读写寄存器,几十个GPIO下来,几百微秒就没了。优化方案很简单:只初始化启动必需的GPIO,其他的留到内核里做。
4.6 完整启动链条
把上面几个阶段串起来,完整的启动链条是这样的:
- BootROM:芯片内部固件,初始化最小硬件,加载TPL或SPL
- TPL(可选):加载SPL到SRAM
- SPL:初始化DDR,加载ATF和主U-Boot
- ATF:初始化安全世界,跳转到主U-Boot
- 主U-Boot:完整初始化,加载内核
每个阶段都有明确的职责边界。你想想看,如果某个阶段出了问题,比如SPL加载ATF失败,系统会卡在哪个位置?答案是——串口可能没有任何输出,因为ATF还没初始化串口。
这就是为什么调试启动问题,要从BootROM的LED闪烁开始。我习惯在SPL里加一个GPIO翻转,用示波器量一下,就能知道SPL有没有跑起来、跑了多久。
4.7 各阶段的内存布局
不同阶段使用不同的内存区域。理解内存布局,对调试启动问题至关重要:
| 阶段 | 运行位置 | 典型大小 | 加载目标 |
|---|---|---|---|
| BootROM | 芯片内部ROM | 32KB-128KB | 加载TPL或SPL到SRAM |
| TPL | SRAM | 2KB-4KB | 加载SPL到SRAM |
| SPL | SRAM | 16KB-64KB | 加载ATF和主U-Boot到DDR |
| ATF | DDR(安全区域) | 64KB-256KB | 跳转到主U-Boot |
| 主U-Boot | DDR(非安全区域) | 256KB-2MB | 加载内核到DDR |
这里有个容易踩的坑:SPL和主U-Boot的链接地址不能重叠。SPL在SRAM里运行,主U-Boot在DDR里运行,看起来不冲突。但有些平台会把SPL的堆栈也放在DDR里——如果SPL还没初始化DDR,堆栈就指向DDR地址,那系统直接崩溃。
警告:检查SPL的链接脚本(u-boot-spl.lds),确认堆栈和堆的地址范围。如果SPL在初始化DDR之前就使用了DDR地址,必死无疑。
4.8 启动时间分布
做启动优化,先要知道时间花在哪。以我手头一个ARMv8平台为例,典型启动时间分布:
| 阶段 | 耗时 | 占比 | 优化空间 |
|---|---|---|---|
| BootROM | ~50ms | 10% | 无(芯片固件,不可改) |
| SPL | ~80ms | 16% | DDR初始化参数优化 |
| ATF | ~30ms | 6% | 减少安全服务初始化 |
| 主U-Boot | ~340ms | 68% | 裁剪驱动、优化设备树解析 |
看到了吗?主U-Boot占了近70%的时间。这也是为什么启动优化的重点,往往放在主U-Boot阶段。SPL和ATF虽然占比小,但它们是"瓶颈前置"——如果SPL加载慢,后面的所有阶段都得等。
我的习惯:在每个阶段入口和出口加GPIO翻转,用逻辑分析仪抓波形。这样能精确测量每个阶段的耗时,而不是靠串口打印估算。串口打印本身就有延迟,测出来的时间不准。
4.9 小结
U-Boot的多阶段启动,本质上是"用空间换时间"——把大镜像拆成小段,让每段都能在有限的内存里运行。SPL负责"开门",主U-Boot负责"干活",ATF负责"看门"。理解了这个链条,你才能知道:
- 异常复位后,系统从哪个阶段开始恢复?
- 哪些阶段可以跳过或加速?
- 哪些阶段是"硬依赖",不能省略?