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了。

SPL/TPL启动流程图 BootROM 芯片上电,执行固化代码 加载TPL TPL(可选) 初始化DDR,加载SPL 加载SPL SPL 初始化外设,加载U-Boot 跳转 完整U-Boot 引导内核 分支情况: 路径A:无TPL BootROM → SPL → U-Boot 路径B:有TPL BootROM → TPL → SPL → U-Boot 存储介质(NAND / eMMC / SD卡 / SPI Flash) 镜像布局:TPL镜像(可选) | SPL镜像 | U-Boot镜像 | 内核/设备树 注:TPL仅在SRAM过小(通常<64KB)时使用。现代SoC大多支持直接SPL启动。

这张图里有两个关键路径:

  • 路径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的配置和代码结构理清楚,这能帮你省下后面大量的调试时间。