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 里加个 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 里的硬件初始化,讲究的是“够用就行”。我一般按这个顺序来:
- 关闭看门狗:防止系统复位。这是第一步,不做的话后面全白搭。
- 初始化时钟:至少让 CPU 和总线跑在正确频率上。
- 初始化串口:用于打印调试信息。没有串口,你就像瞎子摸象。
- 初始化 DDR:这是最复杂的一步。时序参数、ODT 配置、地址映射,一个都不能错。
- 初始化存储控制器:比如 NAND Flash 或 eMMC 控制器,用于读取 U-Boot。
我记得有一次调试,DDR 初始化看起来成功了,但 U-Boot 加载后总是随机崩溃。后来发现是 DDR 的时序参数偏紧,导致高低温下不稳定。从那以后,我养成了一个习惯——在 SPL 里加一段简单的 DDR 压力测试代码。
SPL 如何加载 U-Boot
加载 U-Boot 的过程,说白了就是三步:
- 从存储设备读取 U-Boot 镜像:比如从 NAND Flash 的某个偏移地址开始读。
- 将镜像复制到 DDR 中的指定地址:这个地址通常是 U-Boot 的链接地址。
- 跳转到 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 就像是一个“摆渡人”。它把 U-Boot 从 Flash 这个“岸边”渡到 DDR 这个“对岸”。摆渡过程中,任何一步出错,U-Boot 都到不了对岸。所以,SPL 的调试一定要细致再细致。
下面这张图,展示了 SPL 在整个启动链中的位置和作用:
总结一下:SPL 是启动链中的“关键桥梁”。它虽然代码量小,但责任重大。DDR 初始化、U-Boot 加载、跳转执行,每一步都容不得半点马虎。我建议你在调试 SPL 时,多打日志、多做检查。毕竟,SPL 跑不起来,后面的 U-Boot 和内核都是空中楼阁。
可可精品资料库