3. U-Boot启动流程分析(上):CPU上电复位、SPL执行流程与start.S解析
大家好,我是你们的嵌入式系统讲师。今天我们来啃一块硬骨头——U-Boot的启动流程。说实话,很多工程师做了好几年嵌入式,对U-Boot的启动细节还是一知半解。我个人觉得,搞懂这一章,你才算真正入了Bootloader的门。
咱们分两讲来拆解。这一讲,我们聚焦在CPU上电复位、SPL(第一阶段)的执行流程,以及那个让人又爱又恨的start.S汇编代码。
3.1 从CPU上电复位说起
你想想看,CPU上电的那一刻,它知道自己要干什么吗?
答案是:不知道。但它有一个“出厂设定”。
CPU复位后,硬件会自动做几件事:
- PC指针指向复位向量:通常是0x00000000或0xFFFF0000(取决于架构)。
- 进入特权模式:比如ARM的SVC模式,确保能访问所有资源。
- 禁用中断:防止在初始化过程中被打断。
- 初始化关键寄存器:比如堆栈指针、状态寄存器等。
嗯,这里要注意:不同架构的复位行为有差异。以ARM Cortex-A为例,复位后从0x00000000取第一条指令。这条指令通常是一条跳转指令,直接跳到start.S的入口。
核心要点:CPU上电后的第一件事,就是执行复位向量处的代码。这个代码就是U-Boot的起点,也就是start.S。
3.2 SPL——U-Boot的“迷你版”自己
早期的U-Boot是“一锤子买卖”:一个二进制文件,从ROM加载到RAM,然后执行。但现在的SoC越来越复杂,内部SRAM小得可怜(通常只有几十KB),根本装不下完整的U-Boot。
怎么办?
行业里想了个办法:搞一个“迷你版”的U-Boot,叫SPL(Secondary Program Loader)。它的任务很简单:
- 初始化最基本的硬件(时钟、DDR控制器)。
- 把完整的U-Boot从存储介质(NAND、SD卡、eMMC)加载到DDR。
- 跳转到U-Boot的入口。
说白了,SPL就是一个“搬运工”。
| 特性 | SPL | 完整U-Boot |
|---|---|---|
| 代码大小 | 通常 < 64KB | 几百KB到几MB |
| 运行位置 | 内部SRAM | 外部DDR |
| 功能 | 极简初始化 + 加载 | 完整命令行、文件系统、网络 |
| 编译产物 | u-boot-spl.bin | u-boot.bin |
我的经验:我在调试一块i.MX6ULL板子时,SPL死活加载不了U-Boot。后来发现是DDR时序参数配错了。SPL阶段DDR初始化一旦失败,整个系统就卡死了。所以,SPL的DDR初始化代码,我建议你反复检查。
3.3 start.S——一切从这里开始
start.S是U-Boot的入口文件,也是整个启动流程中最底层的部分。它全部用汇编写成,因为此时C语言运行时环境还没建立。
它的主要工作流程,我画了一张图:
这张图展示了start.S的典型执行路径。但实际代码中,还有更多细节。
3.4 关键汇编代码片段解析
我们来看一段真实的start.S代码(以ARMv7为例):
/*
* 复位向量入口
*/
.globl _start
_start:
b reset @ 跳转到reset标签
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
reset:
/*
* 设置CPU为SVC模式,关闭中断
*/
mrs r0, cpsr
bic r0, r0, #0x1f @ 清除模式位
orr r0, r0, #0xd3 @ SVC模式 + 关IRQ/FIQ
msr cpsr, r0
/*
* 关闭MMU和Cache
* 此时还没初始化DDR,MMU必须关掉
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x0005 @ 关MMU和DCache
bic r0, r0, #0x1000 @ 关ICache
mcr p15, 0, r0, c1, c0, 0
/*
* 设置堆栈指针
* 注意:此时SP指向内部SRAM的顶部
*/
ldr sp, =CONFIG_SPL_STACK
/*
* 跳转到C语言入口
*/
bl board_init_f
我曾经踩过的坑:在设置堆栈指针时,我直接把SP指向了DDR地址。结果SPL一调用函数就崩溃。为什么?因为此时DDR还没初始化!SP必须指向内部SRAM的有效区域。这个错误我查了整整两天。
3.5 SPL阶段的硬件初始化顺序
SPL的board_init_f函数会按特定顺序初始化硬件。这个顺序是有讲究的:
- 时钟系统:先让CPU跑起来,否则后面都是空谈。
- 串口:输出调试信息。我习惯在串口初始化后立刻打印一个字符,确认代码跑到了这里。
- 定时器:为后续的延时操作提供基础。
- DDR控制器:这是最关键的一步。参数配错,系统直接挂掉。
- 存储控制器:初始化NAND、SD卡等接口。
- 加载U-Boot:从存储介质读取完整U-Boot到DDR。
我的建议:在SPL阶段,每完成一个初始化步骤,就在串口输出一个标记字符。比如时钟初始化完打印'A',DDR初始化完打印'B'。这样如果系统卡死,你一眼就能看出是哪个环节出了问题。这招我在调试时屡试不爽。
3.6 从SPL跳转到U-Boot
SPL的最后一步,是跳转到DDR中的U-Boot入口。这个跳转通常是这样实现的:
/*
* SPL加载U-Boot到DDR后
* 跳转到U-Boot的入口地址
*/
ldr pc, =CONFIG_SYS_UBOOT_BASE
嗯,这里要注意:跳转前要确保:
- DDR已经稳定工作。
- U-Boot镜像已经完整加载到指定地址。
- 堆栈指针已经切换到DDR空间(可选,但推荐)。
跳转之后,SPL的使命就完成了。控制权完全交给U-Boot,它会重新初始化一些硬件,然后进入命令行环境。
总结一下:SPL的本质是一个“最小启动加载器”。它用最少的代码完成最核心的任务——初始化DDR,加载U-Boot。理解了SPL,你就理解了整个嵌入式启动链的“第一公里”。