4. 启动流程分析:ARM上电复位向量、异常向量表、SVC模式切换、MMU与Cache初始化

ARM芯片上电后,到底先执行哪条指令?

这个问题,我当年刚入行时也困惑了很久。看着几百页的ARM手册,感觉每个字都认识,但串起来就是不知道CPU在干嘛。后来亲手调了几块板子,踩了几个坑,才算真正搞明白。

今天咱们就把这启动流程掰开揉碎,从复位向量一路走到MMU初始化。

4.1 上电复位向量:CPU的第一口饭

ARM上电后,CPU会去一个固定地址取第一条指令。这个地址就是复位向量。

ARM架构 复位向量地址 说明
ARMv7-A(Cortex-A系列) 0x00000000 或 0xFFFF0000 由异常向量基址寄存器(VBAR)决定
ARMv8-A(64位) 0x00000000 EL3复位后固定地址
传统ARM7/9 0x00000000 固定,不可更改

关键点:复位向量处放的通常是一条跳转指令,而不是用户代码本身。比如:

; 典型的复位向量写法
    b   reset_handler      ; 跳转到复位处理函数
    b   undef_handler      ; 未定义指令异常
    b   svc_handler        ; SVC异常
    b   pabt_handler       ; 预取中止
    b   dabt_handler       ; 数据中止
    b   .                  ; 保留
    b   irq_handler        ; IRQ中断
    b   fiq_handler        ; FIQ中断

为什么不用直接执行?因为向量表每个入口只有4字节(一条指令的空间),放不下完整的处理逻辑。所以放一条跳转指令,跳到别处去执行。

我的习惯:在复位向量处用 b reset_handler 而不是 ldr pc, =reset_handler。前者是位置无关的,后者会生成绝对地址。如果代码还没重定位,绝对地址跳转就出事了——我吃过这个亏。

4.2 异常向量表:CPU的紧急呼叫系统

异常向量表,说白了就是一张“紧急情况处理电话本”。CPU遇到各种异常时,会自动跳到这里查表。

ARM的异常向量表长这样:

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

注意:向量表必须放在4字节对齐的地址上。如果对齐不对,CPU取指令时会出错。我曾经在移植U-Boot时,因为链接脚本没对齐,导致中断死活不响应——查了两天才发现是向量表没对齐。

ARMv7-A架构引入了VBAR(Vector Base Address Register),可以动态设置向量表基址。这意味着你可以把向量表放在任意地址,不用死磕0x00000000了。

; 设置VBAR的例子
ldr r0, =_vector_start    ; 向量表起始地址
mcr p15, 0, r0, c12, c0, 0  ; 写入VBAR

4.3 SVC模式切换:从特权到更特权

ARM有7种处理器模式,其中SVC模式(Supervisor模式)是操作系统内核运行的模式。Bootloader启动时,需要从复位后的SVC模式切换到其他模式,或者确保自己始终在SVC模式下工作。

为什么非要SVC模式?因为只有SVC模式才能访问所有系统资源,才能配置MMU和Cache。其他模式都有权限限制。

模式切换的核心操作:

  1. 读取当前CPSR:获取当前处理器状态
  2. 修改模式位:CPSR的低5位控制模式
  3. 写回CPSR:完成切换
; 切换到SVC模式的典型代码
mrs r0, cpsr          ; 读取当前状态
bic r0, r0, #0x1F     ; 清除模式位
orr r0, r0, #0x13     ; 设置为SVC模式(0b10011)
msr cpsr, r0          ; 写回生效

避坑指南:切换模式时,记得先设置好对应模式的栈指针(SP)。每个模式都有自己的SP,如果不设置,一旦进入异常处理,栈操作就会破坏内存。我曾经在调试时发现函数返回地址被莫名其妙地覆盖了,最后发现是IRQ模式的SP指向了未初始化区域。

各模式对应的SP设置:

; 为各模式设置栈指针
ldr sp, =svc_stack_top    ; SVC模式栈
cps #0x12                 ; 切换到IRQ模式
ldr sp, =irq_stack_top    ; IRQ模式栈
cps #0x13                 ; 切回SVC模式

4.4 MMU与Cache初始化:让CPU跑得更快更稳

MMU(内存管理单元)和Cache是Bootloader启动中后期的重要工作。没有MMU,CPU只能直接访问物理地址;没有Cache,CPU每次读写内存都要等慢速的DRAM。

但初始化顺序有讲究——搞反了就会死机。

