第四章:U-Boot启动流程分析:第一阶段(SPL)、第二阶段(U-Boot proper)、启动介质选择
大家好,我是你们的讲师。今天我们来聊聊U-Boot的启动流程。说实话,很多工程师做了好几年嵌入式,对U-Boot的启动细节还是一知半解。我个人觉得,搞懂这个流程,就像打通了任督二脉,后面看什么都通透。
U-Boot的启动,说白了就是两个阶段:SPL和U-Boot proper。为什么非要拆成两段?嗯,这里有个历史原因——早期的芯片内部SRAM太小了,装不下完整的U-Boot。所以先搞个迷你版(SPL)把DDR初始化好,再把完整版从存储介质里搬进去。
核心要点:SPL负责初始化最小系统(时钟、DDR、存储控制器),然后加载U-Boot proper到DDR中运行。U-Boot proper才是我们平时打交道的那个完整Bootloader。
4.1 第一阶段:SPL(Secondary Program Loader)
SPL是什么?说白了就是U-Boot的"瘦身版"。我记得刚开始做量产方案时,总以为SPL可有可无。直到有一次在NAND Flash上栽了跟头——芯片内部SRAM只有64KB,完整U-Boot编译出来300多KB,根本塞不进去。这才老老实实研究SPL。
SPL的核心任务其实就三件事:
- 初始化硬件:时钟、DDR控制器、存储控制器。这是最关键的,搞不定DDR,后面啥也干不了。
- 加载U-Boot proper:从选定的启动介质里把完整U-Boot读到DDR中。
- 跳转执行:设置好栈指针和跳转地址,把控制权交给U-Boot proper。
我的经验:SPL的代码量一般控制在16KB以内。为什么?因为很多芯片的SRAM就那么大。我曾经在某个项目里为了省200字节,把调试打印全删了——结果出问题没法定位,后来学乖了,至少保留一个"LED闪烁"的调试手段。
SPL的入口函数通常是spl_main(),我们来看看它的典型流程:
// 简化后的SPL主流程
void spl_main(void)
{
// 1. 初始化时钟和DDR
clock_init();
ddr_init();
// 2. 确定启动介质
boot_device = spl_boot_device();
// 3. 从介质加载U-Boot proper
spl_load_image(boot_device);
// 4. 跳转到U-Boot proper入口
jump_to_image_no_args();
}
你可能会问:为什么SPL不直接加载内核?嗯,这里有个设计哲学——SPL只做"最小必要"的事。加载内核是U-Boot proper的活,SPL只管把U-Boot proper请进DDR就行。
4.2 第二阶段:U-Boot proper
U-Boot proper就是我们常说的"完整版U-Boot"。它比SPL复杂得多,功能也丰富得多。我个人习惯把它的启动流程分成三个子阶段:
- 板级初始化:调用
board_init_f(),初始化串口、定时器、GPIO等外设。 - 内存重定位:把U-Boot自身从加载地址复制到DDR的高地址区域,腾出空间给内核。
- 进入主循环:调用
main_loop(),等待用户输入或自动启动内核。
关键点:U-Boot proper的入口是_start,但实际C代码入口是board_init_f()。中间那段汇编代码主要做两件事:设置异常向量表、初始化栈指针。
这里有个容易踩的坑——内存重定位。我曾经在某个项目里,U-Boot proper加载到DDR后,一运行就死机。查了两天才发现,原来是重定位时没处理好全局变量的地址偏移。说白了,就是编译时用的地址和运行时实际地址对不上。
// U-Boot proper启动流程(简化)
_start:
// 汇编阶段:设置异常向量
b reset
// ... 其他异常向量
reset:
// 设置栈指针
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
// 跳转到C代码
bl board_init_f
board_init_f():
// 初始化串口、定时器等
serial_init();
timer_init();
// 初始化DDR(如果SPL没做)
dram_init();
// 返回重定位地址
return gd->relocaddr;
// 重定位后
board_init_r():
// 初始化完整功能
env_init(); // 环境变量
devices_init(); // 块设备
// 进入主循环
main_loop();
4.3 启动介质选择
启动介质选择,说白了就是告诉SPL:"嘿,去哪个地方找U-Boot proper?" 这个选择通常由硬件决定——芯片会采样几个GPIO引脚的电平,或者读取一个Boot Mode寄存器。
常见的启动介质有这些:
| 启动介质 | 典型应用场景 | 注意事项 |
|---|---|---|
| NAND Flash | 大容量存储,成本敏感 | 需要坏块管理、ECC校验 |
| eMMC/SD卡 | 消费电子,性能要求高 | 分区表格式、FAT/EXT4文件系统 |
| SPI NOR Flash | 小容量,快速启动 | 支持XIP(原地执行),但容量小 |
| 网络/USB | 调试阶段,量产烧录 | 依赖外部主机,不适合产品 |
避坑指南:我曾经在一个量产项目里,SPL从NAND Flash加载U-Boot proper时,总是随机失败。查到最后发现是NAND的页大小配置错了——芯片是2KB页,我配成了512字节。这种错误在调试时偶尔能跑,量产时就会暴露。所以,启动介质的参数一定要和芯片手册一一核对。
SPL如何知道用哪个介质?看这段代码:
// 获取启动设备
u32 spl_boot_device(void)
{
// 读取硬件寄存器
u32 boot_mode = readl(BOOT_MODE_REG);
switch (boot_mode) {
case BOOT_MODE_NAND:
return BOOT_DEVICE_NAND;
case BOOT_MODE_MMC:
return BOOT_DEVICE_MMC;
case BOOT_MODE_SPI:
return BOOT_DEVICE_SPI;
default:
// 默认从eMMC启动
return BOOT_DEVICE_MMC;
}
}
你想想看,如果硬件设计时Boot Mode引脚没拉好,或者寄存器读错了,SPL就会去错误的地方找U-Boot proper。结果就是——板子变砖。所以量产时,我建议在SPL里加一段"启动介质确认"的打印,至少能知道它到底去哪个设备找了。
4.4 总结一下
U-Boot的启动流程,说白了就是"接力跑":芯片内部ROM先跑一小段,把SPL请进来;SPL初始化DDR后,把U-Boot proper请进来;U-Boot proper最后把内核请进来。每一棒只做自己该做的事,不多干也不少干。
启动介质的选择,是这场接力赛的"起跑线"。选错了,后面全白搭。我个人建议,在设计量产方案时,先把启动介质这块吃透——读芯片手册的Boot章节,看原理图的Boot Mode引脚,再配合SPL的打印信息验证。这一步走稳了,后面的烧录方案才能站得住脚。
我的习惯:每次拿到新板子,第一件事就是看SPL的串口输出。如果能看到"U-Boot SPL 2023.07"这样的字样,说明SPL跑起来了。如果连这个都没有,那就要检查硬件连接或者启动介质配置了。
好了,这一章的内容就到这里。启动流程是基础,但也是量产烧录方案设计的根基。下一章我们会深入SPL的代码细节,看看它到底是怎么从NAND Flash里把U-Boot proper读出来的。
可可精品资料库