第二章: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的执行流程非常清晰,大致可以分为以下几步:

  1. 设置异常向量表——确保CPU能正确处理中断和异常。
  2. 初始化CPU时钟——把CPU从默认的低频状态提升到目标频率。
  3. 初始化DDR控制器——配置DDR的时序、频率、电压等参数。
  4. 初始化串口——输出调试信息,方便我们观察启动过程。
  5. 加载完整Bootloader——从存储设备读取U-Boot到DDR中。
  6. 跳转到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阶段最重要的调试手段。没有串口输出,你根本不知道系统卡在哪里。

串口初始化很简单:

  1. 设置波特率(通常是115200或921600)。
  2. 配置数据位、停止位、校验位。
  3. 使能发送和接收。

代码示例:

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搭建好的舞台上,完成更复杂的初始化工作,比如设备树解析、驱动加载等。我们到时候见。