← 返回 计组与微机控制

计组与微机控制

9.3 异常响应和返回以及最终总结

9.3 异常响应和返回以及最终总结

异常进入在中断响应压栈的过程时:

Cortex-M 在异常进入时自动完成:

- 压栈 R0、R1、R2、R3;

- 压栈 R12;

- 压栈 LR;

- 压栈 PC;

- 压栈 xPSR;

- 更新 LR 为特殊的 `EXC_RETURN`;

- 更新 PC 为异常服务程序入口地址。

这就是 Cortex-M 中断写起来比传统裸机汇编简单的原因之一。

异常返回(中断返回)

异常返回由特殊值 `EXC_RETURN` 触发。

常见方式:BX LR

当 LR 中保存的是 `EXC_RETURN` 时,CPU 不把它当普通地址,而是执行异常返回流程。

返回过程:

- 根据 `EXC_RETURN` 判断使用 MSP 还是 PSP;

- 从栈中恢复寄存器;

- 恢复 PC;

- 恢复 xPSR;

- 回到线程模式或处理模式。(接下来是总结,可能字多看的眼睛疼,我还是特地打线分开了,分开理解就好了)

Cortex-M 中断机制流程总结

Cortex-M 的中断机制本质上是一套“中断源提出请求,NVIC 统一管理,CPU 根据向量表跳转到对应 ISR,并在返回时恢复原程序现场”的硬件控制流程。

它不是普通函数调用,而是由硬件、寄存器、向量表和优先级规则共同完成的异常响应机制。

整个过程可以从底层关系理解为:中断源负责产生请求,NVIC 负责管理请求和优先级,SCB 负责系统级控制和向量表定位,CPU 负责自动保存现场、查表跳转、执行 ISR、异常返回恢复现场。

首先,中断能够被正确响应的前提是中断向量表已经存在并被 CPU 正确定位。

中断向量表是一张“异常编号到中断服务程序入口地址”的表。Cortex-M 采用矢量中断方式,向量表中存放的通常不是跳转指令,而是 ISR 的入口地址。

向量表的第 0 项比较特殊,它不是中断函数入口,而是初始 MSP 主栈指针;

第 1 项是 Reset_Handler,后面依次是 NMI、HardFault 等系统异常入口。

对外部中断来说,外部中断 IRQn 对应异常编号 `IRQn + 16`,所以它在向量表中的地址为 `VTOR + (IRQn + 16) × 4`。

这里的 VTOR 是 SCB 中的向量表偏移寄存器,用来指定当前 CPU 应该从哪里查找向量表。

如果系统需要在运行时修改中断入口,例如 BootLoader 跳转应用程序、操作系统切换应用或动态修改 ISR,就可以把向量表复制到 RAM,再修改 `SCB->VTOR` 指向新的向量表基地址。

由于 CPU 查表时按固定宽度访问表项,所以 VTOR 的基地址必须满足对齐要求,对齐大小通常由“系统异常数 + 外部中断数”乘以 4 后向上扩展到 2 的整数次幂决定。

其次,中断源本身只是“产生请求”的一端,它可以来自外设,也可以来自软件。

外部中断可能来自定时器、GPIO、串口、SPI、I2C、ADC、DMA、EXTI 等外设;

系统异常可能来自 SysTick、SVCall、PendSV、HardFault 等;

软件也可以通过写入相关寄存器制造中断请求,例如设置 NVIC 的挂起位或通过软件触发外部中断。

中断源产生请求后,并不是 CPU 立即无条件响应,而是先进入 NVIC 的管理范围。

NVIC 即嵌套向量中断控制器,它负责管理外部中断的使能、挂起、清除和优先级,并支持中断嵌套和向量化响应。

可以把 NVIC 理解成 Cortex-M 中断系统的“调度核心”:它手里有很多中断请求,但要根据当前屏蔽状态、优先级和 CPU 当前正在执行的异常级别来判断谁能被响应。

中断能否进入 CPU,还会受到屏蔽寄存器和优先级规则控制。

PRIMASK、FAULTMASK、BASEPRI 是三个重要的中断屏蔽寄存器。

PRIMASK 用于快速屏蔽大多数可屏蔽异常,但不能屏蔽 NMI 和 HardFault;FAULTMASK 更强,置位后除 NMI 外几乎所有异常都被屏蔽,甚至 HardFault 也会被屏蔽;

BASEPRI 则是按优先级阈值屏蔽中断,它不会一刀切关闭所有中断,而是屏蔽某个优先级数值及更低优先级的中断,同时允许更高优先级中断继续响应。

这里要特别注意 Cortex-M 的优先级规则:优先级数值越小,实际优先级越高。

例如 `0x00` 高于 `0x20`,`0x20` 高于 `0x40`。Reset、NMI、HardFault 具有固定优先级,其中 Reset 最高,NMI 次之,HardFault 再次,它们高于普通可编程中断。

