Skip to content

LV002-异常与中断基本概念 ​

在LV001-STM32中断简介里我们快速过了一遍 STM32 的中断体系,但初学时最容易混淆的两个词——异常(Exception)和中断(Interrupt)——还没有展开讲清楚。这篇笔记就把这两个基本概念掰开揉碎:它们分别是什么、从哪来、什么区别、CPU 又是如何响应它们的。理解了这两个词,后面看 NVIC、向量表、HardFault 排查等内容都会顺畅很多。

一、 为什么需要中断 ​

假设没有中断,单片机想知道"串口有没有收到数据",只有一种办法——轮询(Polling):

c
/* 轮询方式:CPU 死等标志位 */
while (1)
{
    if (USART1->SR & USART_SR_RXNE)   /* 反复读状态寄存器 */
    {
        data = USART1->DR;            /* 收到才处理 */
        process(data);
    }
    /* 别的事一件都干不了 */
}

轮询的缺点很明显:

  • CPU 空转:大部分时间标志位都是 0,白白浪费在"反复查看"上;
  • 实时性差:如果还要轮询另一个外设,就得轮流检查,事件可能被耽误;
  • 功耗高:CPU 一直在全速跑。

于是硬件设计师换了一种思路:与其让 CPU 反复去问"有事吗",不如让事件主动通知 CPU。平时 CPU 安心执行主程序,事件发生时硬件强制 CPU 暂停手头工作,先去处理紧急事件,处理完再回来继续。这个"强制暂停并跳转"的机制,就是本篇的主角——中断/异常机制。

一句话:中断的本质是把 CPU 从"查询等待"中解放出来,变轮询驱动为事件驱动。

二、 基本概念详解 ​

1. 异常是什么 ​

1.1 异常的定义 ​

在 ARM 的术语体系里,异常(Exception)是所有需要打断处理器正常执行流的事件的统称。也就是说:只要发生了"CPU 不能按原顺序继续执行下去、必须先处理某件事"的情况,这件事就是一个异常。

这个过程和生活中"看电视剧时有人敲门"是一样的:电视剧暂停(当前程序暂停)→ 去开门(处理紧急事件)→ 回来继续看(返回原程序继续执行)。区别只在于,"敲门这件事"到底是 CPU 自己惹出来的,还是外面送进来的:

  • CPU 内部产生的(执行指令本身引发的):比如除零、取指访问了非法地址、执行了未定义指令——这类事件称为异常(狭义);
  • CPU 外部送来的(片上外设或引脚产生的):比如串口收到数据、定时器溢出、GPIO 引脚电平变化——这类事件称为中断(狭义)。

两者的处理流程几乎完全一样:打断 → 保存现场 → 跳到服务函数 → 恢复现场返回。所以在 ARM 手册里,中断被看作异常的一个子集(详见本节第 3 小节)。

把主程序执行流画出来,两类事件的"打断方式"一目了然:

异常与中断打断执行流示意图:主程序沿竖直时间轴顺序执行指令,DIV #0 指令引发同步异常、外设事件在指令边界送达触发异步中断,两者都跳转到各自的服务程序(竖线段表示服务执行时段),处理完后沿返回线回到同一个断点继续执行原程序

图中可见两类事件唯一的本质区别是到达方式:同步异常由某条指令引发(重跑必在同一位置复现),异步中断与指令无关、随时可能到来;而打断、保存现场、跳转、恢复返回的响应机制完全一致。

1.2 异常的分类 ​

按不同维度,异常可以这样划分:

(1)按来源分

分类来源例子
内部异常(狭义异常)CPU 执行指令引发除零、未定义指令、地址越界、SVC 系统调用
外部异常(即中断)片上外设/引脚信号USART 接收、TIM 更新、EXTI 电平变化

(2)按与指令流的同步关系分

分类含义例子
同步异常由某条指令的执行直接引起,重跑同一程序必然在同一位置复现未定义指令、BusFault、SVC
异步异常与当前指令无关,随时可能到来外设中断、NMI

