简述 #
异常包括:
- 系统异常:由内核、内核外设(如 SysTick 、MPU 等)触发。
- 外部中断:由设备外设(如 GPIO 、USART 等)触发。
异常编号 #
- 异常编号:硬件层面的编号体系。
- CMSIS-Core 枚举值:软件层面的编号体系,使用负数区分系统异常与外部中断,能提高部分 API 函数的效率(设置优先级)。
| 异常编号 | 异常类型 | CMSIS-Core 枚举 | CMSIS-Core 枚举值 | 异常处理名 | 优先级 |
|---|---|---|---|---|---|
| 1 | 复位 | – | – | Reset_Handler | -3(最高) |
| 2 | 不可屏蔽中断 | NonMaskableInt_IRQn | -14 | NMI_Handler | -2 |
| 3 | 硬件故障 | MemoryManagement_IRQn | -13 | HardFault_Handler | -1 |
| 4 | 内存管理故障 | BusFault_IRQn | -12 | MemManage_Handler | 可编程 |
| 5 | 总线故障 | BusFault_IRQn | -11 | BusFault_Handler | 可编程 |
| 6 | 使用故障 | UsageFault_IRQn | -10 | UsageFault_Handler | 可编程 |
| 7 ~ 10 | – | – | – | – | – |
| 11 | 系统服务调用 | SVCall_IRQn | -5 | SVC_Handler | 可编程 |
| 12 | 调试监控 | DebugMonitor_IRQn | -4 | DebugMon_Handler | 可编程 |
| 13 | – | – | – | – | – |
| 14 | 可挂起的服务调用 | PendSV_IRQn | -2 | PendSV_Handler | 可编程 |
| 15 | 系统节拍定时器 | SysTick_IRQn | -1 | SysTick_Handler | 可编程 |
| 16 ~ 255 | 外部中断 #0 ~ #239 | 设备定义 | 0 ~ 239 | 设备定义 | 可编程 |
向量表偏移寄存器 #
- 复位值为 0 。
- TBLOFF:向量表基地址偏移。
- TBLBASE:
0表示向量表基于 CODE 区,1表示向量表基于 SRAM 区。
向量表 #
- 向量地址 = 向量表偏移地址 + 异常编号 × 4 。
- 向量的最低位必须置 1 以表示 Thumb 状态。
CONTROL 寄存器 #
| FPCA | SPSEL | nPRIV | |
|---|---|---|---|
0 | 线程模式下使用主栈指针 MSP | 特权模式 | |
1 | 异常产生时浮点单元正在使用 | 线程模式下使用进程栈指针 PSP | 非特权模式 |
- 复位值为 0 。
- 特权模式:可访问 NVIC 、SCB 等寄存器,主要用于 RTOS 的安全隔离。
- 线程模式:上电复位后默认进入。
- 处理模式:发生异常或中断时硬件自动切换,处理模式下 SPSEL 始终为 0 。
AAPCS #
ARM 架构过程调用标准(ARM Architecture Procedure Call Standard)。
| 寄存器 | 别名 | 作用 |
|---|---|---|
| R0 | a1 | 参数1 / 返回值 |
| R1 | a2 | 参数2 / 返回值(64 位结果时与 R0 组合) |
| R2 | a3 | 参数3 |
| R3 | a4 | 参数4 |
| R4 ~ R11 | v1 ~ v8 | 变量寄存器 |
| R12 | IP | 内部调用临时寄存器 |
| R13 | SP | 堆栈指针 |
| R14 | LR | 链接寄存器 |
| R15 | PC | 程序计数器 |
| … | … | (此表仅截取了部分内容) |
调用者保存寄存器:R0 ~ R3 、 R12 、LR 、xPSR
- 若调用者在函数返回后还需要使用这些寄存器中的值,编译器会按需生成指令保存这些寄存器,返回地址(PC)由函数跳转指令自动保存到 LR 。
- 异常处理函数不由用户程序调用,所以异常发生时由硬件自动将 xPSR 、返回地址(PC)、LR 、R12 、R3 、R2 、R1 、R0 共 8 个寄存器压栈。
被调用者保存寄存器:R4 ~ R11
- 被调用的函数需要确保这些寄存器在进入与退出函数时保持一致。
- 如果 C 函数逻辑复杂,仅用 R0 ~ R3 无法满足需求,编译器会生成指令将需要用到的 R4 ~ R11 中的寄存器保存。
由于 CM3 支持 16 位和 32 位的混合指令(以下两种情况 PC 值不一定相等)
- 硬件自动压栈的 PC 值 = 下一条待执行指令的地址(返回地址)。
- 软件指令读取的 PC 值 = 当前正在执行指令的地址 + 4 字节(流水线效应)。
双字栈对齐 #
双字栈对齐特性由 SCB 中的 CCR 寄存器的第 9 位 STKALIGN 控制。
- Cortex-M3 r0p0 中不可用。
- Cortex-M3 r1p0 和 r1p1 中默认禁止。
- Cortex-M3 r2p0 及之后的版本默认使能。
- Cortex-M4 中默认使能。
AAPCS 中要求函数栈帧的起始地址应是双字(8 字节)对齐的。若异常发生时栈帧未对齐到双字地址上,CM3/4 处理器会自动插入一个字即 SP = SP - 4,然后将压栈的 xPSR 值的第 9 位设置为 1 (表示 SP 调整过)。异常返回时,若栈帧中的 xPSR(bit9=1), 则出栈后 SP = SP + 4 。
xPSR 寄存器的中的第 9 位不会被修改,修改的是栈帧中的 xPSR 值的第 9 位。
| 普通函数调用 | 编译器通过软件指令调整 SP | 编译器知道调用处的上下文,可以预判对齐需求与需要保存的寄存器 |
| 异常/中断 | 硬件自动双字(8字节)对齐 | 异常随时可能发生,SP 状态不确定,必须硬件对齐后保存寄存器 |
EXC_RETURN #
EXC_RETURN 是一个特殊值,处理器在进入异常处理函数后,会设置 LR = EXC_RETURN 。当异常函数返回时会将 LR 加载到 PC ,硬件检测到 PC = EXC_RETURN 后会触发异常返回机制。
| 栈帧类型 | 返回模式 | 返回栈 | |
|---|---|---|---|
0 | 26 字 | 处理模式 | MSP |
1 | 8 字 | 线程模式 | PSP |
C 函数调用流程 #
其汇编的流程大致如下:
.global _start
_start:
MOV R0, #3 /* 参数 1 */
MOV R1, #5 /* 参数 2 */
LDR R4, =0x1234 /* 测试 R4-R6 是否会被破坏 */
LDR R5, =0x5678 /* 测试 R4-R6 是否会被破坏 */
LDR R6, =0x9ABC /* 测试 R4-R6 是否会被破坏 */
BL add /* 设置 LR = 返回地址并跳转到 add */
LDR R3, =0x8888 /* LR = 此处地址:返回后从这里继续执行 */
add:
PUSH {R4-R6,LR} /* 保存 R4-R6 和返回地址 LR */
MOV R4, R0
MOV R5, R1
ADD R6, R4, R5
MOV R0, R6 /* R0 保存返回值 */
POP {R4-R6,PC} /* 恢复 R4-R6,将 LR 的值弹入 PC 寄存器 */
对于 PUSH 指令,编号高的寄存器总是先存储;对于 POP 指令,编号低的寄存器总是先加载。
- 寄存器保存使用 PUSH 或 STM 等指令,取决于编译器。
- 要保存哪些寄存器?编译器会按 AAPCS 保存必要的寄存器并生成相应的汇编指令(并不是所有的函数都需保存)。
- 寄存器恢复使用 POP 或 LDM 等指令,取决于编译器。
- 函数如何返回?只要将返回地址(LR)加载到 PC 寄存器中即可,使用 POP 或 BX 等指令,取决于编译器。
异常处理函数调用流程 #
总结 #
异常处理相当于给标准 C 函数套了一层 “硬件封装” ,异常处理函数的进入与返回都由硬件自动完成。 标准 C 函数的进入与返回则是由编译器按照 AAPCS 约定生成的汇编指令完成。