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

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

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(最高效)

相关推荐

为什么贷款平台总显示无额度?5个原因+解决方法
普通物流200斤收费多少(100公斤发什么物流最便宜)
有人被365黑过钱吗

普通物流200斤收费多少(100公斤发什么物流最便宜)

📅 09-15 👍 714
移除游戏的 Steam 验证
有人被365黑过钱吗

移除游戏的 Steam 验证

📅 01-21 👍 338
王者荣耀暃的连招顺序 暃连招顺序公式技巧推荐
365bet体育在线投注注册备

王者荣耀暃的连招顺序 暃连招顺序公式技巧推荐

📅 07-30 👍 693
win11怎么打开一个游戏的根目录
365客服电话

win11怎么打开一个游戏的根目录

📅 08-20 👍 321
AKM AK4377A HiFi芯片性能参数和音质评测
有人被365黑过钱吗

AKM AK4377A HiFi芯片性能参数和音质评测

📅 08-12 👍 778