Cortex-M 的中断优先级还可以进一步分为抢占优先级和子优先级。

抢占优先级决定一个中断能不能打断另一个正在执行的中断,子优先级只在多个中断同时挂起且抢占优先级相同时决定谁先被响应。

也就是说,抢占优先级决定能不能嵌套,子优先级决定同组等待时谁先执行。

如果两个中断抢占优先级相同,即使其中一个子优先级更高,也不能打断另一个正在执行的中断。

优先级位如何划分为抢占优先级和子优先级,由 SCB 的 AIRCR 寄存器中的 PRIGROUP 字段决定。

由于很多 Cortex-M 芯片并没有实现完整 8 位优先级,而只实现高几位,例如 Cortex-M3/M4 常见只实现 bit7~bit5 三位,所以实际可用优先级通常是 `0x00、0x20、0x40、0x60、0x80、0xA0、0xC0、0xE0` 这类高位有效的编码。

PRIGROUP 的作用就是把这些有效位拆分成“抢占优先级位”和“子优先级位”。

完整的中断过程可以分成五个阶段:初始化、中断申请、中断判断与响应、中断服务、中断返回。

初始化阶段,程序需要配置外设中断触发条件,设置中断优先级和优先级分组,使能外设侧中断请求,并使能 NVIC 中对应的中断通道,必要时还要设置或重定位向量表。

中断申请阶段,中断源产生请求,例如定时器溢出、串口接收到数据、GPIO 检测到边沿,或者软件主动设置挂起位。随后 NVIC 会根据中断是否使能、是否处于挂起状态、是否被 PRIMASK/FAULTMASK/BASEPRI 屏蔽、优先级是否高于当前正在执行的异常或中断,来判断是否允许 CPU 响应。若满足条件,CPU 开始中断响应。

在中断响应阶段,Cortex-M 硬件会自动完成一部分现场保护。典型情况下,CPU 会自动把 `R0、R1、R2、R3、R12、LR、PC、xPSR` 压入当前栈中,这一步保证中断返回后原程序可以从被打断的位置继续执行。

随后 CPU 根据异常编号计算向量表表项地址,从向量表中取出对应 ISR 的入口地址,并跳转到该中断服务程序。

这个过程可以概括为:保存现场 → 查向量表 → 跳转 ISR。中断服务阶段执行对应的中断服务函数,例如定时器中断处理函数、串口接收中断处理函数等。

ISR 内部通常要完成两件事:处理事件和清除中断标志位。如果只处理事件而不清除外设中断标志,那么 ISR 返回后 NVIC 可能会发现该中断仍处于请求状态,从而立刻再次进入中断,导致程序看起来像“中断退不出来”。

中断返回阶段由 Cortex-M 的异常返回机制完成。ISR 执行结束后,CPU 根据特殊的异常返回值,从栈中恢复之前自动压入的寄存器,包括 `R0、R1、R2、R3、R12、LR、PC、xPSR`,然后回到被中断的程序继续执行。

由于栈天然具有后进先出的结构,Cortex-M 可以比较自然地支持中断嵌套:当低优先级 ISR 正在执行时,如果更高抢占优先级的中断到来,CPU 会再次压栈保存当前 ISR 的现场,转去执行更高优先级 ISR;高优先级 ISR 返回后,再恢复低优先级 ISR 的现场继续执行;低优先级 ISR 最终结束后,再恢复原来的主程序现场。也就是说,中断嵌套的关键不在子优先级,而在抢占优先级是否更高。

整体来看,Cortex-M 中断系统可以理解成一条完整链路:

外设或软件产生中断请求,NVIC 根据使能、挂起、屏蔽和优先级规则决定是否响应,CPU 自动保存现场并根据 VTOR 指向的中断向量表找到 ISR 入口,ISR 处理事件并清除标志,最后 CPU 通过异常返回机制恢复现场并回到原程序。

向量表解决“发生中断后去哪里执行”

NVIC 解决“多个中断谁能进来、谁先执行、谁能抢占谁”,PRIMASK/FAULTMASK/BASEPRI 解决“哪些中断暂时不允许进入”,AIRCR.PRIGROUP 解决“优先级位如何划分为抢占优先级和子优先级”

硬件自动压栈和异常返回则解决“被中断的程序如何恢复”。

这一整套机制使得 Cortex-M 能够在不依赖复杂操作系统的情况下,高效、确定地响应外设事件,并支持实时系统中常见的中断优先级和嵌套处理。

中断系统主线:

中断源 -> 中断请求 -> NVIC 判断 -> 向量表取入口 -> 自动压栈 -> ISR -> 清标志 -> 异常返回

和 STM32 EXTI 对应:

GPIO 边沿 -> EXTI 挂起 -> NVIC 使能 -> EXTIx_IRQHandler -> 清除 EXTI_PR