3、SPL阶段详解:SPL的作用与限制、SPL的初始化流程(时钟、DDR、串口)、如何判断SPL是否成功。
好,咱们接着聊。上一章我们把U-Boot的整体启动流程捋了一遍,这一章咱们深入进去,看看启动的第一个阶段——SPL。
SPL,全称是Secondary Program Loader,说白了就是“第二级程序加载器”。你可能会问,为什么叫“第二级”?因为芯片上电后,固化在ROM里的代码(也就是第一级)先跑,它把SPL从Flash里搬出来,然后跳过去执行。SPL的任务很纯粹:初始化最基本的硬件,然后把真正的U-Boot(也就是U-Boot Proper)加载到DDR里运行。
核心理解:SPL是U-Boot的“瘦身版”,它只做一件事——为加载完整的U-Boot铺路。
SPL的作用与限制
SPL的作用,我总结下来就三点:
- 初始化最小系统:时钟、DDR、串口,这三样是必须的。没有时钟,芯片不工作;没有DDR,代码没地方放;没有串口,你没法调试。
- 加载U-Boot Proper:从Flash、SD卡或者eMMC里把完整的U-Boot镜像读到DDR里。
- 跳转执行:把CPU的控制权交给U-Boot Proper。
但SPL的限制也很明显。你想想看,它是在DDR还没初始化的时候运行的,所以代码只能放在芯片内部的SRAM里。这个SRAM通常很小,几KB到几百KB不等。所以SPL的代码必须非常精简,不能像完整的U-Boot那样什么功能都往里塞。
注意:SPL的代码大小受限于SRAM容量。我曾经在一个项目里,SPL编译出来刚好超了2KB,结果死活跑不起来。后来硬是砍掉了一个不必要的调试打印,才搞定。所以,写SPL代码时,每一行都要精打细算。
SPL的初始化流程
SPL的初始化流程,说白了就是一条线:从时钟开始,到DDR,再到串口。咱们一步步看。
1. 时钟初始化
时钟是芯片的心脏。SPL的第一步,就是把CPU、总线、外设的时钟配好。不同的芯片,时钟树不一样,但思路是相通的:先配PLL,再配分频器。
我习惯在时钟初始化时,先确认几个关键频率:CPU主频、DDR频率、串口波特率时钟。这些频率配错了,后面全白搭。
// 伪代码示例:时钟初始化
void clock_init(void) {
// 1. 配置PLL,锁定目标频率
pll_config(CPU_PLL, 1000); // CPU主频1GHz
pll_config(DDR_PLL, 533); // DDR频率533MHz
// 2. 等待PLL稳定
while(!pll_is_locked(CPU_PLL));
while(!pll_is_locked(DDR_PLL));
// 3. 切换时钟源到PLL输出
clock_switch_source(CPU_CLK, CPU_PLL);
clock_switch_source(DDR_CLK, DDR_PLL);
}
小技巧:时钟初始化后,我通常会加一个GPIO翻转的调试手段。比如让某个LED灯闪一下,这样不用串口也能知道时钟跑起来了。
2. DDR初始化
DDR初始化是SPL里最复杂、也最容易出问题的一步。DDR的时序参数、电压、阻抗匹配,任何一个不对,系统就挂在这里。
DDR初始化的核心步骤:
- 配置DDR控制器:设置行列地址宽度、Burst长度、CAS延迟等。
- 配置PHY:调整DQS、DQ的延迟,保证数据采样正确。
- 执行训练:很多芯片支持DDR自动训练,比如写电平训练、读DQS门限训练。
- 验证:写一个已知数据,再读回来,检查是否一致。
// 伪代码示例:DDR初始化与验证
void ddr_init(void) {
// 1. 配置DDR控制器寄存器
ddr_ctrl_config(DDR_TYPE_DDR3, 16, 10, 3); // 16位宽,10行列地址,CAS=3
// 2. 配置PHY
ddr_phy_config(DQS_DELAY, 0x20);
ddr_phy_config(DQ_DELAY, 0x18);
// 3. 执行训练
ddr_training();
// 4. 验证:写0xAA55AA55到DDR地址0x1000,再读回来
*(volatile uint32_t *)0x1000 = 0xAA55AA55;
if (*(volatile uint32_t *)0x1000 != 0xAA55AA55) {
// DDR初始化失败
while(1); // 死在这里
}
}
避坑指南:我曾经在一个项目里,DDR初始化总是失败。查了两天,最后发现是PCB走线导致DQS和DQ的延迟不匹配。后来在PHY配置里手动调整了DQS延迟,问题才解决。所以,如果DDR训练失败,先检查硬件设计,别光盯着软件。
3. 串口初始化
串口是调试的“眼睛”。SPL里初始化串口,主要是为了打印日志,方便我们判断执行到了哪一步。
串口初始化很简单:设置波特率、数据位、停止位、校验位。大部分芯片的UART控制器都差不多,配几个寄存器就行。
// 伪代码示例:串口初始化
void uart_init(void) {
// 1. 使能UART时钟
clock_enable(UART_CLK);
// 2. 配置波特率(假设时钟频率为24MHz,目标波特率115200)
uart_set_baudrate(24000000, 115200);
// 3. 配置数据格式:8位数据,1位停止位,无校验
uart_set_format(8, 1, UART_PARITY_NONE);
// 4. 使能UART
uart_enable();
}
个人经验:串口初始化后,我习惯先打印一个字符,比如'U'。如果能看到这个字符,说明串口工作正常。否则,先检查波特率对不对,再检查引脚复用配置。
如何判断SPL是否成功
SPL是否成功,说白了就是看它能不能把U-Boot Proper加载到DDR里,并跳转过去。判断方法有几种:
- 看串口日志:SPL成功的话,通常会打印类似“U-Boot SPL 2023.07”这样的信息。如果能看到,说明SPL至少跑到了串口初始化之后。
- 看DDR验证:SPL在初始化DDR后,会做一次读写验证。如果验证通过,说明DDR工作正常。
- 看跳转地址:SPL在跳转到U-Boot Proper之前,会打印跳转地址。比如“Jumping to 0x82000000”。如果能看到这个打印,说明SPL已经完成了它的使命。
判断标准:SPL成功的标志,是U-Boot Proper开始执行。如果U-Boot Proper的日志出来了,那SPL肯定没问题。
如果SPL失败了,通常表现为:
- 没有任何输出(串口没初始化或时钟不对)
- 输出乱码(波特率不对)
- 卡在某个地方(DDR初始化失败或Flash读取失败)
嗯,这里要注意:SPL失败时,不要慌。先检查最基础的时钟和电源,再检查DDR配置,最后检查Flash驱动。我遇到过很多次,问题其实出在硬件上,比如DDR供电电压不对,或者Flash引脚虚焊。
这张图把SPL的流程和判断点画得很清楚。你可以看到,SPL是一条直线,每一步都依赖上一步。如果哪一步卡住了,后面的就不用想了。
我的习惯:在SPL的每个关键步骤后加一个打印,比如“Clock OK”、“DDR OK”、“UART OK”。这样一旦出问题,看日志就知道卡在哪一步。省时省力。
好了,SPL阶段就聊这么多。记住,SPL是U-Boot的“先遣队”,它虽然小,但责任重大。把SPL搞定了,后面的U-Boot Proper就顺理成章了。