4. SPL(Secondary Program Loader)深度解析

好,咱们进入正题。SPL 这个名字,全称是 Secondary Program Loader。说白了,它就是 U-Boot 的“迷你版”。

你可能会问:为什么不能直接跑 U-Boot?原因很简单——硬件资源有限。很多 SoC 内部的 SRAM 只有几十 KB,连个完整的 U-Boot 都塞不下。所以,我们需要一个“瘦身版”的加载器,先把 DDR 初始化好,再把真正的 U-Boot 从 Flash 里搬进去。

我个人习惯把 SPL 叫做“硬件唤醒师”。它的任务就三件:初始化最小硬件、加载 U-Boot、跳转执行。听起来简单,但坑不少。我当年第一次调 SPL 时,卡在 DDR 初始化上整整三天——后来发现是时序参数配错了。嗯,这种经历一次就够你记一辈子。

SPL 的作用与设计哲学

SPL 的设计哲学,我总结为四个字:够用就好。

  • 最小化代码体积:能省则省。不需要驱动框架,不需要文件系统,甚至连命令行都不要。
  • 最小化硬件初始化:只初始化必须的模块——时钟、DDR、串口(用于调试)、存储控制器。
  • 单一职责:SPL 只做一件事——加载 U-Boot。别想着在 SPL 里做复杂逻辑,那会把自己坑死。
核心原则:SPL 的代码量通常控制在 4KB ~ 32KB 以内。超过这个范围,你就该反思是不是塞了不该塞的东西。

我在项目中遇到过一位同事,非要在 SPL 里加个 GPIO 中断处理。结果呢?代码体积膨胀到 64KB,连 SRAM 都放不下了。最后只能砍掉重写。所以,记住:SPL 不是万能药,它是“最小可行产品”。

SPL 代码入口与链接脚本

SPL 的入口地址,通常由链接脚本决定。不同 SoC 的入口地址不同,但套路是一样的。

我们来看一个典型的 SPL 链接脚本片段:

MEMORY
{
    SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS
{
    .text : {
        *(.vectors)
        *(.text*)
    } > SRAM

    .data : {
        *(.data*)
    } > SRAM

    .bss : {
        *(.bss*)
    } > SRAM
}

这里有几个关键点:

  • ORIGIN:SPL 的起始地址。通常是 SoC 内部 SRAM 的基地址。
  • LENGTH:可用空间大小。我建议你留点余量,别把 SRAM 塞得满满当当。
  • .vectors:中断向量表。必须放在最前面,否则 CPU 一上电就跑飞了。

我曾经犯过一个低级错误——把 .vectors 段放到了 .text 后面。结果 CPU 上电后直接跳到了未知地址,串口连个字符都打印不出来。查了两天才发现是链接脚本顺序错了。嗯,这种教训一次就够了。

最小化硬件初始化

SPL 里的硬件初始化,讲究的是“够用就行”。我一般按这个顺序来:

  1. 关闭看门狗:防止系统复位。这是第一步,不做的话后面全白搭。
  2. 初始化时钟:至少让 CPU 和总线跑在正确频率上。
  3. 初始化串口:用于打印调试信息。没有串口,你就像瞎子摸象。
  4. 初始化 DDR:这是最复杂的一步。时序参数、ODT 配置、地址映射,一个都不能错。
  5. 初始化存储控制器:比如 NAND Flash 或 eMMC 控制器,用于读取 U-Boot。
注意:DDR 初始化是 SPL 里最容易出问题的环节。我建议你在初始化后做一次“读写测试”,确保 DDR 工作正常。否则 U-Boot 加载进去也是跑不起来的。

我记得有一次调试,DDR 初始化看起来成功了,但 U-Boot 加载后总是随机崩溃。后来发现是 DDR 的时序参数偏紧,导致高低温下不稳定。从那以后,我养成了一个习惯——在 SPL 里加一段简单的 DDR 压力测试代码。

SPL 如何加载 U-Boot

加载 U-Boot 的过程,说白了就是三步:

  1. 从存储设备读取 U-Boot 镜像:比如从 NAND Flash 的某个偏移地址开始读。
  2. 将镜像复制到 DDR 中的指定地址:这个地址通常是 U-Boot 的链接地址。
  3. 跳转到 U-Boot 的入口点:执行 U-Boot 的第一条指令。

代码实现大致是这样的:

void spl_load_uboot(void)
{
    // 1. 从 NAND Flash 读取 U-Boot
    nand_read(CONFIG_UBOOT_OFFSET, CONFIG_UBOOT_SIZE, CONFIG_UBOOT_LOAD_ADDR);

    // 2. 跳转到 U-Boot 入口
    void (*uboot_entry)(void) = (void (*)(void))CONFIG_UBOOT_ENTRY_POINT;
    uboot_entry();
}

这里有几个坑:

  • 偏移地址要对:U-Boot 在 Flash 里的存储位置,必须和烧录时一致。否则读出来的全是乱码。
  • 加载地址要对:U-Boot 编译时指定的链接地址,必须和 DDR 里的加载地址一致。否则跳转后直接崩溃。
  • 入口点要对:U-Boot 的入口点通常是 _start 符号的地址。别跳错了。
小技巧:我习惯在 SPL 跳转前,打印一条“Jumping to U-Boot at 0xXXXX”的日志。这样如果 U-Boot 没跑起来,至少能确认 SPL 已经完成了它的任务。

你想想看,SPL 就像是一个“摆渡人”。它把 U-Boot 从 Flash 这个“岸边”渡到 DDR 这个“对岸”。摆渡过程中,任何一步出错,U-Boot 都到不了对岸。所以,SPL 的调试一定要细致再细致。

下面这张图,展示了 SPL 在整个启动链中的位置和作用:

SPL 在启动链中的位置与作用 ROM Boot 固化在芯片内部 SPL 初始化 DDR + 加载 U-Boot U-Boot 加载内核 + 启动系统 SPL 内部执行流程 关闭看门狗 初始化时钟 + 串口 初始化 DDR 初始化存储控制器 读取 U-Boot 镜像 跳转到 U-Boot SPL 完成使命,控制权移交给 U-Boot

总结一下:SPL 是启动链中的“关键桥梁”。它虽然代码量小,但责任重大。DDR 初始化、U-Boot 加载、跳转执行,每一步都容不得半点马虎。我建议你在调试 SPL 时,多打日志、多做检查。毕竟,SPL 跑不起来,后面的 U-Boot 和内核都是空中楼阁。

一句话总结:SPL 的核心价值在于——用最小的代码量,完成最关键的硬件初始化和加载任务。

可可精品资料库