第三阶段:第一阶段启动(SPL)—— 从零到一的“迷你Bootloader”
好,我们正式开始进入U-Boot启动的第一阶段——SPL。很多刚接触嵌入式Linux的朋友,一上来就被“SPL”、“TPL”、“ATF”这些缩写搞得晕头转向。其实说白了,SPL就是一个“迷你版的U-Boot”。
为什么需要它?因为现在的SoC(系统级芯片)越来越复杂,片内SRAM通常只有几十KB到几百KB,根本装不下完整的U-Boot。SoC上电后,硬件自动从启动介质(比如SD卡、eMMC、NAND Flash)的头几个扇区加载一段代码到SRAM里执行——这段代码就是SPL。它的任务很纯粹:初始化最基本的硬件(时钟、DDR、存储控制器),然后把完整的U-Boot从启动介质加载到DDR中,跳转过去。
我刚开始做第一个i.MX6项目时,就吃过SPL的亏。板子死活起不来,串口一点输出都没有。后来才发现,是DDR初始化参数配错了,SPL卡死在DRAM校准那一步。嗯,从那以后,我养成了一个习惯:拿到一个新板子,第一件事就是先确保SPL能稳定跑通,再谈后面的系统启动。
3.1 SPL的作用:承上启下的“搬运工”
SPL(Secondary Program Loader)在整个启动链中扮演着“桥梁”的角色。它的核心职责可以归纳为三点:
- 硬件最小系统初始化:初始化CPU时钟、关闭看门狗、设置堆栈指针、初始化串口(用于输出日志)、初始化DDR控制器。
- 加载完整U-Boot:从启动介质(SD/eMMC/NAND/SPI Flash)中读取U-Boot的镜像(通常是u-boot.img或u-boot-dtb.img),放到DDR的指定地址。
- 跳转执行:校验镜像完整性(可选),然后跳转到U-Boot的入口地址,把控制权交出去。
关键认知:SPL本身也是一个独立的、可配置的二进制程序。它有自己的链接脚本、配置文件(比如include/configs/xxx.h中的CONFIG_SPL_*选项),甚至有自己的设备树(如果启用了SPL OF控制)。
3.2 SPL初始化流程:从Reset向量到跳转
SPL的执行流程非常清晰,我习惯把它分成四个阶段。下面这张图可以帮你建立整体认知:
下面我们逐阶段拆解,看看每个环节到底做了什么,以及日志里应该关注什么。
3.3 SPL日志关键点:从“无输出”到“跳转成功”
SPL的日志输出量通常不大,但每一行都很有价值。我一般会盯着以下几个关键节点看:
3.3.1 早期串口初始化(阶段1-2)
这是最让人头疼的阶段——如果串口没输出,你根本不知道SPL有没有跑起来。SPL在初始化串口之前,会先做CPU模式设置和堆栈初始化。如果这一步卡住,那就是硬件级问题。
正常启动时,你会看到类似这样的输出:
U-Boot SPL 2023.04 (Jan 15 2024 - 10:30:00 +0800)
Trying to boot from MMC1
注意看第一行:它包含了SPL的版本号和编译时间。如果连这行都没有,说明SPL根本没执行到串口初始化。我遇到过几次这种情况,最后发现是启动介质的前几个扇区被擦除了,或者SoC的启动引脚配置错了。
⚠️ 常见陷阱:有些SoC(比如全志系列)的SPL默认不开启串口输出,需要在配置中显式打开CONFIG_SPL_SERIAL_SUPPORT和CONFIG_SPL_UART。如果你发现板子能启动但没日志,先检查这个。
3.3.2 DDR初始化(阶段3)
DDR初始化是SPL中最关键也最容易出问题的一步。日志中通常会有类似这样的输出:
DRAM: 512 MiB
如果DDR初始化成功,SPL会打印出检测到的内存大小。如果失败,你可能会看到:
DRAM: 0 MiB
### ERROR ### Please RESET the board ###
或者干脆卡死,没有任何后续输出。为什么会这样?DDR初始化涉及时序参数(tRCD、tCL、tRP等)、电压、ODT配置,任何一个参数不对,DDR控制器都无法完成校准。
💡 我的调试习惯:当DDR初始化失败时,我会先检查DDR电源是否稳定(用示波器看波形),然后核对设备树或板级文件中的DDR时序参数是否与DDR颗粒的数据手册一致。我曾经在一个项目中,因为DDR的VREF电压纹波过大,导致SPL在DDR校准阶段随机失败——换了更干净的电源才解决。
3.3.3 加载U-Boot镜像(阶段4)
DDR初始化成功后,SPL会尝试从启动介质加载完整的U-Boot。日志输出类似:
Loading U-Boot from MMC, offset 0x8000, size 0x100000
U-Boot 2023.04 (Jan 15 2024 - 10:30:00 +0800)
这里有两个关键信息:
- 加载源和偏移量:比如
offset 0x8000,表示U-Boot镜像在SD卡的第0x8000字节处。如果这个偏移量错了,SPL会加载到错误的数据,跳转后必然死机。 - 跳转后的U-Boot版本号:如果能看到第二行,说明SPL已经成功跳转,控制权交给了完整的U-Boot。
如果SPL打印了“Loading U-Boot...”但之后没有任何输出,通常有两种可能:
- 镜像损坏或偏移量错误,SPL加载了无效数据。
- DDR地址映射问题,U-Boot被加载到了错误的地址。
3.4 实战排查:一张表搞定SPL日志分析
为了方便你在现场快速定位问题,我整理了一张SPL日志排查表。你可以把它打印出来贴在工位上:
| 日志现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全无输出 | SPL未执行、串口未初始化、启动介质错误 | 检查启动引脚、供电、时钟;确认SPL是否烧录正确 |
| 有版本号,卡在“DRAM:”之后 | DDR初始化失败(时序/电压/硬件连接) | 核对DDR参数、用示波器测DDR电源和时钟 |
| 打印“Loading U-Boot...”后无响应 | 镜像偏移量错误、镜像损坏、DDR地址映射错误 | 检查CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR等配置 |
| SPL反复重启 | 看门狗未关闭、DDR校准失败导致异常复位 | 检查看门狗配置、增加DDR初始化延时 |
| 打印“Trying to boot from...”但一直循环 | 所有启动介质都未找到有效镜像 | 检查启动介质顺序、镜像是否烧录到正确位置 |
核心原则:SPL的日志是“有就是有,没有就是没有”。如果连SPL版本号都看不到,不要浪费时间分析后面的U-Boot或内核问题——先把SPL跑通再说。
3.5 一个真实案例:SPL加载U-Boot后“假死”
最后分享一个我印象很深的案例。有一次调试一块基于Rockchip RK3399的板子,SPL日志一切正常:
U-Boot SPL 2021.07 (Sep 20 2023 - 14:22:00)
Trying to boot from MMC1
DRAM: 4 GiB
Loading U-Boot from MMC, offset 0x4000, size 0x200000
然后就没有然后了。串口完全静默,板子也没重启。我一开始以为是U-Boot镜像坏了,重新烧录了好几次,问题依旧。
后来我仔细看了SPL的源码,发现RK3399的SPL在加载U-Boot后,会先跳转到DDR中的一段“TEE”(可信执行环境)代码,然后再由TEE跳转到U-Boot。而我的板子上根本没有烧录TEE镜像!
解决方案很简单:在SPL配置中关闭TEE支持(#undef CONFIG_SPL_OPTEE),让SPL直接跳转到U-Boot。改完之后,板子一次就起来了。
这个案例告诉我们:SPL的“跳转”并不总是直接跳到U-Boot,中间可能还有ATF、TEE等组件。排查时一定要搞清楚目标平台的启动链结构。