4. 异常与中断系统:异常类型、中断优先级(NVIC)、中断向量表、中断嵌套与抢占

好,咱们今天聊聊 Cortex-M 内核里最核心、也最让人头疼的一块——异常与中断系统。

说实话,我刚开始接触 ARM 的时候,也被这堆概念绕得晕头转向。什么异常、中断、优先级、抢占、嵌套……感觉像一团乱麻。但后来我悟了,其实你只要抓住一条主线:CPU 正在干活,突然有急事要处理,怎么办?

这个“急事”,就是异常或中断。而 Cortex-M 设计了一套非常精巧的机制来管理它们。咱们一个一个拆开看。

4.1 异常类型:不只是中断

很多人以为“异常”就是“中断”,其实不完全对。在 Cortex-M 里,异常是一个更大的概念,中断只是其中一种。

我习惯把异常分成两类:

  • 系统异常:由内核内部产生,比如 HardFault、SysTick、SVCall 等。这些是 CPU 自己“闹情绪”或者“需要帮忙”时触发的。
  • 外部中断:由外设(比如 GPIO、定时器、UART)通过 NVIC 向内核发出的请求。这才是我们通常说的“中断”。

举个例子,你写代码时不小心除零了,CPU 会立刻触发一个 UsageFault 异常。这不是外设干的,是 CPU 自己检测到的。嗯,这里要注意,系统异常往往比外部中断优先级更高,因为系统出问题得赶紧处理。

核心要点:Cortex-M3/M4 支持最多 240 个外部中断(IRQ),加上 15 个系统异常,总共 255 个异常源。但实际芯片厂商会裁剪,比如 STM32F103 只有 68 个中断。

4.2 中断优先级与 NVIC:谁说了算?

好,现在有多个中断同时来了,CPU 先处理谁?这就引出了优先级的概念。

Cortex-M 里管中断的硬件叫 NVIC(嵌套向量中断控制器)。它负责:

  • 使能/禁止中断
  • 设置中断优先级
  • 处理中断挂起和激活状态
  • 实现中断嵌套

优先级怎么设?NVIC 为每个中断提供了一个 8 位的优先级寄存器,但实际使用的位数由芯片厂商决定。比如 STM32 只用了高 4 位,所以优先级范围是 0~15。

这里有个坑,我当年就踩过:数值越小,优先级越高。0 是最高优先级,15 是最低。跟我直觉相反,对吧?

我的习惯:我会把时间关键的中断(比如定时器、DMA)设成 0 或 1,把不太紧急的(比如按键扫描)设成 10 以上。这样能保证系统响应及时。

4.3 中断向量表:中断的“通讯录”

CPU 收到中断后,怎么知道该跳到哪里去执行代码?这就靠 中断向量表

向量表本质上是一个地址数组,每个中断源对应一个表项,里面存放着该中断服务函数(ISR)的入口地址。Cortex-M 的向量表默认放在 0x00000000 地址。

表的结构是这样的:

偏移地址 异常号 功能
0x00 - 主堆栈指针(MSP)初始值
0x04 1 Reset_Handler
0x08 2 NMI_Handler
0x0C 3 HardFault_Handler
... ... ...
0x40 16 IRQ0_Handler(第一个外部中断)

你想想看,CPU 上电后第一件事就是读 0x00 处的值作为栈指针,然后读 0x04 处的值作为复位入口。所以向量表要是坏了,系统根本跑不起来。

我曾经遇到过一个 bug:在程序运行中动态修改了向量表,结果忘记重新使能中断,导致中断触发后 CPU 跳到了一个错误地址,直接 HardFault。所以,修改向量表后一定要做内存屏障(DSB/ISB)

4.4 中断嵌套与抢占:谁插队谁排队?

这是面试常考题,也是实际开发中最容易出问题的地方。

中断嵌套,说白了就是:正在处理一个中断时,来了一个更高优先级的中断,CPU 暂停当前中断,先去处理更紧急的。处理完再回来继续。

Cortex-M 的 NVIC 支持 硬件自动嵌套。你不需要像 8051 那样手动保存现场、开中断。CPU 会自动压栈、跳转、执行、出栈。方便是方便,但有个前提:

  • 高优先级中断可以打断低优先级中断
  • 同级中断不能互相打断(必须等当前处理完)
  • 异常(如 HardFault)可以打断任何中断

举个例子:

// 假设:
// TIM2 中断优先级 = 5
// USART1 中断优先级 = 3(更高)

void TIM2_IRQHandler(void) {
    // 正在处理 TIM2 中断
    // 此时 USART1 中断来了(优先级更高)
    // CPU 自动暂停 TIM2,跳转到 USART1_IRQHandler
    // USART1 处理完后,返回继续执行 TIM2
}

void USART1_IRQHandler(void) {
    // 处理串口数据
}

这里有个关键点:抢占优先级和子优先级。Cortex-M 支持将优先级位拆分成“抢占优先级”和“子优先级”。抢占优先级决定能否打断,子优先级决定同抢占级时的处理顺序。

避坑指南:我曾经在一个项目中,把所有中断都设成了相同的抢占优先级,结果一个长时间的中断把整个系统卡死了。后来我重新规划了优先级分组,把时间敏感的中断单独设成高抢占级,问题就解决了。

4.5 实战建议:中断设计原则

讲了这么多理论,最后分享几个我实际项目中的经验:

  1. 中断服务函数要短:ISR 里只做最必要的事,比如置个标志位、读个数据。复杂的处理放到主循环或任务里。
  2. 优先级分组要统一:整个项目只用一种分组方式(比如 4 位抢占 + 0 位子优先级),别混用,否则逻辑会乱。
  3. 注意临界区保护:如果 ISR 和主循环共享变量,记得用关中断或原子操作保护。
  4. 调试时先关中断:我习惯在调试初期把中断全关了,等基本功能调通再开中断。不然中断一乱,根本不知道哪里出了问题。

好了,异常与中断这块内容就讲到这里。说白了,它就是 Cortex-M 的“应急响应系统”。你设计得好,系统就稳定高效;设计得不好,那就是各种 HardFault 等着你。

下一章咱们聊聊内存保护单元(MPU),那也是个容易让人翻车的地方。