第二章:Bootloader第一阶段(SPL/Preloader)——初始化最小系统
好,我们继续往下走。上一章我们聊了ROM Code,它把Bootloader的第一阶段加载到了SRAM里。这一章,我们就来深入看看这个第一阶段——SPL(Secondary Program Loader)或者叫Preloader,到底在干什么。
说白了,SPL的任务就四个字:搭建舞台。它要为后面那个真正的、完整的Bootloader(比如U-Boot)准备好运行环境。这个环境有多小?小到只需要CPU、内存(DDR)、时钟和最基本的串口输出就够了。
核心目标:初始化DDR控制器、配置系统时钟、使能串口,然后加载完整的Bootloader到DDR中,跳转过去。
2.1 为什么需要SPL?
你可能会问:为什么不能一步到位,直接加载U-Boot?
原因其实很简单:SRAM太小了。我见过很多芯片,SRAM只有几十KB到几百KB。而一个完整的U-Boot,动辄几百KB甚至几MB。SRAM根本装不下。
所以,我们得先搞一个“迷你版”的Bootloader,它小到能塞进SRAM里。它的任务不是引导系统,而是初始化DDR。DDR一旦可用,我们就有了大容量的内存空间,这时候再把完整的U-Boot从存储介质(eMMC、NAND Flash等)里搬进去。
我的经验:有一次我在调试一块新板子,SPL死活跑不起来。后来发现是DDR的时序参数配错了。嗯,DDR初始化这一步,真的是牵一发而动全身。我建议你拿到一个新平台,先花时间把DDR的datasheet吃透。
2.2 SPL的核心工作流程
SPL的执行流程非常清晰,大致可以分为以下几步:
- 设置异常向量表——确保CPU能正确处理中断和异常。
- 初始化CPU时钟——把CPU从默认的低频状态提升到目标频率。
- 初始化DDR控制器——配置DDR的时序、频率、电压等参数。
- 初始化串口——输出调试信息,方便我们观察启动过程。
- 加载完整Bootloader——从存储设备读取U-Boot到DDR中。
- 跳转到U-Boot入口——把控制权交给下一阶段。
我们一个一个来看。
2.3 初始化DDR——最关键的步骤
DDR初始化,是SPL里技术含量最高的部分。为什么?因为DDR的时序参数极其敏感,稍微配错一点,系统就起不来。
DDR控制器初始化通常包括:
- PLL配置:生成DDR所需的工作频率。
- 时序参数设置:比如CAS延迟、tRCD、tRP等。这些参数必须和DDR芯片的datasheet严格对应。
- ZQ校准:用于调整DDR的驱动强度,保证信号质量。
- DDR PHY初始化:物理层的训练,比如读写平衡、DQS门控等。
来看一段典型的DDR初始化代码(伪代码风格):
void ddr_init(void) {
// 1. 配置PLL,设置DDR频率为1600MHz
pll_config(DDR_PLL, 1600);
// 2. 设置时序参数
ddr_timing.tCAS = 11;
ddr_timing.tRCD = 11;
ddr_timing.tRP = 11;
ddr_timing.tRAS = 28;
// 3. 执行ZQ校准
ddr_zq_calibrate();
// 4. 初始化DDR PHY
ddr_phy_init();
// 5. 等待DDR就绪
while (!ddr_is_ready());
}
注意:不同厂商的DDR控制器,初始化流程差异很大。比如高通和联发科的方案就完全不同。我建议你直接参考芯片厂商提供的参考代码,不要自己从头写。
2.4 时钟初始化——让CPU跑起来
SPL刚启动时,CPU通常运行在一个很低的频率上(比如24MHz或32kHz)。这个频率下,系统响应很慢。所以SPL要做的第一件事,就是把时钟提上去。
时钟初始化一般包括:
- 配置PLL:生成CPU、总线、外设所需的各种时钟。
- 设置时钟分频:确保各个模块工作在正确的频率下。
- 使能时钟门控:打开需要用到的外设时钟,比如UART、DDR控制器等。
我记得有一次,我忘了使能UART的时钟门控,结果串口死活没有输出。查了半天才发现是时钟没开。这种小细节,真的很容易忽略。
2.5 串口初始化——调试的窗口
串口是SPL阶段最重要的调试手段。没有串口输出,你根本不知道系统卡在哪里。
串口初始化很简单:
- 设置波特率(通常是115200或921600)。
- 配置数据位、停止位、校验位。
- 使能发送和接收。
代码示例:
void uart_init(void) {
// 设置波特率115200
uart_set_baudrate(115200);
// 8位数据,1位停止位,无校验
uart_set_format(8, 1, UART_PARITY_NONE);
// 使能TX和RX
uart_enable(UART_TX | UART_RX);
}
我的习惯:在SPL的每个关键步骤之后,都打印一条调试信息。比如“DDR init done”、“Clock init done”、“Loading U-Boot...”。这样一旦系统卡住,我就能立刻知道是哪个环节出了问题。
2.6 加载完整Bootloader
DDR和时钟都准备好了,接下来就是把U-Boot从存储设备里读出来。
加载方式取决于存储介质:
| 存储介质 | 加载方式 | 特点 |
|---|---|---|
| eMMC | 通过MMC控制器读取 | 速度快,支持分区 |
| NAND Flash | 通过NAND控制器读取 | 需要处理坏块和ECC |
| SPI NOR Flash | 通过SPI接口读取 | 容量小,但启动简单 |
| UFS | 通过UFS控制器读取 | 速度最快,但协议复杂 |
加载完成后,SPL会跳转到U-Boot的入口地址。这个地址通常是DDR中的某个固定位置,比如0x80000000。
2.7 避坑指南
最后,分享几个我在项目中踩过的坑:
- DDR时序参数错误:我曾经因为datasheet看错了一个参数,导致DDR初始化失败。后来用逻辑分析仪抓波形才找到问题。建议你拿到新板子,先用厂商提供的DDR测试工具验证一下。
- 时钟频率过高:有一次我把CPU频率配得太高,结果系统跑起来不稳定,随机死机。降频后就正常了。嗯,频率不是越高越好,稳定才是王道。
- 串口波特率不匹配:SPL和终端软件的波特率必须一致。我见过有人SPL里配的是115200,但终端软件设成了9600,结果输出全是乱码。
- 存储设备初始化失败:eMMC或NAND Flash的初始化顺序很重要。比如eMMC需要先发送CMD0复位,再发送CMD1获取OCR。顺序错了,设备就认不到。
总结一下:SPL阶段虽然代码量不大,但它是整个启动流程的基石。DDR、时钟、串口,这三个东西只要有一个没配好,后面的所有工作都是白搭。所以,我建议你在这个阶段多花点时间,把基础打牢。
下一章,我们会进入Bootloader的第二阶段——U-Boot。它会在SPL搭建好的舞台上,完成更复杂的初始化工作,比如设备树解析、驱动加载等。我们到时候见。