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)。它的任务很简单:

  1. 初始化最基本的硬件(时钟、DDR控制器)。
  2. 把完整的U-Boot从存储介质(NAND、SD卡、eMMC)加载到DDR。
  3. 跳转到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 核心执行流程 复位向量入口 设置CPU模式(SVC) 关闭中断(CPSR) 初始化关键寄存器 设置堆栈指针(SP) 跳转到C入口(main) 注意 不同SoC的复位向量 地址可能不同 需查阅芯片手册

这张图展示了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函数会按特定顺序初始化硬件。这个顺序是有讲究的:

  1. 时钟系统:先让CPU跑起来,否则后面都是空谈。
  2. 串口:输出调试信息。我习惯在串口初始化后立刻打印一个字符,确认代码跑到了这里。
  3. 定时器:为后续的延时操作提供基础。
  4. DDR控制器:这是最关键的一步。参数配错,系统直接挂掉。
  5. 存储控制器:初始化NAND、SD卡等接口。
  6. 加载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,你就理解了整个嵌入式启动链的“第一公里”。