3. 硬件初始化起点:复位向量、异常向量表、CPU模式切换(SVC模式)

U-Boot 启动的第一行代码,到底在哪里?

很多人以为入口是 _start,或者某个 C 函数。其实不对。CPU 上电后,硬件会强制跳转到一个固定地址——复位向量。这才是 U-Boot 的起点。

我刚开始接触 U-Boot 时,也踩过这个坑。当时想当然地以为入口是 start.S 的第一行,结果调试了半天,发现 PC 指针根本没跑到那里去。后来才明白,是链接脚本里的 ENTRY 和硬件复位向量没对齐。

3.1 复位向量:CPU 上电后的第一站

ARM 架构下,CPU 复位后默认从 0x00000000 或 0xFFFF0000 取指(具体由硬件配置决定)。这个地址存放的就是复位向量。

U-Boot 的链接脚本(比如 u-boot.lds)会保证 _start 符号正好落在这个地址上。我们来看一段典型的 ARM 启动代码:

_start:
    b       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

注意第一行:b reset。这是一个相对跳转指令,跳转到真正的初始化代码。为什么不用 ldr pc, =reset?因为此时 CPU 可能还没初始化好内存控制器,绝对地址访问可能出问题。用 b 指令最安全。

核心要点:复位向量是 CPU 上电后的第一条指令。U-Boot 必须把它放在正确的位置,否则系统连启动都做不到。

3.2 异常向量表:中断与异常的入口

复位向量只是异常向量表的第一个条目。完整的异常向量表包含 8 个入口,每个占用 4 字节(一条指令)。

偏移地址 异常类型 CPU 模式 优先级
0x00 复位 SVC 1(最高)
0x04 未定义指令 UND 6
0x08 软件中断(SWI) SVC 6
0x0C 预取中止 ABT 5
0x10 数据中止 ABT 4
0x14 保留 — —
0x18 IRQ(普通中断) IRQ 2
0x1C FIQ(快速中断) FIQ 3

U-Boot 在启动初期,其实并不依赖中断。但异常向量表必须存在——万一发生硬件异常(比如访问非法地址),CPU 会跳转到对应的向量入口。如果没有处理函数,系统就会死锁。

我曾经在一个项目中,因为异常向量表没对齐到 32 字节边界,导致 IRQ 中断触发后 PC 跑飞。查了两天才发现是链接脚本里 .align 5 写成了 .align 4。嗯,这种细节真的不能马虎。

3.3 CPU 模式切换:为什么要进 SVC 模式?

ARM 有 7 种处理器模式:

  • USR(用户模式):权限最低,不能访问系统寄存器
  • SVC(超级用户模式):权限高,可以访问所有资源
  • IRQ/FIQ(中断模式):硬件自动进入
  • ABT(中止模式):异常时进入
  • UND(未定义模式):遇到未定义指令时进入
  • SYS(系统模式):与 SVC 类似,但使用 USR 的寄存器组

U-Boot 启动时,CPU 默认处于 SVC 模式。为什么?因为复位异常本身就是在 SVC 模式下处理的。但有些 BootROM 或上一级引导程序可能会把 CPU 留在其他模式。所以 U-Boot 的启动代码会主动做一次模式切换,确保进入 SVC。

看这段代码:

reset:
    /* 设置当前 CPSR 为 SVC 模式,关闭 IRQ 和 FIQ */
    mrs     r0, cpsr
    bic     r0, r0, #0x1F          /* 清除模式位 */
    orr     r0, r0, #0xD3          /* 设置为 SVC 模式,关 IRQ/FIQ */
    msr     cpsr, r0

这里 0xD3 的含义是:

  • 位 [4:0] = 0b10011(SVC 模式)
  • 位 [7] = 1(关 IRQ)
  • 位 [6] = 1(关 FIQ)

小技巧:如果你在调试时发现 U-Boot 启动后中断响应异常,先检查 CPSR 里的模式位。我遇到过好几次,都是因为上一级代码把 CPU 留在了 IRQ 模式,导致栈指针不对。

3.4 整体流程:从复位向量到 SVC 模式

把上面几个知识点串起来,U-Boot 启动的第一阶段流程是这样的:

  1. CPU 上电,PC 指向复位向量地址
  2. 执行 b reset,跳转到真正的初始化代码
  3. 设置 CPSR,强制进入 SVC 模式,关闭所有中断
  4. 初始化栈指针(SP),为后续 C 函数调用做准备
  5. 清 BSS 段、设置异常向量表基址(如果支持中断)

这个流程看起来简单,但每一步都有坑。比如栈指针的设置,不同 ARM 架构的栈增长方向不同,SP 的初始值也不同。我建议你拿到一块新板子时,先看芯片手册里的内存映射图,确认 SRAM 的地址范围,再决定 SP 设在哪里。

注意:在设置 SP 之前,绝对不能调用任何 C 函数。因为 C 函数调用会压栈,没有栈指针,程序必崩。U-Boot 的 start.S 里,设置 SP 的代码一定在第一个 C 函数调用之前。

3.5 知识体系图

下面这张图展示了本章的核心逻辑:从 CPU 上电到进入 SVC 模式的完整路径。

U-Boot 启动第一阶段:复位向量 → SVC 模式 CPU 上电复位 跳转复位向量 (0x0000) 执行 b reset 指令 设置 CPSR → SVC 模式 初始化栈指针 (SP) 异常向量表结构 0x00: 复位 0x04: 未定义指令 0x08: SWI 0x0C-0x1C: 其他异常 CPSR 设置 0xD3 = SVC + 关中断 模式位 [4:0] = 10011

这张图把复位向量、异常向量表、模式切换串在了一起。你仔细看,从 CPU 上电到 SP 初始化,每一步都依赖上一步的正确执行。任何一个环节出错,U-Boot 都跑不起来。

3.6 避坑指南

最后分享几个我实际踩过的坑:

  • 向量表对齐问题:ARM 要求异常向量表必须 32 字节对齐。如果链接脚本里没加 .align 5,向量表可能错位。我曾经因为这个,调试了整整一个下午。
  • CPSR 模式位被意外修改:有些外设初始化代码会不小心修改 CPSR。我建议在每次模式切换后,加一个 mrs r0, cpsr 读回来检查一下。
  • SP 初始值不合理:如果 SP 指向了未初始化的内存区域,第一次函数调用就会崩溃。我习惯把 SP 设在 SRAM 的最高地址,这样栈向下增长时不容易溢出。

嗯,这一章的内容就到这里。复位向量和异常向量表是 U-Boot 启动的基石,理解透了,后面的代码分析才会顺手。