第4章:启动流程第一阶段(SPL)——从ROM到DDR的惊险一跳
好,咱们进入正题。上一章聊了ROM Code怎么把SPL从Flash里捞出来,扔到SRAM里跑起来。那SPL拿到接力棒之后,到底要干些什么?说白了,SPL就是个「过渡人」——它要把一个光溜溜的SoC,变成能跑完整U-Boot的环境。
我刚开始接触SPL时,总觉得它就是个「小号的U-Boot」。后来被坑了几次才明白,SPL的使命非常单一:初始化最小系统,加载并跳转到下一级Bootloader。它不需要文件系统,不需要网络协议栈,甚至不需要完整的命令行。你想想看,SRAM通常只有几十KB到几百KB,哪塞得下那么多东西?
4.1 SPL的核心职责
SPL到底要干哪些活?我把它拆成四个关键步骤:
- 设置CPU为SVC模式——关中断、清MMU、设栈指针
- 初始化时钟系统——让CPU和总线跑在目标频率
- 初始化DDR控制器——把外部DRAM训练好、能用
- 加载U-Boot主体——从Flash/NAND/SD卡读到DDR里,然后跳过去
这里面,第2步和第3步是最容易翻车的。我曾在某个项目里,DDR时序参数配错了一个寄存器,结果SPL跑完跳转时直接死机,连个日志都没留下。嗯,那感觉就像你开了门却发现地板是透明的——直接掉下去了。
SPL vs U-Boot 主体对比
| 维度 | SPL | U-Boot主体 |
|---|---|---|
| 运行位置 | 内部SRAM(通常) | 外部DDR |
| 代码大小 | 16KB~128KB | 256KB~2MB+ |
| 功能范围 | 最小系统初始化 + 加载 | 命令行、网络、文件系统、启动OS |
| 调试难度 | 极高(无串口时只能点灯) | 中等(有串口有网络) |
| 典型入口 | _start / reset_vector | board_init_f / board_init_r |
4.2 SPL入口点分析——从哪开始跑?
SPL的入口点,通常就是芯片复位后的第一个有效指令地址。但这里有个细节:SPL的入口和U-Boot主体的入口是同一个符号,都叫 _start 或 reset_vector。那怎么区分谁是谁?
答案在链接脚本里。SPL有自己独立的链接脚本,通常叫 u-boot-spl.lds。它会告诉链接器:代码段放SRAM的哪个地址,数据段放哪,栈顶在哪。
我习惯先看 arch/arm/cpu/armv7/start.S(以ARMv7为例),SPL和U-Boot主体都从这里开始。区别在于编译宏:
#ifdef CONFIG_SPL_BUILD
/* SPL 特有的初始化 */
bl lowlevel_init
bl s_init
#else
/* U-Boot 主体特有的初始化 */
bl board_init_f
#endif
你看,同一个文件,同一个入口,靠宏来分流。这种做法很常见,但也容易让人混淆。我建议你在调试时,先在 _start 处加个点灯代码,确认SPL确实跑起来了。
我的调试小技巧: 在 _start 后立即写一个GPIO翻转循环。如果LED闪烁,说明CPU已经执行到SPL入口。如果没闪,问题出在ROM Code或启动设备选择上。
4.3 时钟初始化——让SoC跑起来
SPL进来时,芯片通常跑在内部低速振荡器上,比如24MHz或32kHz。这个频率跑代码没问题,但要初始化DDR就太慢了——DDR控制器需要几百MHz的时钟才能正常工作。
时钟初始化的典型流程:
- 锁定PLL:配置主PLL、DDR PLL、外设PLL
- 切换时钟源:从低速振荡器切到PLL输出
- 配置分频器:给CPU、总线、外设分配合适频率
- 等待稳定:轮询PLL锁定标志位
以i.MX6为例,时钟初始化代码通常在 board/freescale/mx6ul_14x14_evk/spl.c 里:
void board_init_f(ulong dummy)
{
/* 设置系统时钟 */
clock_init();
/* 初始化串口(可选,用于调试) */
preloader_console_init();
/* 初始化DDR */
ddr_init();
/* 加载U-Boot */
board_boot_order(&spl_boot_list);
}
这里有个坑:时钟顺序不能乱。我遇到过一位同事,先初始化了DDR PLL,再切系统时钟,结果DDR控制器因为时钟域不同步直接挂掉。正确的做法是:先让系统时钟稳定,再初始化DDR时钟域。
警告: 某些SoC的DDR控制器和CPU共享同一个PLL。如果你改了CPU频率,DDR频率也会跟着变。这时必须重新训练DDR,否则数据读写会出错。我曾在TI AM335x上踩过这个坑,排查了整整两天。
4.4 DDR初始化——最考验耐心的环节
DDR初始化,说白了就是让外部DRAM芯片和SoC的DDR控制器「对上话」。这个过程涉及大量时序参数,任何一个不对,系统就起不来。
典型的DDR初始化步骤:
- 配置DDR控制器寄存器:设置行列地址宽度、Burst长度、CAS延迟等
- 设置PHY参数:调整DQS/DQ的延迟、ODT阻抗、驱动强度
- 执行DDR训练:写读校验、眼图扫描、Write Leveling
- 验证可用性:写一个模式再读回来,确认数据通路正常
我见过最头疼的情况是:DDR初始化成功,但跑一段时间后随机死机。后来发现是ODT(片上端接)配置不对,导致信号反射干扰。嗯,这种问题用示波器都很难抓,只能靠反复调整参数。
下面是一个简化的DDR初始化流程SVG图,帮你理清逻辑:
4.5 避坑指南——我踩过的那些坑
做SPL移植这么多年,我总结了几条血泪教训:
- DDR训练失败不要慌:先检查时钟频率是否正确,再检查PHY的延迟参数。我遇到过因为PCB走线长度差异导致DQS延迟需要微调的情况。
- SPL代码不要超过SRAM大小:链接时一定要看map文件。我曾经因为多加了几个调试打印,SPL体积超了2KB,结果链接器静默地把代码放到了DDR地址上——而DDR还没初始化呢!
- 串口输出不一定可靠:在DDR初始化完成前,串口可能还没配好。这时可以用GPIO点灯或JTAG来调试。
- 注意Cache和MMU的状态:SPL阶段通常关MMU、关Cache。如果你不小心开了,访问DDR时可能会读到脏数据。
我的习惯: 每次改DDR参数后,先在SPL里做一次完整的读写校验——写0xAA、0x55、0x00000000、0xFFFFFFFF到每个地址,再读回来比对。这个测试虽然慢,但能发现90%的DDR问题。
4.6 小结
SPL阶段是整个Bootloader流程里最脆弱的一环。它没有完整的调试环境,没有文件系统,甚至没有可靠的串口输出。但正是这个「小不点」,决定了你的系统能不能从ROM顺利过渡到DDR,从裸机过渡到完整的U-Boot环境。
我个人觉得,SPL移植最考验的是对芯片数据手册的理解深度。时钟树怎么走?DDR时序参数怎么算?这些在手册里都有,但需要你一行一行地啃。别怕麻烦,我当年为了调通一个DDR训练,把500页的DDR控制器手册翻了三遍。