中断是嵌入式开发中最重要的基础概念之一,也是 STM32 从入门到进阶必须掌握的核心能力。本文面向 STM32 初学者,从零开始系统讲解中断的完整知识体系:先介绍中断的基础概念与作用,再深入 STM32 中断系统架构(NVIC、中断向量表、处理流程),接着讲解中断优先级的配置方法,最后通过外部中断(EXTI)实战带你完整走通标准库和 HAL 库的配置流程,并给出按键控制 LED 的完整工程示例。
建议阅读顺序:如果你是第一次接触中断,请按章节顺序依次阅读,先理解「什么是中断」和「中断执行过程」,再进入架构与优先级部分;如果你已有一定基础,可以直接跳到「五、中断配置完整步骤」和「七、常见问题与注意事项」对照实践。文中所有代码示例基于 STM32F103 、STM32F411系列,建议边读边在开发板上动手验证,遇到问题可随时回看对应章节的排查清单。
1.1 什么是中断
中断(Interrupt)是指 CPU 在正常执行程序的过程中,由于内部或外部事件的触发,暂时中止当前程序的执行,转而去处理该事件对应的服务程序,处理完毕后再回到原程序被打断的位置继续执行的过程。
类比理解:你正在看书(主程序),电话铃响了(中断触发),你在书页上夹个书签(保存现场),去接电话(中断服务函数),接完电话后回到书签位置继续看书(恢复现场、返回主程序)。

1.2 中断的作用
-
提高 CPU 效率:避免 CPU 不断轮询外设状态,让 CPU 在等待时处理其他任务。
-
实时响应:紧急事件可以立即得到处理,满足实时系统的要求。
-
实现多任务调度:操作系统利用 SysTick 等定时器中断实现任务切换。
-
低功耗支持:CPU 可进入休眠模式,由中断唤醒。
1.3 中断 vs 轮询
|
对比项 |
中断方式 |
轮询方式 |
|---|---|---|
|
CPU 占用 |
低,事件到来时才响应 |
高,持续查询状态 |
|
响应速度 |
快,实时性好 |
取决于轮询周期 |
|
实现复杂度 |
较高,需配置中断和服务函数 |
简单,循环判断即可 |
|
适用场景 |
实时性要求高、事件频率低 |
事件频繁、实时性要求低 |
1.4 中断执行过程

二、STM32 中断系统架构




EXTI 中断线一共 23 个,其中外部 GPIO 的中断有 16 个。

