4. SPL与TPL机制
我记得刚入行那会儿,第一次接触U-Boot的SPL机制。当时我天真地以为,BootROM直接加载完整的U-Boot就完事了。结果呢?板子死活起不来,串口输出一片空白。后来才发现,很多SoC的片上SRAM就那么几十KB,根本塞不下完整的U-Boot。嗯,这就是SPL存在的根本原因。
4.1 SPL的作用:BootROM的“快递员”
SPL,全称Secondary Program Loader。说白了,它就是BootROM和完整U-Boot之间的“二传手”。
为什么需要这个二传手?你想想看,BootROM是固化在芯片里的,它只能做最简单的事情——从固定的存储介质(比如NAND、SD卡、SPI Flash)读取固定大小的数据到SRAM里。但SRAM通常很小,比如我遇到过的一个Cortex-A7平台,SRAM只有64KB。完整U-Boot动辄几百KB,根本放不下。
SPL的核心职责就三条:
- 初始化硬件的最小集:时钟、DDR控制器、串口。够用就行,别多干。
- 从存储介质加载完整U-Boot:支持NAND、eMMC、SD卡、SPI Flash等。
- 跳转到完整U-Boot:把控制权交出去,自己功成身退。
关键点:SPL的大小受限于SRAM。通常被限制在16KB到64KB之间。所以SPL的代码必须精简,不能像完整U-Boot那样啥功能都往里塞。
我在项目中遇到过一个问题:SPL编译出来刚好超过SRAM大小1KB,结果BootROM加载到SRAM末尾时直接覆盖了关键数据,板子死得透透的。后来我养成了一个习惯——每次改完SPL代码,第一件事就是看编译出来的二进制大小。
4.2 TPL的引入背景:当SPL也“塞不下”了
随着SoC越来越复杂,问题来了:有些芯片的BootROM只加载前4KB或8KB到SRAM。这点空间,连SPL都塞不下了。怎么办?
这时候TPL(Tertiary Program Loader)就登场了。它是“三级加载器”,夹在BootROM和SPL之间。
TPL的引入背景其实很现实:
- SRAM太小:比如某些低功耗MCU,SRAM只有4KB。SPL本身就要几十KB,根本不可能。
- BootROM能力有限:它只能加载固定大小的镜像,比如4KB。这4KB里要包含初始化DDR的代码,然后才能把SPL加载到DDR里。
- 安全启动需求:TPL可以做简单的签名验证,确保SPL是可信的。
我的经验:TPL不是必须的。如果你的SoC的SRAM足够大(比如256KB以上),完全可以直接用SPL加载完整U-Boot。我做过一个项目,SRAM有512KB,我直接把完整U-Boot的早期初始化部分塞进去了,省掉了TPL的麻烦。
4.3 SPL/TPL的启动流程:一张图说清楚
下面这张图是我自己画的,展示了从芯片上电到完整U-Boot运行的完整流程。你仔细看,就能理解为什么需要SPL和TPL了。
这张图里有两个关键路径:
- 路径A(无TPL):BootROM → SPL → 完整U-Boot。这是最常见的情况。BootROM从存储介质加载SPL到SRAM,SPL初始化DDR,然后把完整U-Boot加载到DDR里,最后跳转过去。
- 路径B(有TPL):BootROM → TPL → SPL → 完整U-Boot。当SRAM小到连SPL都放不下时,TPL先上场。它只做一件事——初始化DDR。然后从DDR里加载SPL,SPL再加载完整U-Boot。
避坑指南:我曾经在一个项目里踩过坑——TPL和SPL使用了不同的DDR初始化参数。结果SPL加载到DDR后,数据全是错的。后来我强制要求:TPL和SPL必须共用同一份DDR初始化代码,通过宏定义控制编译。
4.4 SPL/TPL的代码结构
在U-Boot源码里,SPL和TPL的代码是分开的。我一般这样组织:
// SPL入口(通常位于arch/arm/cpu/armv7/start.S)
_start:
// 设置栈指针
ldr sp, =CONFIG_SPL_STACK
// 跳转到C语言入口
bl board_init_f
// board_init_f中初始化DDR
void board_init_f(ulong boot_flags)
{
// 初始化时钟
init_clocks();
// 初始化DDR控制器
init_dram();
// 从存储介质加载完整U-Boot
spl_load_image();
// 跳转到U-Boot入口
jump_to_image_no_args(&spl_image);
}
TPL的代码结构类似,但更精简:
// TPL入口
void tpl_board_init(void)
{
// 只初始化DDR
init_dram();
// 从存储介质加载SPL到DDR
tpl_load_spl();
// 跳转到SPL
jump_to_spl();
}
4.5 配置SPL/TPL的关键宏
在U-Boot的配置文件中,有几个宏我每次都会仔细检查:
| 宏定义 | 作用 | 我的建议 |
|---|---|---|
CONFIG_SPL |
启用SPL支持 | 必须开启,除非你的SRAM足够大 |
CONFIG_TPL |
启用TPL支持 | 仅在SRAM < 64KB时开启 |
CONFIG_SPL_TEXT_BASE |
SPL在SRAM中的加载地址 | 必须与BootROM的加载地址一致 |
CONFIG_SPL_MAX_SIZE |
SPL的最大大小限制 | 设为SRAM大小减去栈空间 |
CONFIG_SPL_STACK |
SPL使用的栈地址 | 放在SRAM的高地址,避免与代码冲突 |
个人习惯:我每次移植新板子时,都会先确认SoC的SRAM大小和BootROM的加载行为。然后设置CONFIG_SPL_MAX_SIZE为SRAM大小的80%,留出20%给栈和临时数据。这个比例是我踩过几次坑后总结出来的。
4.6 调试SPL/TPL的实用技巧
调试SPL和TPL是最头疼的,因为串口可能还没初始化。我分享几个我常用的方法:
- GPIO点灯法:在SPL的不同阶段点亮不同的GPIO,通过LED状态判断执行到哪里了。这招虽然土,但非常有效。
- 早期串口输出:在board_init_f的最开始就初始化串口,然后加打印。注意串口时钟可能还没配好,需要手动配置。
- 使用JTAG/SWD:如果板子支持,直接用调试器单步执行。我遇到过SPL在DDR初始化时挂掉的情况,JTAG一看就知道是时序参数配错了。
我曾经在一个项目里,SPL加载U-Boot后跳转失败。折腾了两天,最后发现是U-Boot镜像的校验和不对。从那以后,我每次都会在SPL里加一段CRC校验代码,确保加载的数据是完整的。
好了,关于SPL和TPL的核心机制,我就讲到这里。记住一句话:SPL是BootROM的延伸,TPL是SPL的延伸。它们的存在,都是为了解决“空间不够”这个现实问题。下一节我们会深入U-Boot的镜像格式和加载地址映射,到时候你会更清楚这些加载器之间的配合关系。
总结:SPL和TPL是U-Boot启动流程中的关键环节。理解它们的作用和启动流程,是进行U-Boot移植和优化的基础。我个人建议,在开始任何U-Boot相关开发前,先花半小时把SPL/TPL的配置和代码结构理清楚,这能帮你省下后面大量的调试时间。