这个分类在排查死机时非常有用:同步异常意味着程序本身有 bug(如野指针),必然可复现;异步异常则要看是哪个外设的事件,方向完全不同(可参考LV001-死机原因与故障定位)。

1.3 Cortex-M 中的系统异常 ​

Cortex-M3 内核定义了 16 个编号(1~15,没有 0 号)的系统异常,它们不是外设事件,而是内核级事件。完整清单如下(也是启动文件里最前面那几个 *_Handler 的来历):

异常编号名称优先级说明
-(0 号不存在)-向量表第 0 项存的是 MSP 初始值
1Reset-3(固定,最高)复位,程序从这里开始执行
2NMI-2(固定)不可屏蔽中断,如时钟安全系统 CSS 触发
3HardFault-1(固定)硬件异常,所有无法处理的错误的"兜底"
4MemManage可编程存储器管理异常(MPU 违规等)
5BusFault可编程总线错误(访问非法地址等)
6UsageFault可编程用法错误(未定义指令、除零等)
7~10Reserved-保留
11SVCall可编程执行 SVC 指令触发(系统服务调用)
12Debug Monitor可编程调试监视器(断点、观察点)
13Reserved-保留
14PendSV可编程可挂起系统服务,RTOS 常用它做任务切换
15SysTick可编程系统滴答定时器,RTOS 的心跳

关于这张表的优先级规则、256 个异常编号的完整划分,在LV040-CortexM3异常中已有详细展开,这里只需要建立一个印象:编号 1~15 是内核自己的事(系统异常),编号 16 及以后才是外设的事(中断)。

1.4 常见 Fault 异常的触发场景 ​

实际开发中最常遇到的异常就是各种 Fault(有的资料翻译为"失效/错误"),先混个眼熟:

Fault典型触发场景
HardFault野指针/未初始化指针跳转、栈溢出破坏现场、除零(未开启 DIV_0_TRP 时也会升级为 HardFault)、其它 Fault 未使能时"升级"上报
BusFault访问不存在的地址(如把外设地址写错)、预取指失败
UsageFault执行未定义指令、非对齐访问、除零(使能 DIV_0_TRP 后)
MemManageMPU 规则违反(未配置 MPU 的 F103 一般遇不到)

遇到 HardFault 不要慌,本质就是"CPU 执行不下去了,跳进了 HardFault_Handler 死循环",把 PC/LR 等现场打印出来即可定位,具体方法见《80-异常与死机排查》系列。

2. 中断是什么 ​

2.1 中断的定义 ​

中断(Interrupt)是由 CPU 外部的事件引起的、请求 CPU 暂停当前程序去处理的服务请求。注意三点:

  • 来源在外部:由片上外设(USART、TIM、ADC、DMA……)或外部引脚(EXTI)产生,通过 NVIC 送入内核;
  • 异步:什么时候来与 CPU 正在执行什么指令无关;
  • 需要处理的服务:来的是"事",CPU 要去执行对应的中断服务函数(ISR)。

在 Cortex-M 的编号体系里,中断 = 编号 ≥ 16 的异常,每个中断对应一个外设事件线,这个编号就是后面 HAL 库里的 IRQn_Type(如 USART1_IRQn = 37)。NVIC 最多支持 240 个中断输入,具体数目由芯片厂商裁剪决定,STM32F103 只用了 60 个。

2.2 中断的分类 ​

(1)按请求源分

分类说明例子
片上外设中断芯片内部外设的事件,通过 NVIC 统一管理USART1_IRQn、TIM2_IRQn、DMA1_Channel1_IRQn
外部引脚中断(EXTI)芯片引脚电平变化,经 EXTI 送入 NVIC按键按下(EXTI0_IRQn)

注意"外部中断(EXTI)"这个词指的是引脚来的中断,是"片上外设中断"的子集,不要和"中断=外部事件"这种广义说法混淆。

(2)按能否屏蔽分

