STM32 中断系统详解
1. 什么是中断?
CPU 正在执行主程序,突然外部或内部事件发生(按键按下、定时器溢出、串口收到数据……),CPU 暂停当前工作,跳去处理紧急事件,处理完再回来继续。
中断响应与返回流程
💡 核心思想:中断让 CPU 不用"傻等",而是事件驱动——有事才处理,没事干正事。
2. NVIC — 嵌套向量中断控制器
NVIC = Nested Vectored Interrupt Controller,是 Cortex-M3 内核自带的中断管理器,负责:
接收所有中断请求
判断优先级,决定谁先执行
支持嵌套——高优先级中断可以打断低优先级中断
2.1 NVIC 关键寄存器
名称
位数
个数
作用
中断使能寄存器(ISER,Interrupt Set Enable Register)
32
8
每一位控制一个中断(打开)
中断失能寄存器(ICER,Interrupt Clear Enable Register)
32
8
每一位控制一个中断(关闭)
应用程序中断及复位控制寄存器(AIRCR,Application Interrupt and Reset Control Register)
32
1
位 [10:8] 控制中断优先级分组
中断优先级寄存器(IPR,Interrupt Priority Register)
8
240
8 位对应一个中断,STM32 只用高 4 位
💡 寄存器之间的配合关系
ISER / ICER:32 × 8 = 256 位,控制 240 个中断的开关
AIRCR:位 [10:8] 共 3 bit → 2³ = 8 种组合,取其中 5 组作为优先级分组(Group 0~4)
IPR:每个中断占 8 位,但只用高 4 位设置优先级;哪几位是抢占、哪几位是响应,由 AIRCR 决定
2.2 NVIC 工作原理
NVIC嵌套向量中断控制器
💡 NVIC 与 CPU 的关系:NVIC 和 CPU 同处于 Cortex-M3 内核中,NVIC 相当于 CPU 的"秘书"——负责屏蔽中断、判断优先级、处理中断向量,CPU 只管执行最终送来的 ISR。
ℹ️ 这些寄存器都在 NVIC 里面吗?
ISER、ICER、IPR → 是 NVIC 内部寄存器
AIRCR、SHPR → 严格来说属于 SCB(System Control Block,系统控制块),但 SCB 控制着 NVIC 的分组和内核中断优先级,两者通常一起讨论
NVIC 和 SCB 都位于 Cortex-M3 内核中,与 CPU 紧密协作
ℹ️ 工作过程
外部中断触发后,先经过 ISER/ICER 判断该中断是否被使能
使能的中断进入 IPR 寄存器,根据 AIRCR 的分组规则,判断抢占优先级和响应优先级
最终按优先级高低依次送入 CPU 执行
内核中断(如 SysTick、PendSV)由 SHPR 寄存器控制,与 IPR 属于同一级别
2.3 中断优先级分组(Priority Group)
STM32 用 4 个 bit 来表示中断优先级,这 4 位可以灵活分配给两种优先级:
分组
抢占优先级位数
响应优先级位数
抢占级范围
响应级范围
0
0 bit
4 bit
0
0-15
1
1 bit
3 bit
0-1
0-7
2
2 bit
3 bit
0-3
0-3
3
3 bit
1 bit
0-7
0-1
4
4 bit
0 bit
0-15
0
⚠️ 常用分组:实际项目中 Group 2 或 Group 3 最常见,兼顾嵌套层数和同级排序。
2.4 抢占优先级 vs 响应优先级
对比项
抢占优先级(Preemption)
响应优先级(Sub-priority)
核心能力
能打断别人
不能打断,只能排队
作用时机
中断正在执行时,新中断能否插队
两个中断同时到达时,谁先执行
类比
急诊 vs 普通号
普通号里的排队顺序
📋 优先级判断规则(从高到低)
抢占优先级(Preemption):高抢占可以打断正在执行的低抢占中断
响应优先级(Sub-priority):抢占相同时,响应高的先执行,但不能互相打断
自然优先级:抢占和响应都相同时,按中断向量表编号排序(编号小的优先)
数值越小 = 优先级越高
举个例子(分组 2:2 位抢占 + 2 位响应)
| 中断 | 抢占优先级 | 响应优先级 |
| :--- | :---: | :---: |
| 定时器 TIM2 | 1 | 0 |
| 外部中断 EXTI0 | 0 | 1 |
| 串口 USART1 | 1 | 1 |
- EXTI0 的抢占级最高(0 < 1),可以打断 TIM2 和 USART1
- TIM2 和 USART1 抢占级相同(都是 1),不能互相打断
- 如果 TIM2 和 USART1 同时到达,TIM2 响应级更高(0 < 1),TIM2 先执行
- 如果抢占和响应都相同?→ 比中断向量号(硬件编号小的优先)
⚠️ 数值越小 = 优先级越高:这是 STM32 的规则,0 是最高优先级。和 FreeRTOS 相反(FreeRTOS 数值越大优先级越高),和 Linux 普通进程优先级(nice 值)相同。
2.5 NVIC 使用步骤(HAL 库)
NVIC使用步骤
步骤
操作
寄存器
HAL 函数
1
设置中断分组
AIRCR[10:8]
HAL_NVIC_SetPriorityGrouping()
2
设置中断优先级
IPR[7:4]
HAL_NVIC_SetPriority()
3
使能中断
ISER
HAL_NVIC_EnableIRQ()
3. EXTI — 外部中断/事件控制器
EXTI = External Interrupt/Event Controller,用来检测 GPIO 引脚的电平变化并产生中断。
3.1 EXTI 与 GPIO 的映射关系
stm32 gpio外部中断简图
每条 EXTI 线对应同一编号的所有端口引脚,但同一时刻只能选一个。每组(如 PA0/PB0/PC0)经 AFIO 选择器后只有一个能连到 EXTI0,以此类推。
⚠️ 不能同时用 PA0 和 PB0 做外部中断:因为它们都映射到 EXTI0,只能二选一。
3.2 EXTI 信号流程
从 GPIO 引脚到 CPU 响应中断,经过以下步骤:
EXTI信号流程框图
ℹ️ EXTI 线数量说明
框图中有 20 条线,但 STM32F103 实际是 19 条 EXTI 线(19 = 16 + 3):
16 条分配给外部 GPIO(PA0~PA15 / PB0~PB15 … 每个 pin 编号对应一条 EXTI 线)
3 条是内部系统用(PVD、RTC 闹钟、USB 唤醒)
注意:EXTI 线与 GPIO 引脚按编号一一对应,但经过 NVIC 后会有合并(EXTI5~9 共享一个 NVIC 中断号,EXTI10~15 同理)。
💡 参考手册原版框图
完整的 EXTI 功能框图请参考 RM0008 参考手册(STM32F103)第 10 章 "Interrupts and events",或野火文档:EXTI 外部中断/事件控制器
参考来源:King~30+STM32--中断使用(超详细!)-CSDN博客
按键按下后发生了什么?(中断触发全流程)
步骤
发生了什么
涉及寄存器
1
PA0 电平从低→高(上升沿)
GPIO 硬件
2
EXTI0 边沿检测电路捕获到上升沿
EXTI_RTSR
3
检查 IMR:EXTI0 未被屏蔽 → 放行
EXTI_IMR
4
PR 第 0 位自动置 1(挂起标志)
EXTI_PR
5
中断请求送入 NVIC → 判断优先级
NVIC_IPR
6
CPU 保存现场,跳转执行 EXTI0_IRQHandler()
—
7
ISR 中写 1 清除 PR 第 0 位
EXTI_PR
8
CPU 恢复现场,回到主程序
—
ℹ️ 中断 vs 事件
中断(Interrupt):经过 NVIC → CPU 执行 ISR(软件处理)
事件(Event):不经 CPU,直接触发其他硬件(如 DMA、ADC),更快、不占 CPU
3.3 EXTI 关键寄存器
寄存器
全称
功能
EXTI_IMR
Interrupt Mask Register
中断屏蔽:对应位置 1 允许中断,置 0 屏蔽
EXTI_EMR
Event Mask Register
事件屏蔽:同上,控制事件输出
EXTI_RTSR
Rising Trigger Selection
上升沿触发:对应位置 1 启用
EXTI_FTSR
Falling Trigger Selection
下降沿触发:对应位置 1 启用
EXTI_PR
Pending Register
中断挂起标志:对应位为 1 表示该线有中断发生
EXTI_SWIER
Software Interrupt Event
软件触发中断(写 1 触发)
💡 实际只需掌握 4 个 EXTI 寄存器:框图中涉及 7 个寄存器,但对于外部中断开发,核心只有 4 个:EXTI_RTSR(上升沿)、EXTI_FTSR(下降沿)、EXTI_PR(挂起标志)、EXTI_IMR(中断屏蔽)。
EXTI寄存器示例1
EXTI寄存器示例2
💡 上升沿 & 下降沿可以同时启用:RTSR 和 FTSR 对应位都置 1 = 双边沿触发。
EXTI PR寄存器
⚠️ PR 寄存器清除方式:中断挂起标志必须在 ISR 中写 1 清零(不是写 0!),否则中断会反复触发。
寄存器直接操作:
EXTI->PR = (1 << line); // 写1清除对应位
CubeIDE 生成的 HAL 宏:
#define __HAL_GPIO_EXTI_CLEAR_IT(__EXTI_LINE__) (EXTI->PR = (__EXTI_LINE__))
3.4 实例:PA0 按键上升沿中断——寄存器全流程
以 PA0 接按键、上升沿触发中断 为例,从头到尾需要配置哪些寄存器:
配置步骤:① RCC 开时钟 → ② GPIO 设输入模式 → ③ AFIO 映射到 EXTI → ④ EXTI 选触发沿 → ⑤ EXTI 取消屏蔽 → ⑥ NVIC 使能 + 设优先级
对应的寄存器操作(标准库写法):
/* ① 开时钟:GPIOA + AFIO */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA 时钟
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // AFIO 时钟(EXTI映射必须开)
/* ② PA0 配置为下拉输入(CNF=10 输入上拉/下拉, MODE=00 输入模式)*/
GPIOA->CRL &= ~(0xF << 0); // 清除 PA0 配置
GPIOA->CRL |= (0x8 << 0); // CNF=10, MODE=00
GPIOA->ODR &= ~(1 << 0); // ODR=0 → 下拉(按键按下给高电平)
/* ③ AFIO:PA0 映射到 EXTI0 */
AFIO->EXTICR[0] &= ~(0xF << 0); // EXTI0 选择 PA(0000=PA)
/* ④ EXTI:上升沿触发 */
EXTI->RTSR |= (1 << 0); // EXTI0 上升沿使能
EXTI->FTSR &= ~(1 << 0); // 关闭下降沿(只要上升沿)
/* ⑤ EXTI:允许 EXTI0 中断(取消屏蔽)*/
EXTI->IMR |= (1 << 0); // 第0位置1 = 允许EXTI0中断
/* ⑥⑦ NVIC:使能 + 设优先级 */
NVIC_SetPriority(EXTI0_IRQn, 2); // 优先级 = 2
NVIC_EnableIRQ(EXTI0_IRQn); // 使能 EXTI0 中断
3.5 中断向量表(STM32F103C8T6 常用部分)
STM32F103C8T6 共有 60 个可屏蔽中断,以下是最常用的:
中断号
中断源
说明
6
EXTI0
外部中断线 0
7
EXTI1
外部中断线 1
8
EXTI2
外部中断线 2
9
EXTI3
外部中断线 3
10
EXTI4
外部中断线 4
23
EXTI9_5
外部中断线 5-9(共享)
25
TIM1_UP
定时器 1 更新中断
28
TIM2
定时器 2 全局中断
29
TIM3
定时器 3 全局中断
30
TIM4
定时器 4 全局中断
37
USART1
串口 1 全局中断
38
USART2
串口 2 全局中断
40
EXTI15_10
外部中断线 10-15(共享)
⚠️ EXTI5-9 和 EXTI10-15 是共享中断:EXTI0 ~ EXTI4 每条线有独立的中断服务函数,但 EXTI5-9 共享一个 ISR,EXTI10-15 共享一个 ISR。在 ISR 内部需要手动判断是哪条线触发的。
ℹ️ 什么是 ISR?
ISR = Interrupt Service Routine(中断服务程序),就是中断发生后 CPU 跳去执行的那个函数。
在 STM32 HAL 库中,ISR 就是那些以 _IRQHandler 结尾的函数,比如 EXTI0_IRQHandler()、USART1_IRQHandler()。
IRQHandler = Interrupt Request Handler(中断请求处理程序),是 STM32 中断服务程序(ISR)的标准化命名。
| 术语 | 来源 | 说的是同一个东西 |
| :--- | :--- | :---: |
| **ISR** | 通用计算机术语 | ✅ |
| **IRQHandler** | ARM CMSIS / STM32 命名规范 | ✅ |
3.6 EXTI 线分组与 ISR
EXTI 线
中断服务函数名
特点
EXTI0
EXTI0_IRQHandler
独立 ISR
EXTI1
EXTI1_IRQHandler
独立 ISR
EXTI2
EXTI2_IRQHandler
独立 ISR
EXTI3
EXTI3_IRQHandler
独立 ISR
EXTI4
EXTI4_IRQHandler
独立 ISR
EXTI5-9
EXTI9_5_IRQHandler
共享,需判断具体线
EXTI10-15
EXTI15_10_IRQHandler
共享,需判断具体线
共享 ISR 内需要这样判断:
void EXTI9_5_IRQHandler(void)
{
if (EXTI->PR & (1 << 5)) // 是 EXTI5 触发的?
{
EXTI->PR = (1 << 5); // 清除标志
// 处理 EXTI5 的逻辑
}
if (EXTI->PR & (1 << 6)) // 是 EXTI6 触发的?
{
EXTI->PR = (1 << 6);
// 处理 EXTI6 的逻辑
}
// ... EXTI7, EXTI8, EXTI9 同理
}
4. USART 串口中断
EXTI 是外部中断(GPIO 电平变化触发),而 USART 中断是内部外设中断——串口硬件自己检测到事件后通知 CPU。
4.1 USART 常见中断源
中断标志
含义
典型场景
RXNE
接收数据寄存器非空
收到 1 字节数据,最常用
TXE
发送数据寄存器空
可以写入下一字节
TC
发送完成
最后一个字节完全发出(含停止位)
IDLE
空闲线检测
一帧数据接收完毕(配合 DMA 用)
ORE
溢出错误
数据没及时取走,被覆盖了
💡 最常用的两个
逐字节接收:用 RXNE 中断,每收 1 字节进一次中断
不定长数据接收:用 IDLE 中断 + DMA,一整帧数据到齐后才进一次中断,效率高得多
4.2 中断接收 vs 轮询 vs DMA
方式
CPU 占用
适合场景
实时性
轮询
最高
简单测试、数据量极少
高
RXNE 中断
中等
逐字节处理、命令解析
高
DMA + IDLE
最低
大数据量、不定长协议
中
方式一:轮询(阻塞等待)—— CPU 在 while 循环中傻等 RXNE 标志,期间什么也干不了。
方式二:中断接收(RXNE)—— 主程序正常跑,收到 1 字节时 USART1_IRQHandler() 触发,读取 DR 存入 buffer,然后回到主程序。CPU 只在有数据时才处理。
方式三:DMA + IDLE 中断(最高效)—— DMA 自动搬运数据到 buffer,CPU 完全不参与。对方停止发送后 IDLE 中断触发,一帧数据全部到齐,再统一处理。
4.3 HAL 库串口中断代码
CubeMX 配置 USART1 中断接收后,代码调用链和 EXTI 类似:
USART1_IRQHandler() ← stm32f1xx_it.c
→ HAL_UART_IRQHandler(&huart1) ← HAL 库内部(检查标志+清除)
→ HAL_UART_RxCpltCallback() ← 🎯 你写代码的地方
/* --- 启动中断接收(只需调用一次) --- */
// 在 main() 初始化后调用,告诉 HAL 库"准备接收 1 字节"
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
/* --- 接收完成回调 --- */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// rx_byte 就是刚收到的数据
rx_buffer[rx_index++] = rx_byte;
// ⚡ 重新开启接收(重要!不调这句就只收一次)
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
⚠️ 必须重新调用 HAL_UART_Receive_IT():HAL 库的中断接收是一次性的——收完指定字节数就停了。在回调里必须再次调用,否则后续数据收不到。这是新手最容易踩的坑!
5. CubeMX 生成的代码执行流程
用 STM32CubeMX 配置 EXTI 中断后,生成的 HAL 库代码执行路径如下:
CubeMX中断代码执行流程
⚠️ 你只需要重写 Callback:HAL 库已经帮你完成了标志检查和清除,你只需要在 HAL_GPIO_EXTI_Callback() 里写业务逻辑。这个函数在 HAL 库中是 __weak 定义的,你重写即可覆盖。
5.1 CubeMX 配置步骤
Pinout 视图:选中 GPIO 引脚 → 设为 GPIO_EXTIx
GPIO 配置:
GPIO mode:选择触发方式(Rising / Falling / Rising Falling)
GPIO Pull-up/Pull-down:根据电路选择上拉/下拉/无
NVIC 配置:
勾选对应的 EXTI 中断 Enabled
设置抢占优先级和响应优先级
生成代码 → 在 main.c 或单独文件中重写 Callback
5.2 完整代码示例
/* --- CubeMX 自动生成的初始化 (gpio.c) --- */
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE(); // 开启 GPIOA 时钟
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); // 分组 2
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); // 抢占1, 响应0
HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 使能中断
}
/* --- 中断服务函数 (stm32f1xx_it.c) --- */
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
/* --- 你的回调函数 (main.c) --- */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转 LED
}
}
6. 中断使用注意事项
⚠️ ISR 中不要做耗时操作:中断服务函数应该尽快执行完毕,不要在里面写延时、printf、大量计算。推荐做法是设置一个标志位,在主循环中处理。
⚠️ 别忘了清中断标志:使用 HAL 库时 HAL_GPIO_EXTI_IRQHandler() 会自动清除,但如果直接操作寄存器,必须手动清除 PR 寄存器,否则中断会反复进入。
💡 按键消抖
按键产生的中断容易触发多次(机械抖动),常见方案:
硬件消抖:RC 滤波电路
软件消抖:ISR 中记录时间戳,两次中断间隔 < 50ms 就忽略
```c
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
static uint32_t last_tick = 0;
if (HAL_GetTick() - last_tick < 50) return; // 50ms 消抖
last_tick = HAL_GetTick();
// 正式处理逻辑...
}
```
💡 中断优先级分组只设置一次:HAL_NVIC_SetPriorityGrouping() 在整个项目中只应调用一次(通常在 HAL_Init() 或 SystemInit() 中)。重复设置可能导致优先级混乱。
7. 总结速查
📋 中断系统三大组件
中断源(EXTI / TIM / USART) → NVIC(优先级仲裁) → CPU(执行 ISR)
📋 优先级规则
抢占优先级不同 → 数值小的能打断大的
抢占相同,响应不同 → 同时到达时数值小的先执行
都相同 → 中断向量号小的先执行
📋 EXTI 线分组
EXTI0~4 → 各自独立 ISR(5 个函数)
EXTI5~9 → 共享 EXTI9_5_IRQHandler()
EXTI10~15 → 共享 EXTI15_10_IRQHandler()
📋 HAL 库调用链
EXTI:IRQHandler → HAL_GPIO_EXTI_IRQHandler → HAL_GPIO_EXTI_Callback
USART:IRQHandler → HAL_UART_IRQHandler → HAL_UART_RxCpltCallback
📋 串口接收三种方式
轮询(傻等)→ 中断 RXNE(逐字节)→ DMA + IDLE(最高效)