EXTI流程
EXTI 可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不同。
首先我们来看图中红色虚线指示的电路流程。它是一个产生中断的线路,最终信号流入到 NVIC 控制器内。
编号 1 是输入线,EXTI 控制器有 19 个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个 GPIO,也可以是一些外设的事件,这部分内容我们将在后面专门讲解。输入线一般是存在电平变化的信号。
编号 2 是一个边沿检测电路,它会根据上升沿触发选择寄存(EXTI_RTSR)和下降沿触发选择寄存器(EXTI_FTSR)对应位的设置来控制信号触发。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号 1 给编号 3 电路,否则输出无效信号0。而 EXTI_RTSR 和 EXTI_FTSR 两个寄存器可以控制器需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。
编号 3 电路实际就是一个或门电路,它一个输入来自编号 2 电路,另外一个输入来自软件中断事件寄存器(EXTI_SWIER)。EXTI_SWIER允许我们通过程序控制就可以启动中断/事件线,这在某些地方非常有用。我们知道或门的作用就是有 1 就为 1,所以这两个输入随便一个有有效信号 1就可以输出 1 给编号 4和编号 6电路。
编号 4 电路是一个与门电路,它一个输入是编号 3 电路,另外一个输入来自中断屏蔽寄存器(EXTI_IMR)。与门电路要求输入都为 1 才输出 1,导致的结果是如果 EXTI_IMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,最终编号 4 电路输出的信号都为 0;
如果EXTI_IMR设置为1时,最终编号4电路输出的信号才由编号3电路的输出信号决定,这样我们可以简单的控制 EXTI_IMR 来实现是否产生中断的目的。编号 4 电路输出的信号会被保存到挂起寄存器(EXTI_PR)内,如果确定编号 4 电路输出为 1 就会把 EXTI_PR 对应位置 1。
编号 5 是将 EXTI_PR 寄存器内容输出到 NVIC 内,从而实现系统中断事件控制。
接下来我们来看看绿色虚线指示的电路流程。它是一个产生事件的线路,最终输出一个脉冲信号。
产生事件线路是在编号3电路之后与中断线路有所不同,之前电路都是共用的。
编号6电路是一个与门,它一个输入来自编号 3 电路,另外一个输入来自事件屏蔽寄存器(EXTI_EMR)。如果 EXTI_EMR设置为 0时,那不管编号 3电路的输出信号是 1还是 0,最终编号 6 电路输出的信号都为 0;如果 EXTI_EMR 设置为 1 时,最终编号 6 电路输出的信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_EMR 来实现是否产生
事件的目的。
编号 7 是一个脉冲发生器电路,当它的输入端,即编号 6 电路的输出端,是一个有效信号 1 时就会产生一个脉冲;如果输入端是无效信号就不会输出脉冲。
编号 8 是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如定时器 TIM、模拟数字转换器 ADC 等等,这样的脉冲信号一般用来触发 TIM 或者 ADC开始转换。
产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。
另外,EXTI是在 APB2总线上的,在编程时候需要注意到这点。
2.1 NVIC 嵌套向量中断控制器
STM32(基于 Cortex-M 内核)的中断管理由 NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)负责。NVIC 是内核的一部分,与 CPU 紧密耦合,支持:
-
可嵌套中断(高优先级中断可打断低优先级中断)
-
向量中断(自动定位中断服务函数入口,无需软件查询)
-
动态优先级调整
-
中断屏蔽(除 HardFault 外,所有中断均可被屏蔽)
2.2 中断向量表
中断向量表是一个存放中断服务函数入口地址的数组,位于存储器起始地址(默认 0x08000000)。每个中断源对应一个固定的向量编号和偏移地址。
以 STM32F103 为例,向量表前几项如下:
|
向量编号 |
中断源 |
说明 |
|---|---|---|
|
0 |
— |
初始栈顶地址(MSP) |
|
1 |
Reset |
复位中断,程序入口 |
|
2 |
NMI |
不可屏蔽中断 |
|
3 |
HardFault |
硬件错误中断 |
|
4-10 |
MemManage / BusFault / UsageFault 等 |
内核异常 |
|
11 |
SVCall |
系统服务调用 |
|
14 |
PendSV |
可挂起系统调用(常用于 OS 任务切换) |
|
15 |
SysTick |
系统滴答定时器 |
|
16+ |
IRQn(外部中断) |
外设中断,如 EXTI、USART、TIM 等 |
编号 0-15 为内核异常(部分可配置优先级),编号 16 及以后为外部中断(IRQ),由片上外设触发。
2.3 中断处理流程
-
中断请求:外设或外部事件产生中断信号,置位对应中断标志位。
-
中断使能判断:NVIC 检查该中断是否被使能,以及 CPU 当前是否允许响应中断(PRIMASK 等)。
-
优先级仲裁:若同时有多个中断请求,NVIC 按优先级高低决定先响应哪个。
-
保存现场:CPU 自动将 xPSR、PC、LR、R12、R3-R0 压入栈中(硬件自动完成)。
-
取向量:从向量表中取出对应中断服务函数的入口地址,跳转执行。
-
执行中断服务函数(ISR):用户编写的中断处理逻辑,通常需要清除中断标志位。
-
恢复现场:执行完 ISR 后,CPU 从栈中弹出之前保存的寄存器,恢复到被打断的位置继续执行。
三、中断优先级
3.1 两级优先级机制
Cortex-M 内核的中断优先级分为两个层级:
-
抢占优先级(Preemption Priority):决定中断是否可以嵌套。高抢占优先级的中断可以打断正在执行的低抢占优先级中断。
-
子优先级(Sub Priority / 响应优先级):当两个中断的抢占优先级相同时,子优先级高的先响应。但子优先级不能实现中断嵌套。
关键规则:只有抢占优先级不同才能嵌套;抢占优先级相同、子优先级不同时,高优先级先执行但不能打断正在执行的同抢占级中断。
3.2 优先级分组
STM32 使用 4 位(共 16 级)来表示中断优先级,通过优先级分组决定这 4 位中几位用于抢占优先级、几位用于子优先级。
|
优先级分组 |
抢占优先级位数 |
子优先级位数 |
抢占优先级范围 |
子优先级范围 |
|---|---|---|---|---|
|
NVIC_PriorityGroup_0 |
0 位 |
4 位 |
0(仅1级) |
0-15 |
|
NVIC_PriorityGroup_1 |
1 位 |
3 位 |
0-1 |
0-7 |
|
NVIC_PriorityGroup_2 |
2 位 |
2 位 |
0-3 |
0-3 |
|
NVIC_PriorityGroup_3 |
3 位 |
1 位 |
0-7 |
0-1 |
|
NVIC_PriorityGroup_4 |
4 位 |
0 位 |
0-15 |
0(仅1级) |
数值越小,优先级越高!例如抢占优先级 0 高于抢占优先级 2。整个工程中优先级分组通常只设置一次,建议在系统初始化时统一配置。
3.3 优先级配置(标准库)
// 设置优先级分组(整个工程只调用一次)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占,2位子
// 配置具体中断的优先级
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; // 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能
NVIC_Init(&NVIC_InitStructure);
3.4 优先级配置(HAL 库)
// 设置优先级分组(整个工程只调用一次)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 配置具体中断的优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 1); // (IRQn, 抢占优先级, 子优先级)
HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 使能中断
四、外部中断/事件控制器(EXTI)
4.1 EXTI 概述
EXTI(External Interrupt/Event Controller,外部中断/事件控制器)负责管理 GPIO 引脚的外部中断和事件。它支持:
-
每个 EXTI 线可独立配置触发方式(上升沿、下降沿、双边沿)
-
每个 EXTI 线可独立使能或屏蔽
-
产生中断请求或事件请求(事件用于唤醒低功耗模式,不进入 ISR)
-
软件触发中断/事件
4.2 EXTI 线与 GPIO 映射
STM32F1 系列共有 19 条 EXTI 线(EXTI0-EXTI18),其中 EXTI0-EXTI15 对应 GPIO 引脚:
|
EXTI 线 |
对应 GPIO 引脚 |
中断向量 |
|---|---|---|
|
EXTI0 |
Px0(PA0/PB0/PC0...) |
EXTI0_IRQn |
|
EXTI1 |
Px1 |
EXTI1_IRQn |
|
EXTI2 |
Px2 |
EXTI2_IRQn |
|
EXTI3 |
Px3 |
EXTI3_IRQn |
|
EXTI4 |
Px4 |
EXTI4_IRQn |
|
EXTI5-EXTI9 |
Px5-Px9 |
EXTI9_5_IRQn(共用) |
|
EXTI10-EXTI15 |
Px10-Px15 |
EXTI15_10_IRQn(共用) |
|
EXTI16 |
PVD 输出 |
PVD_IRQn |
|
EXTI17 |
RTC 闹钟事件 |
RTCAlarm_IRQn |
|
EXTI18 |
USB 唤醒事件 |
USBWakeUp_IRQn |
重要限制:同一编号的引脚(如 PA0、PB0、PC0)在同一时刻只能有一个映射到 EXTI0。也就是说,EXTIx 只能选择一个端口的第 x 号引脚。EXTI5-9 和 EXTI10-15 共用中断向量,需要在 ISR 中通过标志位判断具体是哪条线触发的。
4.3 触发方式
-
上升沿触发:信号从低电平跳变到高电平时触发。
-
下降沿触发:信号从高电平跳变到低电平时触发。
-
双边沿触发:上升沿和下降沿均触发。
五、中断配置完整步骤
以"PA0 引脚下降沿触发外部中断"为例,完整配置流程如下:
5.1 标准库配置流程
步骤 1:使能相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 复用功能时钟(EXTI 需要)
步骤 2:配置 GPIO 为输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入(按键接 GND 时用)
GPIO_Init(GPIOA, &GPIO_InitStructure);
步骤 3:配置 EXTI 线映射(选择端口)
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // PA0 映射到 EXTI0
步骤 4:配置 EXTI 触发方式和使能
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式(非事件模式)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
步骤 5:配置 NVIC 优先级并使能
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 通常在 main 开头统一设置
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
步骤 6:编写中断服务函数
void EXTI0_IRQHandler(void)
{
// 第一步:判断中断标志位是否置位(必须!)
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 中断处理逻辑
// 例如:翻转 LED、发送数据、设置标志位等
// 最后一步:清除中断标志位(必须!否则会反复进入中断)
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
5.2 HAL 库配置流程
// 1. 使能时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 2. 配置 GPIO
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断(HAL 库一步配置 GPIO+EXTI)
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置 NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
// 4. 中断服务函数(HAL 库风格)
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); // HAL 库统一入口,内部清标志
}
// 5. 回调函数(实际处理逻辑写在这里)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// 中断处理逻辑
}
}
5.3 HAL库按键控制LED实验
配置 GPIO 引脚 PA0 为 EXTI 复用功能

