STM32 中断系统详解:NVIC、EXTI、优先级分组与 HAL 库实战

STM32 中断系统详解

1. 什么是中断?

CPU 正在执行主程序,突然外部或内部事件发生(按键按下、定时器溢出、串口收到数据……),CPU 暂停当前工作,跳去处理紧急事件,处理完再回来继续。

中断响应与返回流程

💡 核心思想:中断让 CPU 不用"傻等",而是事件驱动——有事才处理,没事干正事。

2. NVIC — 嵌套向量中断控制器

NVIC = Nested Vectored Interrupt Controller,是 Cortex-M3 内核自带的中断管理器,负责:

  1. 接收所有中断请求
  2. 判断优先级,决定谁先执行
  3. 支持嵌套——高优先级中断可以打断低优先级中断

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 里面吗?
  • ISERICERIPR是 NVIC 内部寄存器
  • AIRCRSHPR → 严格来说属于 SCB(System Control Block,系统控制块),但 SCB 控制着 NVIC 的分组和内核中断优先级,两者通常一起讨论
  • NVIC 和 SCB 都位于 Cortex-M3 内核中,与 CPU 紧密协作
ℹ️ 工作过程
  1. 外部中断触发后,先经过 ISER/ICER 判断该中断是否被使能
  2. 使能的中断进入 IPR 寄存器,根据 AIRCR 的分组规则,判断抢占优先级和响应优先级
  3. 最终按优先级高低依次送入 CPU 执行
  4. 内核中断(如 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 普通号 普通号里的排队顺序
📋 优先级判断规则(从高到低)
  1. 抢占优先级(Preemption):高抢占可以打断正在执行的低抢占中断
  2. 响应优先级(Sub-priority):抢占相同时,响应高的先执行,但不能互相打断
  3. 自然优先级:抢占和响应都相同时,按中断向量表编号排序(编号小的优先)
  4. 数值越小 = 优先级越高
举个例子(分组 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 配置步骤

  1. Pinout 视图:选中 GPIO 引脚 → 设为 GPIO_EXTIx
  2. GPIO 配置
    • GPIO mode:选择触发方式(Rising / Falling / Rising Falling)
    • GPIO Pull-up/Pull-down:根据电路选择上拉/下拉/无
  3. NVIC 配置
    • 勾选对应的 EXTI 中断 Enabled
    • 设置抢占优先级和响应优先级
  4. 生成代码 → 在 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)
📋 优先级规则
  1. 抢占优先级不同 → 数值小的能打断大的
  2. 抢占相同,响应不同 → 同时到达时数值小的先执行
  3. 都相同 → 中断向量号小的先执行
📋 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(最高效)