分类说明
可屏蔽中断(IRQ)绝大多数中断,可以通过 PRIMASK/BASEPRI/NVIC 使能位屏蔽
不可屏蔽中断(NMI)优先级固定为 -2,无法屏蔽,来了必须响应,如 F103 的时钟安全系统 CSS

(3)按触发方式分

触发方式说明适用场景
边沿触发信号出现上升沿/下降沿时置位请求按键、脉冲捕获
电平触发信号保持有效电平期间持续请求需要电平同步的场合

Cortex-M 的 NVIC 中断输入是电平敏感的(悬起寄存器把请求保存下来),STM32 的 EXTI 则可配置为上升沿/下降沿/双边沿触发。

2.3 中断相关术语详解 ​

这一小节是重点。中断系统术语多,很多资料一带而过,这里逐个说透:

术语英文/寄存器含义
中断源-产生中断事件的源头(某个外设、某根引脚)
中断请求IRQ(Interrupt Request)中断源向 CPU 发出的"请处理我"的信号
中断号(IRQn)IRQn_TypeNVIC 给每个中断的编号,从 0 开始;异常号 = 中断号 + 16
异常号IPSR 寄存器可读向量表中的位置编号,系统异常 1~15,中断从 16 起
中断标志位外设状态寄存器(如 SR)外设自己记录"事件发生了",与 NVIC 无关,需软件清除
悬起(挂起)Pending(ISPR/ICPR)请求已送达 NVIC 但尚未被响应的"排队"状态,即使源信号消失也不会丢
中断使能SETENA/CLRENANVIC 中控制某中断是否被允许响应的开关,两级:全局(PRIMASK)+ 单个中断
中断向量Vector该中断服务函数的入口地址
中断向量表VTOR 指向的表按异常号顺序排列的所有向量的表格,启动文件 __Vectors 定义
中断服务函数ISR(Interrupt Service Routine)响应中断后 CPU 去执行的函数,如 USART1_IRQHandler
现场保护/恢复自动压栈/出栈保存被打断时的寄存器状态,以便处理完后原样恢复
中断嵌套Nesting高抢占优先级中断可以打断正在执行的低优先级中断
临界区Critical Section主程序与 ISR 都会访问的共享数据区域,需要关中断等手段保护

其中三个最容易混:中断标志位(外设记录事件,要手动清)、悬起状态(NVIC 记录"排队中",响应后自动清,也可软件清)、使能位(NVIC 的响应开关)。一次完整的中断传递是:

外设事件发生 → 外设状态寄存器置标志位 → 中断输出线送 NVIC(若外设中断使能)
→ NVIC 置悬起位 → 优先级允许则响应(悬起位自动清除)→ 执行 ISR → ISR 内清外设标志位

漏了最后一步"清外设标志位",退出 ISR 后会立刻再次进入,程序表现为"死在中断里"。

3. 异常与中断的关系和区别 ​

3.1 包含关系 ​

用一张图表示 Cortex-M 中两者的关系:

异常与中断的包含关系图:异常是统称,编号 1~15 为系统异常,编号 16 及以上为外部中断 IRQ
  • 广义上:异常 = 系统异常 + 外部中断,中断是异常的子集;
  • 狭义上(日常口语区分):异常指内核内部事件,中断指外设请求事件。

3.2 对比表 ​

对比项异常(狭义)中断(狭义)
产生者CPU 内部(指令执行引发)CPU 外部(外设/引脚)
同步性同步为主,与指令绑定异步,与指令无关
编号1~15≥16(IRQn = 异常号 - 16)
可屏蔽性NMI/HardFault 等不可屏蔽一般均可屏蔽
服务函数名HardFault_Handler、SVC_Handler 等USART1_IRQHandler、EXTI0_IRQHandler 等(带 _IRQ)
典型例子除零、非法地址、SVC 调用串口接收、定时器溢出、按键
谁的责任多为软件 bug,可复现排查多为正常业务事件,需设计处理

