9.STM32 中断系统详解:从基础概念到 EXTI 实战

中断是嵌入式开发中最重要的基础概念之一,也是 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 中断处理流程

  1. 中断请求:外设或外部事件产生中断信号,置位对应中断标志位。

  2. 中断使能判断:NVIC 检查该中断是否被使能,以及 CPU 当前是否允许响应中断(PRIMASK 等)。

  3. 优先级仲裁:若同时有多个中断请求,NVIC 按优先级高低决定先响应哪个。

  4. 保存现场:CPU 自动将 xPSR、PC、LR、R12、R3-R0 压入栈中(硬件自动完成)。

  5. 取向量:从向量表中取出对应中断服务函数的入口地址,跳转执行。

  6. 执行中断服务函数(ISR):用户编写的中断处理逻辑,通常需要清除中断标志位。

  7. 恢复现场:执行完 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 中断不触发的排查清单

  1. 时钟是否使能?(GPIO、AFIO/外设时钟)

  2. GPIO 模式是否正确配置为输入?

  3. EXTI 线映射是否正确?(GPIO_EXTILineConfig)

  4. 触发方式是否与实际信号匹配?(上升沿/下降沿)

  5. NVIC 是否使能了对应中断通道?

  6. 中断服务函数名是否正确?(检查 startup 文件)

  7. 全局中断是否被关闭?(__disable_irq() 后忘记 __enable_irq())

  8. 引脚是否有实际信号输入?(用万用表/示波器测量)

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 中断优先级详解」等章节,配有完整的工程源码和图文讲解,适合入门实践时对照学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值