4.4.1 初始化流程

  1. 关闭MMU和Cache:复位后默认是关闭的,但保险起见先关一次
  2. 初始化页表:在内存中建立页表结构
  3. 设置TTB(Translation Table Base):告诉MMU页表在哪
  4. 使能MMU:开启地址转换
  5. 使能Cache:开启数据/指令缓存

我曾经犯过的错:先开了Cache再开MMU。结果CPU用虚拟地址去查Cache,Cache里存的却是物理地址的缓存行——直接数据不一致,系统跑几步就崩了。正确的顺序一定是:先MMU,再Cache。

4.4.2 页表配置示例

ARMv7-A使用两级页表。一级页表每个条目对应1MB空间,二级页表每个条目对应4KB空间。Bootloader通常只用一级页表做简单映射。

; 一级页表初始化(简化版)
; 假设物理地址0x00000000映射到虚拟地址0x00000000
; 属性:可读可写、Cache使能、Buffer使能

ldr r0, =page_table_base    ; 页表基址
mov r1, #0x0000             ; 虚拟地址起始
mov r2, #0x1000             ; 映射大小(4GB)

loop:
    ; 计算页表条目
    ; 每个条目对应1MB空间
    ; 条目格式:物理地址[31:20] | 属性位
    mov r3, r1, lsr #20     ; 取虚拟地址高12位
    orr r3, r3, #0xC0E      ; 设置属性:可读可写、Cache、Buffer
    str r3, [r0, r1, lsr #18] ; 写入页表
    add r1, r1, #0x100000   ; 下一个1MB
    cmp r1, r2
    bne loop

4.4.3 使能MMU的关键步骤

; 使能MMU
; 1. 设置TTB(Translation Table Base)
ldr r0, =page_table_base
mcr p15, 0, r0, c2, c0, 0   ; 写入TTBR0

; 2. 设置域访问控制(Domain Access Control)
mov r0, #0xFFFFFFFF
mcr p15, 0, r0, c3, c0, 0   ; 所有域设为Manager模式

; 3. 使能MMU和Cache
mrc p15, 0, r0, c1, c0, 0   ; 读取SCTLR
orr r0, r0, #0x1             ; 使能MMU(bit0)
orr r0, r0, #0x1000          ; 使能ICache(bit12)
orr r0, r0, #0x4             ; 使能DCache(bit2)
mcr p15, 0, r0, c1, c0, 0   ; 写回SCTLR

; 4. 同步屏障(确保MMU生效)
dsb
isb

我的建议:使能MMU后,立即执行一条跳转指令,刷新流水线。因为使能MMU前CPU已经预取了指令,这些指令用的是旧地址空间。跳转一下,强制CPU重新取指,避免指令混乱。

; 使能MMU后立即跳转
ldr r0, =mmu_enabled
bx r0

mmu_enabled:
    ; 现在可以放心使用虚拟地址了

4.5 整体启动流程图

下面这张图把整个启动流程串起来,方便你对照理解:

ARM Bootloader启动流程图 ARM Bootloader 启动流程 上电复位 读取复位向量 (0x00000000) 跳转到 reset_handler 初始化各模式栈指针 关闭 MMU 和 Cache 初始化页表并设置TTB 使能 MMU 和 Cache 注意点 • 向量表必须4字节对齐 • 先MMU后Cache • 使能后执行跳转刷新流水线

4.6 常见问题与调试技巧

问题现象 可能原因 排查方法
上电后无任何输出 复位向量地址不对或代码没烧录 用JTAG读PC寄存器,看是否停在0x00000000
中断不响应 向量表未对齐或VBAR设置错误 检查VBAR值,确认向量表在链接脚本中的位置
使能MMU后死机 页表映射错误或TTB地址不对 关闭MMU,单步调试页表初始化代码
Cache使能后数据错乱 先开了Cache再开MMU 检查初始化顺序,确保MMU先于Cache

总结一下:ARM启动流程说白了就是“从硬到软、从简到繁”的过程。先搞定最基本的复位和异常处理,再逐步开启MMU和Cache这些高级功能。每一步都有严格的顺序要求,搞反了就是死机。

我个人调试时有个习惯:每完成一个步骤,就在串口打印一个字符。比如初始化栈后打印'A',关闭MMU后打印'B',使能MMU后打印'C'。这样死机时看一眼最后一个字符,就知道卡在哪一步了——这招帮我省了不少时间。