3.3 为什么经常统称"中断" ​

因为两者的响应机制完全一致(保存现场 → 查向量表 → 执行服务函数 → 恢复现场),CPU 硬件并不区分处理,日常交流说"进中断了"往往把 HardFault 也包含在内。但在读 ARM 官方手册时必须严谨:手册中的 Interrupt 特指 IRQ,Exception 才是统称。

3.4 常见疑问澄清 ​

  • HardFault_Handler 是中断服务函数吗? 习惯上叫,但严格说它是异常服务函数——看名字没有 _IRQ 后缀;
  • SysTick 是中断还是异常? 它是 15 号系统异常,但因为行为和中断一样(周期性打断 CPU),习惯都叫"SysTick 中断";
  • 为什么启动文件里中断函数名是固定的? 因为向量表里存的就是这些名字对应的地址,名字写错向量就指向了默认的死循环(Default_Handler),表现为"中断开了但没反应"。

3.5 扩展:其它体系结构的叫法 ​

如果以后接触 x86 或其它内核,会看到 Trap(陷阱)、Fault(故障)、Abort(中止)等词,它们本质上都是"异常的细分类型"。Cortex-M 简化了这套模型,只保留 Fault + IRQ + 少量系统异常,理解了 Cortex-M 的分类,再看其它架构就是换个名字而已。

三、 中断响应机制 ​

先看全景:一次中断从外设到达 CPU,要经过"中断源 → NVIC → 内核 → 向量表"这条硬件链路:

STM32 中断系统硬件架构图:USART、TIM、DMA、EXTI 等中断源把请求送入 NVIC,NVIC 裁决后送内核响应,内核从向量表取入口地址

1. 中断被响应的条件 ​

一个中断要被响应,需要同时满足:

(1)全局中断使能:PRIMASK 未关中断(__disable_irq() 就是置位 PRIMASK);

(2)该中断在 NVIC 中已使能:SETENA 对应位为 1(HAL_NVIC_EnableIRQ() 干的事);

(3)优先级允许:没有同级或更高优先级的异常正在执行;

(4)指令边界:当前指令执行完毕(中断不能打断一条执行到一半的指令)。

四者缺一,中断就进入悬起状态排队等待。

2. 响应的五个阶段 ​

(1)事件检出:NVIC 收到请求,置悬起位;

(2)优先级裁决:若同时有多个悬起,先响应优先级最高的;

(3)现场保护(自动压栈):硬件自动把 8 个寄存器压入当前栈(主栈 MSP 或进程栈 PSP):{PC, xPSR, R0-R3, R12, LR}——R4~R11 不自动保存,所以 ISR 中若用到,由编译器插入的代码负责保存;

(4)取向量:从向量表中取出该异常号对应的服务函数入口地址(NVIC 的"V"就是 Vector,硬件自动查表,无需软件判断来源,这也是"向量化中断"相比"非向量化中断"快的原因);

(5)跳转执行 ISR:PC 被装载为服务函数地址,开始执行我们写的代码。

把这条链路画成流程图,配合上面的架构图对照着看:

中断响应完整流程图:外设事件置标志位并送入 NVIC 悬起,条件允许时硬件自动压栈、查向量表执行 ISR,清标志后经异常返回序列恢复原程序

3. 异常返回 ​

ISR 执行完最后一条 BX LR 时,由于进入异常时 LR 被硬件改写为特殊的 EXC_RETURN 值(0xFFFFFFF1/F9/FD 之一),CPU 识别到它就触发异常返回序列:自动出栈恢复 8 个寄存器,继续执行被打断的代码。所以 ISR 在 C 语言里就是一个普通函数,不需要写任何特殊返回语句。

4. 两个性能优化概念 ​

Cortex-M3 为了缩短中断响应还设计了两个机制,这里只留个印象(详见《CM3权威指南》):

  • 晚到中断:压栈尚未完成时来了更高优先级中断,先响应后来者,现场无需保存两遍;
  • 咬尾中断(Tail-Chaining):一个 ISR 刚结束又有一个悬起中断要处理时,不再出栈再压栈,直接"咬"着处理下一个,节省约 12 个周期。

