CM3 异常处理

简述 #

异常包括:

  • 系统异常:由内核、内核外设(如 SysTick 、MPU 等)触发。
  • 外部中断:由设备外设(如 GPIO 、USART 等)触发。

异常编号 #

  • 异常编号:硬件层面的编号体系。
  • CMSIS-Core 枚举值:软件层面的编号体系,使用负数区分系统异常与外部中断,能提高部分 API 函数的效率(设置优先级)。
异常编号异常类型CMSIS-Core 枚举CMSIS-Core 枚举值异常处理名优先级
1复位Reset_Handler-3(最高)
2不可屏蔽中断NonMaskableInt_IRQn-14NMI_Handler-2
3硬件故障MemoryManagement_IRQn-13HardFault_Handler-1
4内存管理故障BusFault_IRQn-12MemManage_Handler可编程
5总线故障BusFault_IRQn-11BusFault_Handler可编程
6使用故障UsageFault_IRQn-10UsageFault_Handler可编程
7 ~ 10
11系统服务调用SVCall_IRQn-5SVC_Handler可编程
12调试监控DebugMonitor_IRQn-4DebugMon_Handler可编程
13
14可挂起的服务调用PendSV_IRQn-2PendSV_Handler可编程
15系统节拍定时器SysTick_IRQn-1SysTick_Handler可编程
16 ~ 255外部中断 #0 ~ #239设备定义0 ~ 239设备定义可编程

向量表偏移寄存器 #

位 31:30
位 31:30
位 29
位 29
位 28:7
位 28:7
位 6:0
位 6:0
向量表偏移寄存器 VTOR ,地址 0xE000ED08
向量表偏移寄存器 VTOR ,地址 0xE000ED08
TBLOFF
TBLOFF
--
--
TBLOFF
TBLOFF
--
--
TBLBASE
TBLBASE
--
--
CM3 r2p1
CM3 r2p1
CM3 r0p0 - r2p0
CM3 r0p0 - r2p0
SRAM → 0x20000000
SRAM → 0x20000000
0
0
1
1
00
00
  • 复位值为 0 。
  • TBLOFF:向量表基地址偏移。
  • TBLBASE:0 表示向量表基于 CODE 区,1 表示向量表基于 SRAM 区。

向量表 #

  • 向量地址 = 向量表偏移地址 + 异常编号 × 4 。
  • 向量的最低位必须置 1 以表示 Thumb 状态。
MemManage 向量
MemManage 向量
BusFault 向量
BusFault 向量
UsageFault 向量
UsageFault 向量
--
--
--
--
--
--
--
--
SVC 向量
SVC 向量
调试监控向量
调试监控向量
--
--
PendSV 向量
PendSV 向量
SysTick 向量
SysTick 向量
外部中断 #0 向量
外部中断 #0 向量
外部中断 #1 向量
外部中断 #1 向量
外部中断 #2 向量
外部中断 #2 向量
外部中断 #3 向量
外部中断 #3 向量
0
0
1
1
2
2
3
3
4
4
5
5
6
6
7
7
8
8
9
9
10
10
11
11
12
12
13
13
14
14
15
15
16
16
17
17
18
18
19
19
异常编号
异常编号
0x00000000
0x00000000
0x00000004
0x00000004
0x00000008
0x00000008
0x0000000C
0x0000000C
0x00000010
0x00000010
0x00000014
0x00000014
0x00000018
0x00000018
0x0000001C
0x0000001C
0x00000020
0x00000020
0x00000024
0x00000024
0x00000028
0x00000028
0x0000002C
0x0000002C
0x00000030
0x00000030
0x00000034
0x00000034
0x00000038
0x00000038
0x0000003C
0x0000003C
0x00000040
0x00000040
0x00000044
0x00000044
0x00000048
0x00000048
0x0000004C
0x0000004C
存储器地址
存储器地址
MSP 初始值
MSP 初始值
复位向量
复位向量
NMI 向量
NMI 向量
HardFault 向量
HardFault 向量

CONTROL 寄存器 #

--
--
SPSEL
SPSEL
nPRIV
nPRIV
位 31:3
位 31:3
位 1
位 1
位 0
位 0
FPCA
FPCA
位 2
位 2
FPCASPSELnPRIV
0线程模式下使用主栈指针 MSP特权模式
1异常产生时浮点单元正在使用线程模式下使用进程栈指针 PSP非特权模式
  • 复位值为 0 。
  • 特权模式:可访问 NVIC 、SCB 等寄存器,主要用于 RTOS 的安全隔离。
  • 线程模式:上电复位后默认进入。
  • 处理模式:发生异常或中断时硬件自动切换,处理模式下 SPSEL 始终为 0 。

AAPCS #

ARM 架构过程调用标准(ARM Architecture Procedure Call Standard)。

寄存器别名作用
R0a1参数1 / 返回值
R1a2参数2 / 返回值(64 位结果时与 R0 组合)
R2a3参数3
R3a4参数4
R4 ~ R11v1 ~ v8变量寄存器
R12IP内部调用临时寄存器
R13SP堆栈指针
R14LR链接寄存器
R15PC程序计数器
(此表仅截取了部分内容)

