第1章 ARM Cortex-A系列启动详解:异常向量表、启动模式与SVC模式切换

各位同学,咱们今天正式开讲。做游戏机Bootloader,ARM Cortex-A系列是绕不开的坎。我这些年拆过的游戏机,从PSP到Switch,核心清一色都是ARM架构。说白了,搞懂Cortex-A的启动流程,你就能看懂八成游戏机的底层运作。

这一章,咱们聚焦四个核心点:异常向量表、启动模式、SVC模式切换,还有MMU初始化前的准备工作。嗯,内容有点干,但都是硬通货。

1.1 异常向量表——系统的“紧急电话本”

异常向量表是什么?我习惯把它比作一张“紧急电话本”。CPU遇到异常,比如按了电源键、触发了中断,它会立刻查这个表,找到对应的处理函数。

这张表固定在地址 0x000000000xFFFF0000(由SCTLR.V位控制)。每个表项占4字节,一共8个异常。

偏移地址 异常类型 触发场景
0x00 复位 (Reset) 上电或按复位键
0x04 未定义指令 (Undef) CPU不认识这条指令
0x08 软件中断 (SWI) 执行SVC指令
0x0C 预取中止 (Prefetch Abort) 取指令时内存出错
0x10 数据中止 (Data Abort) 读写数据时内存出错
0x14 保留 (Reserved) 别用,留给未来
0x18 IRQ (普通中断) 外设请求CPU处理
0x1C FIQ (快速中断) 高速外设请求,优先级最高

核心要点:复位异常是Bootloader的入口。CPU上电后,第一条指令就是从0x00000000处取出来的。所以,你的Bootloader代码必须从这儿开始。

我在项目中遇到过一个问题:某款国产游戏机,启动时偶尔会卡死。查了半天,发现是异常向量表被意外覆盖了。嗯,从那以后,我都在向量表前面加一段保护代码,防止被乱写。

1.2 启动模式——FIQ与IRQ的选择

Cortex-A支持两种中断模式:IRQ和FIQ。你想想看,为什么要有两个?

说白了,FIQ是VIP通道。它有自己的专用寄存器(R8-R14),不用像IRQ那样压栈保存。所以FIQ响应更快,适合处理高速数据流,比如游戏机的DMA传输。

但FIQ也有代价——它只能挂一个中断源。IRQ虽然慢一点,但可以挂多个。我个人的习惯是:

  • FIQ:给最高优先级的外设,比如视频刷新中断
  • IRQ:给普通外设,比如按键、音频

小技巧:在Bootloader阶段,我建议先关闭所有中断。等MMU和栈都初始化好了,再按需开启。否则,中断来了你没栈可用,直接崩掉。

关闭中断的代码很简单:

// 关闭IRQ和FIQ
MRS R0, CPSR
ORR R0, R0, #0xC0    // 设置I位和F位
MSR CPSR_c, R0

1.3 SVC模式切换——Bootloader的“特权模式”

ARM有七种运行模式,但Bootloader最常用的是SVC(超级用户模式)。为什么?因为SVC有最高权限,能访问所有系统资源。

CPU上电后,默认进入SVC模式。但如果你不小心切到了其他模式,比如用户模式(USR),那就麻烦了——很多关键寄存器都访问不了。

我曾经犯过这个错:写了一个Bootloader,调试时发现MMU死活配不上。查了两天,才发现代码跑在用户模式下。唉,那两天真是白费了。

切换到SVC模式的方法:

// 切换到SVC模式
MRS R0, CPSR
BIC R0, R0, #0x1F    // 清除模式位
ORR R0, R0, #0x13    // 设置为SVC模式
MSR CPSR_c, R0

警告:切换模式时,一定要确保栈指针(SP)已经设置好。SVC模式有自己的栈,如果没设,函数调用会直接崩溃。我见过有人在这上面栽跟头,整块板子变砖。

1.4 MMU初始化前的准备——别急着开MMU

很多新手一上来就开MMU,结果系统直接挂掉。为什么?因为MMU开启后,CPU访问的都是虚拟地址。如果你的页表没配好,CPU根本找不到代码在哪儿。

我个人建议,MMU初始化前,先做好三件事:

  1. 关掉所有中断——防止中断处理时地址错乱
  2. 设置好栈指针——每个模式都要有独立的栈
  3. 清空TLB和Cache——避免旧数据干扰新页表

清空TLB的代码示例:

// 清空TLB
MOV R0, #0
MCR p15, 0, R0, c8, c7, 0   // 清空TLB
DSB                          // 确保操作完成
ISB                          // 刷新指令流水线

嗯,这里要注意:DSB和ISB这两个指令,很多人会漏掉。DSB保证前面的操作都完成了,ISB保证后面的指令重新读取。少了任何一个,都可能出现诡异的问题。

避坑指南:我曾经在MMU初始化时,忘了关Cache。结果页表被Cache缓存了,修改后没生效。系统跑着跑着就跳转到错误地址。从那以后,我每次初始化MMU,都先关Cache,再操作页表,最后开MMU。顺序一步都不能错。

1.5 实战:一个最小Bootloader的启动流程

说了这么多,咱们来点实际的。一个最小Bootloader的启动流程,大概是这样:

// 伪代码,展示启动流程
_start:
    // 1. 设置异常向量表
    LDR PC, Reset_Handler
    LDR PC, Undef_Handler
    LDR PC, SWI_Handler
    // ... 其他异常向量

Reset_Handler:
    // 2. 关闭中断
    MRS R0, CPSR
    ORR R0, R0, #0xC0
    MSR CPSR_c, R0

    // 3. 设置SVC模式栈
    LDR SP, =SVC_STACK_TOP

    // 4. 初始化关键硬件(时钟、内存控制器等)
    BL SystemInit

    // 5. 准备页表(但先不开MMU)
    BL PageTableInit

    // 6. 开启MMU
    BL MMUEnable

    // 7. 跳转到主程序
    BL main

你看,流程很清晰。每一步都有它的道理。我刚开始做Bootloader时,总觉得这些步骤多余。后来被坑了几次,才明白——每一步都是在给后面的代码铺路。

个人经验:调试Bootloader时,我习惯在每个关键步骤后加一个LED闪烁。比如,初始化完内存,LED闪一下;配好页表,再闪一下。这样,系统卡在哪儿,一眼就能看出来。比用调试器快多了。

小结

这一章的内容,说白了就是Bootloader的“起手式”。异常向量表是入口,启动模式决定权限,SVC模式是工作环境,MMU准备是基础。把这四点吃透了,后面的内容就好办了。

下一章,咱们聊聊具体的硬件初始化——时钟、内存、串口。这些东西配不好,你的Bootloader连个“Hello World”都打印不出来。嗯,到时候见。