5. 优先级与中断嵌套 ​

多个中断"抢"CPU 时靠优先级裁决,规则只有四条:

(1)数值越小,优先级越高;复位(-3)> NMI(-2)> HardFault(-1)> 其它(0 起可编程);

(2)优先级分为抢占优先级和子优先级两段(分组决定各占几位);

(3)只有抢占优先级高才能打断正在执行的中断(中断嵌套);子优先级只在"多个中断同时悬起排队"时决定先响应谁,不能引发嵌套;

(4)抢占和子优先级都相同时,异常编号小的先响应。

STM32F103 只用了 4 位表达优先级(16 级),分组配置与 HAL 库函数的用法在LV001-STM32中断简介第 4.3、4.4 节有完整实例;NVIC 各控制寄存器的细节见LV045-CortexM3的NVIC中断控制。

四、 实践与总结 ​

1. 使用中断的注意事项 ​

概念清楚了,写代码时还要遵守几条实践原则:

(1)ISR 尽量短小。中断里只做"取数据、置标志",耗时的解析、打印、延时放到主循环处理。ISR 拖得太长会阻塞低优先级中断,实时性反而变差。

(2)主程序与 ISR 共享的变量必须加 volatile,否则编译器优化(比如把变量缓存在寄存器里)会让主循环"看不见"ISR 的修改。

c
volatile uint8_t rx_done = 0;   /* 少了 volatile,主循环可能永远读不到 1 */

void USART1_IRQHandler(void)
{
    /* ... */
    rx_done = 1;
}

int main(void)
{
    /* ... */
    while (1)
    {
        if (rx_done) { /* 处理 */ rx_done = 0; }
    }
}

(3)访问共享数据要进临界区。主程序修改多字节共享数据前关中断,改完再开,防止改到一半被 ISR 打断:

c
__disable_irq();
shared_var = new_value;     /* 原子地完成修改 */
__enable_irq();

(4)及时、正确地清除标志位。清标志的顺序也有讲究:一般先清标志再做后续处理,且要按参考手册要求的方式清(写 0 清、写 1 清、读 SR 后写 DR 清各有不同),漏清会反复进中断,清错会丢中断。

(5)注意栈大小。每层中断嵌套都要在栈上压入至少 32 字节现场,加上 ISR 局部变量,启用嵌套多的工程要给栈留足余量(MSP 初始值在启动文件开头定义)。

(6)HAL 库的中断流程要认全。以串口为例,中断入口是向量表指定的 USART1_IRQHandler,真正的处理在 HAL 库内分两层:

c
void USART1_IRQHandler(void)                 /* ① 向量表指定的入口 */
{
    HAL_UART_IRQHandler(&huart1);            /* ② HAL 统一处理:判断标志、清标志 */
}                                            /* ③ 再回调用户代码,如 HAL_UART_RxCpltCallback() */

入口函数名必须与启动文件一致,业务逻辑写在回调里,不要直接在 IRQHandler 里既清标志又写业务,容易和 HAL 的清标志动作打架。

2. 要点小结 ​

  • 异常是统称:一切打断 CPU 正常执行流的事件;中断是子集:编号 ≥16、来自外设的那部分;
  • 狭义区分看来源:内部指令引发的叫异常(Fault、SVC……),外部事件请求的叫中断(USART、EXTI……);
  • 一次完整传递:外设置标志 → NVIC 悬起 → 优先级裁决 → 自动压栈 → 查向量表 → 执行 ISR → 清标志 → EXC_RETURN 返回;
  • 记住三个"记录员":外设标志位(软件清)、NVIC 悬起位(响应后自动清)、使能位(响应开关);
  • 实践口诀:ISR 短小、共享加 volatile、临界区保护、标志位及时清。

相关阅读: