中断原理
Author:余生
一、什么是中断?—— 先从 “生活场景” 讲起
想象你正在写作业(主程序),突然电话响了(外部事件),你必须暂停写作业,去接电话(处理事件),接完电话再回来继续写作业。
这个 “暂停当前任务 → 处理突发事件 → 回到原来任务” 的过程,就是中断(Interrupt)。
在 STM32 中:
- 写作业 = 主程序(main 函数里的代码)
- 电话响了 = 某个外设发出了中断请求(比如按键按下、定时器溢出)
- 接电话 = 执行中断服务函数(ISR)
- 继续写作业 = 返回主程序继续执行
中断的本质:让 CPU 能 “及时响应” 外部或内部的重要事件,而不是傻等或不断轮询。
二、为什么要用中断? vs 轮询(Polling)
对比项 | 轮询方式 | 中断方式 |
---|---|---|
CPU 是否空转 | 是,一直在查状态 | 否,平时正常工作 |
响应速度 | 慢,取决于轮询频率 | 快,事件一发生立刻响应 |
效率 | 低,浪费 CPU 资源 | 高,CPU 可做其他事 |
实时性 | 差 | 强 |
结论:
轮询适合简单、不紧急的任务;
中断适合对实时性要求高的场景,如按键检测、串口收数据、定时任务等。
三、中断系统的三大核心组件
STM32 的中断系统由三个关键部分组成:
- 中断源(Interrupt Source)
- EXTI(外部中断 / 事件控制器)
- NVIC(嵌套向量中断控制器)
我们一个一个深入讲。
四、中断源(Interrupt Source)
中断源就是 “谁发起了中断请求”。
常见中断源类型
类型 | 举例 |
---|---|
外部中断 | 按键按下、外部传感器信号 |
定时器中断 | TIM2 溢出、PWM 周期结束 |
串口中断 | USART1 收到一个字节数据 |
ADC 中断 | 模数转换完成 |
DMA 中断 | 数据传输完成 |
系统异常 | 硬件错误、NMI、SysTick 等 |
所有这些外设都可以配置为 “产生中断”,一旦条件满足,就会向 NVIC 发出请求。
五、EXTI(External Interrupt/Event Controller)—— 外部中断控制器
5.1 什么是 EXTI?
EXTI 是 STM32 中专门用来管理外部引脚中断的模块。它就像一个 “门口保安”,负责监听哪些 GPIO 引脚发生了电平变化,并决定是否上报给 NVIC。
注意:虽然叫 “外部中断”,但它其实是芯片内部的一个控制器,专门处理来自 GPIO 引脚的中断请求。
5.2 EXTI 的结构(以 STM32F1 为例)
STM32F1 系列有 19 条 EXTI 线路(Line 0~15 + 16~18)
- Line 0 ~ 15:对应每个 GPIO 引脚(PA0、PB0、PC0…)
- Line 16:PVD(可编程电压检测)
- Line 17:RTC 闹钟
- Line 18:USB 唤醒
关键点:
虽然有多个 GPIO 端口(A/B/C/D…),但每个编号的引脚共用一条 EXTI 线。
例如:PA0、PB0、PC0 都连接到 EXTI Line 0,但同一时间只能有一个能触发中断(需要软件选择)。
5.3 EXTI 的工作流程
- 配置 GPIO 为输入模式(如上拉输入)
- 选择哪个引脚作为中断源(通过 AFIO 寄存器选择 PA0 还是 PB0)
- 设置触发方式:
- 上升沿触发(从低变高)
- 下降沿触发(从高变低)
- 双边沿触发(高低都触发)
- 使能 EXTI 中断输出
- EXTI 检测到信号变化 → 向 NVIC 发送中断请求
5.4 EXTI 的 “中断” vs “事件”
EXTI 不仅能产生中断,还能产生 “事件”(Event)。
对比 | 中断(Interrupt) | 事件(Event) |
---|---|---|
是否进入 CPU | 是,会跳转到 ISR | 否,不进 CPU |
是否需要 NVIC | 是 | 否 |
用途 | 需要 CPU 参与处理 | 触发其他外设(如启动 ADC、DMA) |
延迟 | 有中断响应延迟 | 极快,硬件直连 |
举例:
你可以设置 “按键按下” 产生一个事件,直接触发 ADC 开始采样,全程不需要 CPU 参与,效率极高!
六、NVIC(Nested Vectored Interrupt Controller)—— 嵌套向量中断控制器
这是整个中断系统的 “大脑” 和 “调度中心”。
6.1 NVIC 的功能
- 接收所有中断请求(来自外设或 EXTI)
- 判断优先级,决定先响应哪个
- 支持中断嵌套(高优先级可打断低优先级)
- 自动保存 / 恢复上下文(CPU 寄存器)
- 跳转到正确的 ISR
NVIC 是 ARM Cortex-M 内核的一部分,不是 STM32 厂商自己设计的,所有 Cortex-M 芯片都有 NVIC。
6.2 中断优先级(Priority)
STM32 的中断优先级分为 4 位(F1 系列),可以分成:
- 抢占优先级(Preemption Priority):决定是否可以 “打断” 其他中断
- 子优先级(Subpriority):决定多个中断同时发生时的执行顺序
优先级分组(Priority Group)
由于只有 4 位,需要事先分配多少位给抢占,多少位给子优先级。这叫 “优先级分组”。
分组模式 | 抢占位数 | 子优先级位数 | 最大组数 |
---|---|---|---|
Group 0 | 0 位 | 4 位 | 1 组,16 子 |
Group 1 | 1 位 | 3 位 | 2 组,8 子 |
Group 2 | 2 位 | 2 位 | 4 组,4 子 ✅ 常用 |
Group 3 | 3 位 | 1 位 | 8 组,2 子 |
Group 4 | 4 位 | 0 位 | 16 组,无子 |
规则:
- 抢占优先级高的可以打断抢占优先级低的(嵌套)
- 抢占相同,子优先级高的先执行
- 抢占和子都相同,看中断号(越小越优先)
推荐使用 Group 2:4 个抢占优先级 + 4 个子优先级,够用且不易出错。
6.3 中断向量表(Interrupt Vector Table)
这是 NVIC 的 “电话簿”,记录了每个中断对应的处理函数地址。
特点
- 存放在 Flash 开头(地址 0x0000_0000)
- 每个中断占 4 字节,存的是函数指针
- 包括:
- 异常(Exception):复位、NMI、Hard Fault、SysTick 等
- 外设中断:TIM2_IRQn、USART1_IRQn、EXTI0_IRQn…
举例: 当你按下按键触发 EXTI0 中断,NVIC 查表发现 EXTI0_IRQn 对应的函数是 EXTI0_IRQHandler
,于是跳过去执行。
6.4 NVIC 的寄存器
寄存器 | 功能 |
---|---|
ISER(Set Enable Register) | 使能中断 |
ICER(Clear Enable Register) | 关闭中断 |
ISPR(Set Pending Register) | 手动触发中断(软件中断) |
ICPR(Clear Pending Register) | 清除中断挂起状态 |
IPR(Interrupt Priority Register) | 设置中断优先级 |
实际编程中我们用库函数(如
NVIC_EnableIRQ()
)操作,不用直接写寄存器。
七、中断的完整执行流程(从触发到返回)
我们以 “按键触发 EXTI0 中断” 为例,详细走一遍流程:
步骤 1:事件发生
- 用户按下按键 → PA0 引脚从高电平变为低电平(下降沿)
步骤 2:EXTI 检测
- EXTI Line 0 检测到下降沿
- 触发条件满足 → 设置 “挂起寄存器”(Pending Bit)
步骤 3:发送请求给 NVIC
- EXTI 向 NVIC 发出中断请求(IRQ)
步骤 4:NVIC 判断是否响应
- 当前是否有更高优先级中断正在运行?
- 全局中断是否使能?(CPSR 寄存器 I 位)
- 如果可以响应 → 进入响应阶段
步骤 5:CPU 响应中断(异常进入)
- 自动保存上下文(R0~R3, R12, LR, PC, xPSR)
- 读取中断向量表,跳转到
EXTI0_IRQHandler
步骤 6:执行中断服务函数(ISR)
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
// 处理按键逻辑:比如翻转LED
LED_Toggle();
// 清除中断标志位(重要!)
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
2
3
4
5
6
7
8
步骤 7:中断返回
- 执行
BX LR
指令 - CPU 自动恢复之前保存的上下文
- 继续执行被中断的主程序
八、中断服务函数(ISR)编写注意事项
正确做法
- 函数名必须和启动文件中定义的一致(如
EXTI0_IRQHandler
) - 尽量简短,不要做耗时操作(如延时、打印)
- 及时清除中断标志位(否则会反复进入中断)
- 可以设置标志位,让主程序去处理复杂逻辑
错误做法
void EXTI0_IRQHandler(void) {
Delay_ms(1000); // 千万不要在这里延时!
printf("Key pressed!\n"); // 打印太慢,影响实时性
}
2
3
4
推荐做法
volatile uint8_t key_flag = 0; // 全局标志
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0)) {
key_flag = 1; // 只设标志
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
// 主程序中处理
while (1) {
if (key_flag) {
key_flag = 0;
LED_Toggle();
printf("Key pressed!\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
九、中断嵌套(Nested Interrupt)
什么是嵌套?
高优先级中断可以打断正在执行的低优先级中断。
举例
- 中断 A:优先级 2(抢占)
- 中断 B:优先级 1(抢占) ← 更高
当 CPU 正在执行 A 的 ISR 时,B 发生 → CPU 会暂停 A,先执行 B,B 执行完再回到 A。
注意:
- 只有抢占优先级更高才能打断
- 子优先级不能打断
十、常见中断编号与命名(STM32F1 参考)
中断名 | 对应外设 | 说明 |
---|---|---|
NMI_IRQn | 非屏蔽中断 | 不受 NVIC 控制,必须响应 |
HardFault_IRQn | 硬件错误 | 如访问非法地址 |
SysTick_IRQn | 系统滴答定时器 | 操作系统常用 |
WWDG_IRQn | 窗口看门狗 | |
EXTI0_IRQn ~ EXTI15_IRQn | 外部中断线 0~15 | |
TIM2_IRQn | 定时器 2 | |
USART1_IRQn | 串口 1 | |
ADC1_2_IRQn | ADC1 和 ADC2 |
所有中断名定义在
stm32f10x.h
文件中。
十一、软件配置流程(以 HAL 库为例)
// 1. 使能时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_AFIO_CLK_ENABLE(); // EXTI需要AFIO
// 2. 配置GPIO为输入
GPIO_InitTypeDef gpio;
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &gpio);
// 3. 配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); // 抢占2,子0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
// 4. 编写回调函数(HAL库风格)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_0) {
LED_Toggle();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
十二、中断的优缺点总结
优点 | 缺点 |
---|---|
响应快,实时性强 | 配置复杂 |
节省 CPU 资源 | 多中断时优先级管理复杂 |
支持嵌套,灵活性高 | ISR 中不能调用阻塞函数 |
可实现事件驱动编程 | 调试困难(断点难打) |
总结:一张图看懂 STM32 中断系统
[GPIO 引脚]
↓ (电平变化)
[EXTI 控制器] —— 判断是否触发、是中断还是事件
↓ (中断请求)
[NVIC] —— 查优先级、查向量表、决定是否响应
↓
[CPU] —— 保存现场 → 跳转到 ISR → 执行 → 恢复现场
↓
[主程序继续运行]
2
3
4
5
6
7
8
9