STM32 中断系统详解:NVIC、EXTI、优先级分组与 HAL 库实战
- 嵌入式开发
- 6小时前
- 15热度
- 0评论
STM32 中断系统详解
1. 什么是中断?
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 工作原理

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 |
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 先执行
- 如果抢占和响应都相同?→ 比中断向量号(硬件编号小的优先)
2.5 NVIC 使用步骤(HAL 库)

| 步骤 | 操作 | 寄存器 | 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 的映射关系

每条 EXTI 线对应同一编号的所有端口引脚,但同一时刻只能选一个。每组(如 PA0/PB0/PC0)经 AFIO 选择器后只有一个能连到 EXTI0,以此类推。
3.2 EXTI 信号流程
从 GPIO 引脚到 CPU 响应中断,经过以下步骤:

框图中有 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 恢复现场,回到主程序 | — |
- 中断(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 触发) |
EXTI_RTSR(上升沿)、EXTI_FTSR(下降沿)、EXTI_PR(挂起标志)、EXTI_IMR(中断屏蔽)。



寄存器直接操作:
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(共享) |
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);
}
}
5. CubeMX 生成的代码执行流程
用 STM32CubeMX 配置 EXTI 中断后,生成的 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 - 设置抢占优先级和响应优先级
- 勾选对应的 EXTI 中断
- 生成代码 → 在
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. 中断使用注意事项
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)
- 抢占优先级不同 → 数值小的能打断大的
- 抢占相同,响应不同 → 同时到达时数值小的先执行
- 都相同 → 中断向量号小的先执行
- EXTI0~4 → 各自独立 ISR(5 个函数)
- EXTI5~9 → 共享
EXTI9_5_IRQHandler() - EXTI10~15 → 共享
EXTI15_10_IRQHandler()
- EXTI:IRQHandler → HAL_GPIO_EXTI_IRQHandler →
HAL_GPIO_EXTI_Callback - USART:IRQHandler → HAL_UART_IRQHandler →
HAL_UART_RxCpltCallback
轮询(傻等)→ 中断 RXNE(逐字节)→ DMA + IDLE(最高效)