GPIO 配置

NVIC 使能

-
配置中断优先级

找中断向量表——中断服务程序

-
跳转到中断服务程序

-
跳转

-
再跳转中断回调函数

-
中断回调函数里写

-
消抖

5.4 完整工程示例
下面给出一个基于 HAL 库的按键中断控制 LED 的完整 main.c 示例。硬件连接:按键接 PA0(另一端接 GND,使用上拉输入),LED 接 PB0(低电平点亮)。按下按键触发下降沿中断,在回调函数中翻转 LED 状态,并加入简单的软件消抖。
#include "main.h"
#include "gpio.h"
/* 按键消抖计数:在回调中置位,主循环中做延时消抖 */
volatile uint8_t key_pressed_flag = 0;
/**
@brief 系统时钟配置(此处以 72MHz 为例,实际由 SystemClock_Config 生成)
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/* 使能 HSE 外部高速晶振 /
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; / 8MHz * 9 = 72MHz */
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/* 配置 AHB、APB1、APB2 总线时钟分频 /
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; / APB1 = 36MHz /
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; / APB2 = 72MHz */
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/**
@brief GPIO 初始化:PA0 按键输入(下降沿中断),PB0 LED 输出
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 使能 GPIOA、GPIOB 时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 配置 PA0 为下降沿中断输入,上拉(按键接 GND) /
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; / 下降沿触发中断 /
GPIO_InitStruct.Pull = GPIO_PULLUP; / 上拉输入 */
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* 配置 PB0 为推挽输出,用于驱动 LED */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* 默认熄灭 LED(PB0 输出高电平) */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
}
/**
@brief NVIC 初始化:使能 EXTI0 中断并设置优先级
/
static void MX_NVIC_Init(void)
{
/ 设置优先级分组:2 位抢占优先级 + 2 位子优先级 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
/* 配置 EXTI0 中断:抢占优先级 1,子优先级 1 */
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
/**
@brief 主函数
/
int main(void)
{
/ 复位所有外设,初始化 Flash 接口和 SysTick */
HAL_Init();
/* 配置系统时钟为 72MHz */
SystemClock_Config();
/* 初始化 GPIO 和 NVIC */
MX_GPIO_Init();
MX_NVIC_Init();
while (1)
{
/* 主循环:检测到按键标志后做软件消抖 /
if (key_pressed_flag)
{
/ 延时 20ms 跳过机械抖动区间 */
HAL_Delay(20);
/* 再次确认按键确实被按下(低电平有效) */
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
{
/* 翻转 LED 状态 */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
}
/* 清除按键标志,等待下一次按下 */
key_pressed_flag = 0;
}
}
}
/**
@brief EXTI0 中断服务函数(HAL 库统一入口,内部自动清除标志位)
*/
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
/**
@brief EXTI 中断回调函数:实际处理逻辑写在这里
@param GPIO_Pin 触发中断的引脚
/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
/ 置位按键标志,由主循环完成消抖和 LED 翻转 */
key_pressed_flag = 1;
}
}
/**
@brief 错误处理函数:程序出错时进入死循环
*/
void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}
代码要点说明:
- 时钟配置:通过
SystemClock_Config()将系统时钟配置为 72MHz,APB2 总线(GPIO、EXTI 所在总线)为 72MHz。 - GPIO 初始化:PA0 配置为
GPIO_MODE_IT_FALLING下降沿中断模式并启用上拉,PB0 配置为推挽输出驱动 LED。 - NVIC 配置:使用
HAL_NVIC_SetPriorityGrouping()设置优先级分组,再通过HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()配置并使能 EXTI0 中断。 - 中断服务函数:
EXTI0_IRQHandler()调用HAL_GPIO_EXTI_IRQHandler(),HAL 库内部自动清除中断标志位。 - 回调函数:
HAL_GPIO_EXTI_Callback()中只置位标志位,不在中断中做耗时操作,符合"快进快出"原则。 - 软件消抖:主循环检测到标志后延时 20ms,再确认引脚电平,避免机械抖动导致 LED 多次翻转。
六、中断服务函数(ISR)编写规范
6.1 函数命名
中断服务函数的名称是固定的,由启动文件(startup_xxx.s)中的向量表定义决定,不能随意命名。常见的中断服务函数名:
|
中断源 |
服务函数名 |
|---|---|
|
EXTI0 |
EXTI0_IRQHandler |
|
EXTI1 |
EXTI1_IRQHandler |
|
EXTI9_5 |
EXTI9_5_IRQHandler |
|
EXTI15_10 |
EXTI15_10_IRQHandler |
|
USART1 |
USART1_IRQHandler |
|
TIM2 |
TIM2_IRQHandler |
|
SysTick |
SysTick_Handler |
如果函数名写错,中断触发后 CPU 会跳到默认的空循环(Default_Handler)中,表现为程序卡死。务必检查函数名拼写!
6.2 编写原则
-
快进快出:ISR 应尽可能短,避免在中断中执行耗时操作(如延时、串口打印大量数据、复杂算法)。耗时操作应在 ISR 中设置标志位,在主循环中处理。
-
必须清除中断标志位:如果不清除,中断会反复触发,导致 CPU 一直停留在 ISR 中。
-
先判断标志位再处理:对于共用中断向量的情况(如 EXTI9_5),必须先判断是哪个标志位置位,再做对应处理。
-
避免在 ISR 中调用可能阻塞的函数:如 HAL_Delay()(依赖 SysTick,若 SysTick 优先级低于当前中断则会卡死)。
-
共享变量使用 volatile:在 ISR 和主循环之间共享的变量必须声明为
volatile,防止编译器优化导致主循环看不到变量变化。
6.3 共用中断向量的处理
以 EXTI9_5 为例,EXTI5 到 EXTI9 共用同一个中断向量,需要在 ISR 中分别判断:
void EXTI9_5_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line5) != RESET)
{
// EXTI5 的处理逻辑
EXTI_ClearITPendingBit(EXTI_Line5);
}
if (EXTI_GetITStatus(EXTI_Line6) != RESET)
{
// EXTI6 的处理逻辑
EXTI_ClearITPendingBit(EXTI_Line6);
}
// ... EXTI7/8/9 同理
}
七、常见问题与注意事项
7.1 中断不触发的排查清单
-
时钟是否使能?(GPIO、AFIO/外设时钟)
-
GPIO 模式是否正确配置为输入?
-
EXTI 线映射是否正确?(GPIO_EXTILineConfig)
-
触发方式是否与实际信号匹配?(上升沿/下降沿)
-
NVIC 是否使能了对应中断通道?
-
中断服务函数名是否正确?(检查 startup 文件)
-
全局中断是否被关闭?(__disable_irq() 后忘记 __enable_irq())
-
引脚是否有实际信号输入?(用万用表/示波器测量)
7.2 中断抖动问题
机械按键按下/释放时会产生抖动,导致一次按键触发多次中断。解决方案:
-
硬件消抖:按键并联电容(通常 0.1μF)。
-
软件消抖:在 ISR 中检测到中断后,延时 10-20ms 再判断引脚电平,确认后再执行逻辑。但注意 ISR 中不宜用 HAL_Delay,可用定时器或在主循环中消抖。
-
定时器消抖:ISR 中只置标志位,主循环中用定时器做延时消抖判断。
7.3 中断嵌套与优先级冲突
-
确保需要嵌套的中断具有不同的抢占优先级。
-
高优先级中断中不要调用依赖低优先级中断的函数(如在高优先级 ISR 中调用 HAL_Delay,而 SysTick 优先级更低,会导致死等)。
-
FreeRTOS 中,调用 OS API 的中断优先级必须低于或等于
configMAX_SYSCALL_INTERRUPT_PRIORITY定义的优先级(即数值要大于等于该值)。
7.4 中断标志位清除顺序
中断标志位的清除时机直接关系到中断是否会被反复触发,需要根据标志位的类型和中断信号的状态来选择合适的清除时机。
1. 两种清除时机及适用场景
处理前清除:在进入 ISR 后、执行具体处理逻辑之前先清除标志位。这种方式适用于中断信号是边沿触发且信号持续时间较短的场景,例如按键按下产生的下降沿。由于边沿信号在触发后很快消失,先清除标志位不会导致标志位再次置位,还能避免在处理逻辑执行期间被同一次事件再次打断。但它的风险在于:如果中断信号是电平触发且电平持续有效,先清除标志位后,硬件会立即再次置位,导致 ISR 被反复进入,处理逻辑可能永远执行不完。
处理后清除:在 ISR 的处理逻辑全部执行完毕之后,最后再清除标志位。这是最稳妥、最常用的方式,适用于大多数场景,尤其是电平触发或信号持续时间较长的情况。它的好处是:只要标志位还置位,同一次事件就不会再次触发中断,保证处理逻辑能完整执行。它的风险在于:如果在处理逻辑执行期间,又来了新的事件,新事件会被挂起,需要等本次 ISR 退出后才能响应,可能造成一定延迟。
总结:边沿触发且信号短暂,两种方式都可以;电平触发或信号持续有效,必须采用处理后清除,否则会反复进入中断。
2. 以 EXTI 为例的清除操作
标准库:EXTI 的标志位是"写 1 清零"类型,使用 EXTI_ClearITPendingBit() 清除指定线的挂起标志位。
void EXTI0_IRQHandler(void)
{
// 处理逻辑
// ...
// 最后清除标志位(写 1 清零)
EXTI_ClearITPendingBit(EXTI_Line0);
}
HAL 库:HAL 库在 HAL_GPIO_EXTI_IRQHandler() 内部已经自动完成了标志位清除,用户只需在回调函数中编写处理逻辑,无需手动清除。
void EXTI0_IRQHandler(void)
{
// HAL 库统一入口,内部自动清除标志位
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// 处理逻辑,无需手动清标志
}
}
3. 典型错误示例与正确写法
下面以按键控制 LED 为例,演示清除时机不当导致中断反复触发的问题。
错误写法:在 ISR 开头就清除标志位,随后执行耗时较长的处理逻辑。如果按键信号仍处于有效电平,标志位会被立即重新置位,导致 ISR 反复进入,LED 状态不断翻转,表现为按键一次却多次翻转。
// 错误示例:先清标志,再处理
void EXTI0_IRQHandler(void)
{
EXTI_ClearITPendingBit(EXTI_Line0); // 先清除标志位
// 模拟耗时处理(如延时、复杂计算)
for (volatile int i = 0; i < 100000; i++);
// 翻转 LED
GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0)));
}
正确写法:先完成处理逻辑,最后再清除标志位,确保同一次事件只被处理一次。
// 正确示例:先处理,最后清标志
void EXTI0_IRQHandler(void)
{
// 判断标志位是否置位(必须!)
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 翻转 LED
GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0)));
// 最后清除标志位(写 1 清零)
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
注意:对于"读即清零"的标志位(如 USART 的 RXNE),读取数据寄存器即自动清零,无需也不能手动清除,否则可能误清其他标志。
清除标志位的时机很重要:对于"写 1 清零"类标志,建议在处理完中断逻辑之后清除。如果在处理之前清除,而中断信号仍然有效,可能导致标志位再次置位,逻辑还没执行完就被再次打断。但对于某些"读即清零"的标志(如 USART 的 RXNE),读取数据寄存器即自动清零。
7.5 全局中断开关
// 关闭全局中断(除 HardFault、NMI 外所有中断被屏蔽)
__disable_irq(); // 或 PRIMASK = 1
// 打开全局中断
__enable_irq(); // 或 PRIMASK = 0
// 注意:关闭全局中断期间,所有中断请求会被挂起(pending),
// 打开后会按优先级依次响应,不会丢失。
八、其他常用外设中断简介
|
外设 |
常见中断事件 |
中断标志位(标准库) |
|---|---|---|
|
USART |
接收完成、发送完成、空闲总线 |
USART_IT_RXNE / TXE / TC / IDLE |
|
TIM |
更新(溢出)、输入捕获、输出比较 |
TIM_IT_Update / CC1 / CC2 |
|
ADC |
转换完成、注入转换完成、模拟看门狗 |
ADC_IT_EOC / JEOC / AWD |
|
DMA |
传输完成、半传输、传输错误 |
DMA_IT_TC / HT / TE |
|
I2C |
事件中断、错误中断 |
I2C_IT_EVT / ERR |
|
SPI |
接收缓冲区非空、发送缓冲区空、错误 |
SPI_IT_RXNE / TXE / ERR |
|
RTC |
秒中断、闹钟中断、溢出中断 |
RTC_IT_SEC / ALR / OW |
所有外设中断的配置思路都是一致的:使能时钟 → 配置外设 → 使能外设中断位 → 配置 NVIC → 编写 ISR(判断标志 + 处理 + 清标志)。掌握了 EXTI 的配置方法,其他外设中断可以举一反三。
九、串口发送中断
-
使能NVIC


