一、前言
GPIO 外部中断(EXTI)是嵌入式最常用的异步信号采集方案,常用于按键、光电传感器、脉冲信号、外部告警信号捕获。
新手常见疑难问题:
- 电平触发模式下中断反复进入,无法退出;
- 多个外部中断同时触发,高优先级无法抢占低优先级;
- 中断服务函数内执行耗时代码,系统卡顿;
- 外部信号干扰导致中断频繁误触发;
- 搞不懂抢占优先级、响应优先级,不知道如何配置中断嵌套。
重要前提:STM32F1 系列,同一 GPIO 引脚号共享一条 EXTI 线
PA0/PB0/PC0 → EXTI0;PA1/PB1 → EXTI1...... 因此不能同时开启同引脚号外部中断。
二、EXTI 硬件基础与四种触发方式
2.1 触发模式详解
- 下降沿触发 GPIO_MODE_IT_FALLING
信号从高电平→低电平瞬间触发。
适用:上拉按键、NPN 传感器、低电平脉冲信号。 - 上升沿触发 GPIO_MODE_IT_RISING
信号从低电平→高电平瞬间触发。
适用:外部上电信号、上升沿脉冲。 - 双边沿触发 GPIO_MODE_IT_RISING_FALLING
电平跳变任意方向均触发。
适用:正交编码器、双向脉冲信号。 - 电平触发(EXTI 硬件支持,HAL 库不直接封装,不推荐裸机使用)
只要维持对应电平就持续申请中断,极易造成死循环 。
✅项目规范:裸机开发一律使用边沿触发,杜绝电平触发。
关键区别:边沿触发只在跳变瞬间产生一次中断请求;电平触发只要电平满足就持续请求中断。
三、NVIC 核心概念:抢占优先级 & 响应优先级(中断嵌套关键)
STM32 中断分组决定优先级划分规则(常用中断分组 2)
- 抢占优先级(PreemptionPriority):能否打断正在运行的中断(实现嵌套)
- 响应优先级(SubPriority) :同抢占优先级下,谁先执行;不能实现嵌套
嵌套成立条件:
新中断抢占优先级 < 当前正在执行中断抢占优先级
抢占优先级数值越小,优先级越高。
示例(分组 2):
EXTI0:抢占优先级 1,子优先级 0
EXTI1:抢占优先级 0,子优先级 0
EXTI1 触发时可以打断 EXTI0,实现中断嵌套。
⚠️大坑:
两个中断抢占优先级相同,无论子优先级如何,无法相互抢占,不存在嵌套。
四、项目方案设计
硬件定义
KEY1:PA0 下降沿外部中断 EXTI0
KEY2:PA1 下降沿外部中断 EXTI1
逻辑设计:
- KEY1 中断:普通任务,模拟较长耗时处理;
- KEY2 中断:更高优先级,能够打断 KEY1 的中断服务函数,验证中断嵌套;
- 中断内仅标记事件,复杂业务放到主循环处理(嵌入式标准规范);
- 模块化分层,引脚统一管理,代码便于移植。
五、完整模块化代码(HAL 库)
hardware_gpio.h
#ifndef __HARDWARE_GPIO_H
#define __HARDWARE_GPIO_H
#include "stm32f1xx_hal.h"
#define KEY1_PORT GPIOA
#define KEY1_PIN GPIO_PIN_0
#define KEY2_PORT GPIOA
#define KEY2_PIN GPIO_PIN_1
//中断事件标志
extern uint8_t key1_int_flag;
extern uint8_t key2_int_flag;
#endif
exti_gpio.h
#ifndef __EXTI_GPIO_H
#define __EXTI_GPIO_H
void EXTI_Key_Init(void);
#endif
exti_gpio.c
#include "exti_gpio.h"
#include "hardware_gpio.h"
uint8_t key1_int_flag = 0;
uint8_t key2_int_flag = 0;
void EXTI_Key_Init(void)
{
GPIO_InitTypeDef gpio_conf = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
//PA0 下降沿中断
gpio_conf.Pin = KEY1_PIN;
gpio_conf.Mode = GPIO_MODE_IT_FALLING;
gpio_conf.Pull = GPIO_PULLUP;
HAL_GPIO_Init(KEY1_PORT, &gpio_conf);
//PA1 下降沿中断(更高抢占优先级,支持嵌套)
gpio_conf.Pin = KEY2_PIN;
gpio_conf.Mode = GPIO_MODE_IT_FALLING;
gpio_conf.Pull = GPIO_PULLUP;
HAL_GPIO_Init(KEY2_PORT, &gpio_conf);
//配置NVIC 中断分组2
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
//EXTI0:抢占优先级1,子优先级0
HAL_NVIC_SetPriority(EXTI0_IRQn,1,0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
//EXTI1:抢占优先级0,子优先级0(更高优先级,可嵌套)
HAL_NVIC_SetPriority(EXTI1_IRQn,0,0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);
}
stm32f1xx_it.c 中断服务函数
#include "hardware_gpio.h"
//EXTI0中断服务函数
void EXTI0_IRQHandler(void)
{
if(HAL_GPIO_EXTI_GetHandle(&hgpio,KEY1_PIN))
{
HAL_GPIO_EXTI_Callback(KEY1_PIN);
}
}
//EXTI1中断服务函数
void EXTI1_IRQHandler(void)
{
if(HAL_GPIO_EXTI_GetHandle(&hgpio,KEY2_PIN))
{
HAL_GPIO_EXTI_Callback(KEY2_PIN);
}
}
//外部中断回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KEY1_PIN)
{
key1_int_flag = 1;
}
else if(GPIO_Pin == KEY2_PIN)
{
key2_int_flag = 1;
}
}
main.c 主循环业务处理
#include "stm32f1xx_hal.h"
#include "exti_gpio.h"
#include "hardware_gpio.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
EXTI_Key_Init();
while(1)
{
if(key1_int_flag)
{
key1_int_flag = 0;
//此处放置KEY1业务逻辑
}
if(key2_int_flag)
{
key2_int_flag = 0;
//此处放置KEY2紧急业务逻辑
}
}
}
六、工程硬性规范(量产项目必须遵守)
- 中断回调函数只做标记,禁止复杂运算、长延时
中断执行越短越好,耗时业务转移至主循环; - 外部按键中断务必开启内部上拉,减少外部干扰;
- 工业环境信号建议外部增加 RC 滤波,防止高频毛刺频繁触发中断;
- 同一 EXTI 线只能使用一个引脚,不要同时启用 PA0、PB0 外部中断;
- 需要中断嵌套,必须合理拉开抢占优先级差距;
- 尽量统一项目全局中断分组,不要多处代码修改 NVIC 分组。
七、高频踩坑汇总
坑 1:中断反复不断触发
原因:电平触发模式、信号有抖动毛刺、无滤波。
解决:切换边沿触发,硬件增加 RC 滤波或软件短时标志屏蔽。
坑 2:高优先级中断无法抢占低优先级(不能嵌套)
原因:抢占优先级相同,仅子优先级不同。
解决:调整 PreemptionPriority 数值,拉开层级。
坑 3:中断进不去
排查顺序:时钟开启 → GPIO 模式配置 → NVIC 使能 IRQ → 信号电平变化方向与触发方式匹配。
坑 4:多个引脚共用一条 EXTI 线导致异常
例如同时开启 PA0 和 PB0 外部中断,中断号冲突,出现随机错乱。
八、拓展适配场景
- 光电开关、流量脉冲采集(双边沿 / 下降沿中断)
- 设备外部紧急告警输入(设置最高抢占优先级)
- 编码器脉冲捕获(双边沿外部中断简易方案)
- 485 总线接收唤醒信号
九、全文总结
- 裸机项目优先使用边沿触发,远离电平触发;
- 中断嵌套由抢占优先级决定,响应优先级仅用于同级仲裁;
- 中断服务函数遵循 "快进快出" 原则,只置标志;
- EXTI 引脚存在硬件分组限制,同编号引脚不能同时开启外部中断;
- NVIC 中断分组全局唯一,项目内统一配置,避免多处冲突。