调用者保存寄存器: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 (Bit9=1)
xPSR (Bit9=1)
插入的空位
插入的空位
已使用的栈空间
已使用的栈空间
0x2000FFF4
0x2000FFF4
0x2000FFF0
0x2000FFF0
高地址
高地址
返回地址
返回地址
LR
LR
R12
R12
R3
R3
R2
R2
R1
R1
R0
R0
低地址
低地址
8 字节对齐,地址末尾为 0 或 8
8 字节对齐,地址末尾为 0 或 8
栈帧
栈帧
xPSR 寄存器的中的第 9 位不会被修改,修改的是栈帧中的 xPSR 值的第 9 位。
普通函数调用编译器通过软件指令调整 SP编译器知道调用处的上下文,可以预判对齐需求与需要保存的寄存器
异常/中断硬件自动双字(8字节)对齐异常随时可能发生,SP 状态不确定,必须硬件对齐后保存寄存器

EXC_RETURN #

EXC_RETURN 是一个特殊值,处理器在进入异常处理函数后,会设置 LR = EXC_RETURN 。当异常函数返回时会将 LR 加载到 PC ,硬件检测到 PC = EXC_RETURN 后会触发异常返回机制。

全为 1
全为 1
返回模式
返回模式
返回栈
返回栈
位 31:5
位 31:5
位 3
位 3
位 2
位 2
栈帧类型
栈帧类型
位 4
位 4
0
0
1
1
位 1
位 1
位 0
位 0
栈帧类型返回模式返回栈
026 字处理模式MSP
18 字线程模式PSP

C 函数调用流程 #

进入函数
进入函数
函数处理
函数处理
恢复寄存器
恢复寄存器
保存寄存器
保存寄存器
加载 LR 到 PC
加载 LR 到 PC
函数返回
函数返回
BL 指令:设置 LR = 返回地址,PC = 函数地址
BL 指令:设置 LR = 返回地址,PC = 函数地址

其汇编的流程大致如下:

.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 等指令,取决于编译器。

异常处理函数调用流程 #

进入函数
进入函数
函数处理
函数处理
恢复寄存器
恢复寄存器
保存寄存器
保存寄存器
加载 LR 到 PC
加载 LR 到 PC
函数返回
函数返回
将 xPSR 、返回地址(PC)、LR 、R12 、R3 、R2 、R1 、R0 共八个寄存器压栈
将 xPSR 、返回地址(PC)、LR 、R12 、R3 、R2 、R1 、R0 共八个寄存器压栈
设置 LR = EXC_RETURN
设置 LR = EXC_RETURN
设置 PC = 异常向量(向量地址 = 向量表偏移地址 + 异常编号 × 4)
设置 PC = 异常向量(向量地址 = 向量表偏移地址 + 异常编号 × 4)
硬件检测到 PC = EXC_RETURN 触发异常返回机制
硬件检测到 PC = EXC_RETURN 触发异常返回机制
异常产生
异常产生
内核接收并开始处理
内核接收并开始处理
异常处理函数
异常处理函数
NVIC
NVIC
处理模式,强制使用主栈 MSP
处理模式,强制使用主栈 MSP
若 SP 未调整,则压栈的 xPSR[9]=0
若 SP 未调整,则压栈的 xPSR[9]=0
栈已双字对齐,则 SP 不变
栈已双字对齐,则 SP 不变
若 CONTROL[1]=0,使用 MSP
若 CONTROL[1]=0,使用 MSP
若 EXC_RETURN[2]=0 则使用 MSP,CONTROL[1]=0
若 EXC_RETURN[2]=0 则使用 MSP,CONTROL[1]=0
若出栈的 xPSR[9]=0 则 SP 不变
若出栈的 xPSR[9]=0 则 SP 不变
将 xPSR 、返回地址(PC)、LR 、R12 、R3 、R2 、R1 、R0 出栈
将 xPSR 、返回地址(PC)、LR 、R12 、R3 、R2 、R1 、R0 出栈
PC = 出栈的返回地址
PC = 出栈的返回地址
若 CONTROL[1]=1,使用 PSP
若 CONTROL[1]=1,使用 PSP
栈未双字对齐,则 SP = SP - 4
栈未双字对齐,则 SP = SP - 4
若 SP 已调整,则压栈的 xPSR[9]=1
若 SP 已调整,则压栈的 xPSR[9]=1
若 EXC_RETURN[2]=1 则使用 PSP,CONTROL[1]=1
若 EXC_RETURN[2]=1 则使用 PSP,CONTROL[1]=1
若出栈的 xPSR[9]=1 则 SP = SP + 4
若出栈的 xPSR[9]=1 则 SP = SP + 4

总结 #

异常处理相当于给标准 C 函数套了一层 “硬件封装” ,异常处理函数的进入与返回都由硬件自动完成。 标准 C 函数的进入与返回则是由编译器按照 AAPCS 约定生成的汇编指令完成。