·main函数

·发送完成回调函数

十、参考资料
以下资料可作为深入学习 STM32 中断系统的参考,建议结合本文内容按需查阅。
-
《STM32F10x 参考手册》:其中第 8 章「中断和事件」详细介绍了 EXTI 的寄存器配置、触发方式与事件/中断线路,第 9 章「嵌套向量中断控制器(NVIC)」说明了中断优先级分组、向量表及中断使能的相关寄存器。该手册是 STM32F103 系列开发最权威的硬件参考。
-
《Cortex-M3 权威指南》(Joseph Yiu 著):第 7 章「异常」和第 8 章「NVIC 与中断控制」系统讲解了中断向量表的结构、异常优先级模型、嵌套中断机制以及尾链(Tail-Chaining)等内核级概念,有助于从架构层面理解 STM32 中断的工作方式。
-
STM32 官方标准外设库用户手册:即《STM32F10x Standard Peripheral Library Documentation》,其中对 EXTI、NVIC、GPIO 等外设的驱动函数、数据结构和使用示例做了完整说明,是使用标准库开发时的重要参考。
-
STM32 HAL 库用户手册:即《Description of STM32F1 HAL and Low-layer drivers》,详细介绍了 HAL_GPIO_EXTI_IRQHandler、HAL_NVIC_SetPriority 等中断相关 API 的用法与回调机制,是使用 HAL 库开发时的必备文档。
-
正点原子 / 野火教程:正点原子《STM32F1 开发指南》和野火《STM32F103 霸道/指南者开发指南》中均有「外部中断实验」「NVIC 中断优先级详解」等章节,配有完整的工程源码和图文讲解,适合入门实践时对照学习。


9887

被折叠的 条评论
为什么被折叠?



