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。其他模式都有权限限制。
模式切换的核心操作:
- 读取当前CPSR:获取当前处理器状态
- 修改模式位:CPSR的低5位控制模式
- 写回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 初始化流程
- 关闭MMU和Cache:复位后默认是关闭的,但保险起见先关一次
- 初始化页表:在内存中建立页表结构
- 设置TTB(Translation Table Base):告诉MMU页表在哪
- 使能MMU:开启地址转换
- 使能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 整体启动流程图
下面这张图把整个启动流程串起来,方便你对照理解:
4.6 常见问题与调试技巧
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上电后无任何输出 | 复位向量地址不对或代码没烧录 | 用JTAG读PC寄存器,看是否停在0x00000000 |
| 中断不响应 | 向量表未对齐或VBAR设置错误 | 检查VBAR值,确认向量表在链接脚本中的位置 |
| 使能MMU后死机 | 页表映射错误或TTB地址不对 | 关闭MMU,单步调试页表初始化代码 |
| Cache使能后数据错乱 | 先开了Cache再开MMU | 检查初始化顺序,确保MMU先于Cache |
总结一下:ARM启动流程说白了就是“从硬到软、从简到繁”的过程。先搞定最基本的复位和异常处理,再逐步开启MMU和Cache这些高级功能。每一步都有严格的顺序要求,搞反了就是死机。
我个人调试时有个习惯:每完成一个步骤,就在串口打印一个字符。比如初始化栈后打印'A',关闭MMU后打印'B',使能MMU后打印'C'。这样死机时看一眼最后一个字符,就知道卡在哪一步了——这招帮我省了不少时间。