3. SPL (Secondary Program Loader) 概述:SPL在启动链中的位置、作用与基本工作原理
3.1 SPL在嵌入式启动链中的位置
在典型的嵌入式Linux启动流程中,SPL(Secondary Program Loader)位于启动链的第二阶段,紧接在第一阶段启动ROM(BootROM)之后。BootROM通常固化在SoC内部,负责从预定义的存储介质(如NAND Flash、eMMC、SD卡或SPI NOR Flash)中加载并执行SPL。SPL完成硬件初始化后,再加载并跳转到完整的U-Boot(或ATF BL2等)主引导程序。
启动链关键阶段: BootROM → SPL → U-Boot / ATF BL2 → Kernel → RootFS
SPL的出现主要为了解决BootROM容量限制与主引导程序功能膨胀之间的矛盾。BootROM通常只有几KB到几十KB的片上SRAM可用,无法承载功能完整的引导程序;而SPL作为精简的“桥梁”,在有限的资源下完成关键初始化,为后续加载完整引导程序铺平道路。
3.2 SPL的核心作用
SPL在启动链中承担着承上启下的关键角色,其核心作用可以归纳为以下五个方面:
| 作用 | 说明 | 典型实现 |
|---|---|---|
| 1. 硬件最小初始化 | 初始化时钟、PLL、引脚复用、看门狗等基础外设 | board_init_f() 阶段 |
| 2. DDR控制器初始化 | 配置DDR时序、频率、电压,使能内存控制器 | ddr_init() / dram_init() |
| 3. 存储介质驱动 | 驱动NAND、eMMC、SD、SPI Flash等存储设备 | SPL DM驱动模型 |
| 4. 加载主引导程序 | 从存储介质读取U-Boot/ATF到DDR并校验 | spl_load_image() |
| 5. 跳转执行 | 设置CPU状态、栈指针,跳转到主引导程序入口 | spl_jump_to_image() |
设计要点: SPL必须保持极小的代码体积(通常限制在16KB~64KB),因此只能包含最必要的驱动和功能。复杂的设备树解析、文件系统支持、网络协议栈等通常留给U-Boot主程序处理。
3.3 SPL的基本工作原理
SPL的工作流程可以划分为四个主要阶段,每个阶段都有明确的目标和约束:
- 入口与早期初始化(start.S): 设置CPU模式、关闭中断、初始化栈指针、清零BSS段。此阶段通常用汇编语言编写,与SoC架构紧密相关。
- 板级初始化(board_init_f): 初始化时钟、串口、GPIO、定时器等基础外设,完成DDR控制器的初始化。此阶段仍在SRAM中运行,栈空间有限。
- SPL重定位(可选): 如果SPL体积较大或需要更大的栈空间,可以将自身从SRAM复制到DDR中继续执行。部分SoC跳过此步骤。
- 加载与跳转(spl_load_image + spl_jump_to_image): 从存储介质读取主引导程序到DDR指定地址,进行完整性校验(如CRC或哈希),然后设置跳转参数并执行。
3.4 SPL与BootROM、U-Boot的对比
为了更清晰地理解SPL在启动链中的定位,下表从多个维度对比BootROM、SPL和U-Boot:
| 对比维度 | BootROM | SPL | U-Boot |
|---|---|---|---|
| 存储位置 | SoC内部ROM | Flash / eMMC / SD | Flash / eMMC / SD |
| 运行位置 | 片上SRAM | 片上SRAM → DDR | DDR |
| 典型大小 | 4KB ~ 32KB | 16KB ~ 128KB | 256KB ~ 2MB |
| 可修改性 | 不可修改(掩膜ROM) | 可更新(通过烧录) | 可更新(通过烧录) |
| 主要功能 | 加载SPL到SRAM | 初始化DDR,加载U-Boot | 加载Kernel、DTB、RootFS |
| 驱动复杂度 | 极简(仅存储介质驱动) | 精简(必要外设+存储) | 完整(网络、USB、文件系统等) |
3.5 SPL的代码结构示例
以U-Boot SPL为例,其代码组织通常遵循以下目录结构:
u-boot/
├── arch/arm/mach-xxx/ # SoC架构相关代码
│ ├── start.S # SPL入口汇编
│ ├── lowlevel_init.S # 低层初始化
│ └── spl.c # SPL板级初始化
├── board/vendor/board_name/ # 板级支持
│ ├── board.c # board_init_f() 实现
│ └── Kconfig # SPL配置选项
├── common/
│ ├── spl/
│ │ ├── spl.c # SPL核心框架
│ │ ├── spl_mmc.c # MMC/SD加载实现
│ │ ├── spl_nand.c # NAND加载实现
│ │ └── spl_spi.c # SPI Flash加载实现
│ └── image.c # 镜像解析与校验
├── drivers/
│ ├── ddr/ # DDR控制器驱动
│ ├── serial/ # 串口驱动
│ └── spi/ # SPI驱动
└── include/
├── spl.h # SPL API头文件
└── configs/board.h # 板级配置宏
常见陷阱: SPL开发中最容易遇到的问题包括:DDR时序配置错误导致系统无法启动、SPL体积超出SRAM限制、存储介质驱动未正确初始化导致加载失败。建议在开发初期通过串口输出调试信息,逐步验证每个阶段的正确性。
3.6 SPL的启动时间优化要点
SPL作为启动链中的关键环节,其执行时间直接影响整体启动速度。以下是几个常见的优化方向:
- 减少不必要的初始化: 只初始化启动必需的硬件,将非关键外设的初始化推迟到U-Boot或Kernel阶段。
- 优化DDR初始化: DDR训练和校准是SPL中最耗时的操作之一,可以通过使用预训练参数或快速训练模式来缩短时间。
- 提高存储介质读取速度: 使用DMA传输、增大读取块大小、启用缓存等策略。
- 压缩SPL自身: 如果SPL体积接近SRAM上限,可以考虑使用压缩技术,由BootROM解压后执行。
- 并行化加载: 在支持多通道存储介质的平台上,可以并行加载U-Boot的多个部分。
典型优化效果: 通过上述优化,SPL阶段的执行时间可以从数百毫秒降低到数十毫秒级别,为整体启动时间优化奠定基础。