简介:STM32外部中断EXTI按键控灯实验完整工程资源包,面向单片机初学者和嵌入式入门开发者,解决初学者对中断机制理解不深、轮询浪费资源的问题。内容围绕GPIO初始化、EXTI线配置、NVIC中断优先级设置及中断服务函数编写展开,覆盖按键触发外部中断翻转LED状态的完整流程。
资源共185个文件,压缩包4.75MB,以C源码、H头文件、D/O中间文件、HEX烧录文件及Keil工程文件(uvprojx)为主,含编译生成物和辅助脚本,可直接在MDK环境下打开编译,也可对照源码学习中断配置与硬件映射关系。
目前已有4929人学习下载,适合作为STM32入门阶段的中断实验参考。通过该工程,读者可直观理解EXTI线与GPIO映射、下降沿触发、NVIC优先级分组等核心概念,并复用其中断框架至传感器信号检测、无线接收等场景。
1. 外部中断EXTI:按键轮询换成中断后,单片机不再空转等待
写过按键控制LED的人,第一版多半是while(1)里不停读GPIO_ReadInputDataBit,按下了就翻转。这个写法在只跑一个任务时没问题,可一旦程序里同时要处理串口、屏幕刷帧、传感器采集,轮询按键就会变成一种尴尬的浪费——CPU大多数时间在空转,按键响应也可能被别的耗时操作拖住。单片机领域的EXTI外部中断,解决的就是这个"时机"问题:按键按下那一下,硬件直接给CPU发中断请求,CPU放下手头的事跳去写LED,处理完再回来。这篇以STM32F103的EXTI0为例,把引脚映射、触发边沿、NVIC配置、消抖和避坑完整走一遍,适合刚学完GPIO想入门中断,以及被"按键按了没反应"卡住的人对照排查。
2. EXTI工作机制:边沿触发、引脚映射与NVIC优先级一次理清
2.1 一个EXTI线对应多个引脚,输入先过AFIO选择器
很多新手第一次看EXTI会觉得玄学:明明初始化了GPIO,也开了EXTI,按键就是不触发。原因往往在EXTI的输入结构上——EXTI模块本身只有19条中断/事件线,其中EXTI0到EXTI15对应GPIO的16个引脚,但每条线并不是和某个引脚一一绑定。以EXTI0为例,PA0、PB0、PC0这几个引脚在硬件上都能连到EXTI0,同一时刻只能通过AFIO的EXTICR寄存器选一个进来。
这个"多选一"的机制就是为什么标准库里有个单独的GPIO_EXTILineConfig函数。它的作用不是使能中断,而是把你要用的那个引脚明确挂到某条EXTI线上。比如GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0),就是把PA0接到EXTI0。如果漏掉这一步,GPIO和NVIC配置得再完整也白搭。
| EXTI线 | 可选的GPIO引脚 | 同一时刻生效 |
|---|---|---|
| EXTI0 | PA0、PB0、PC0、PD0…… | 只能选一个 |
| EXTI1 | PA1、PB1、PC1、PD1…… | 只能选一个 |
| EXTI15 | PA15、PB15、PC15…… | 只能选一个 |
提示:18、19号EXTI线是给RTC闹钟、以太网唤醒这类内部事件用的,和GPIO无关,配置按键时不用去动它们。
2.2 上升沿、下降沿和双边沿:按键到底该用哪个
EXTI的触发条件由EXTI_Trigger字段决定,可选上升沿、下降沿、双边沿。选择依据不是"哪个好用",而是按键硬件接法。最常见的接法是按键一端接GPIO引脚,另一端接GND,引脚内部上拉。按键未按下时引脚是高电平,按下瞬间被拉到低电平,产生一个下降沿,所以用EXTI_Trigger_Falling。
反过来,如果按键一端接3.3V、一端接GPIO,引脚配下拉输入,未按下时低电平,按下变高,就该用EXTI_Trigger_Rising。有些项目要求"按下和松开都要处理",比如长按检测、编码器方向判断,那就用EXTI_Trigger_Rising_Falling双边沿,但注意松开的瞬间也会进中断,逻辑里要加区分。
| 触发方式 | 典型场景 | 注意事项 |
|---|---|---|
| 下降沿 | 按键接GND、上拉输入 | 机械抖动会在下降沿附近产生多次跳变 |
| 上升沿 | 按键接VCC、下拉输入 | 同样存在抖动,需要软件消抖 |
| 双边沿 | 编码器、需要同时感知按下和释放 | ISR里要做状态判断,避免逻辑混乱 |
机械按键最麻烦的是抖动:按下和松开的瞬间,簧片会弹跳几次,持续约5到20毫秒。从硬件层看,一次按下会在下降沿附近产生一串毛刺,每一串毛刺都可能触发一次EXTI。这个问题硬件解决不了,必须软件消抖,第4章会展开写。
2.3 中断模式与事件模式:按键必须走中断
EXTI每条线都有两种工作模式:中断模式和事件模式。区别不在"能不能触发",而在触发信号的去向。中断模式下,EXTI检测到边沿后会把请求送到NVIC,CPU停止当前任务跳进中断服务函数,这是写按键控制LED必须用的模式。
事件模式不经过NVIC,边沿信号直接以脉冲形式输出给其他外设,比如触发ADC采样、定时器捕获。CPU全程不参与,所以也别指望用事件模式唤醒CPU——它压根到不了中断控制器。我见过有人把EXTI_Mode误配成事件模式,结果按键怎么按程序都没反应,查了半天才发现是模式选错。
| 对比项 | 中断模式 | 事件模式 |
|---|---|---|
| 是否经过NVIC | 是 | 否 |
| CPU是否需要执行ISR | 需要 | 不需要 |
| 典型用途 | 按键、外部唤醒 | 触发ADC采样、定时器同步 |
2.4 NVIC:使能、优先级分组与抢占关系
EXTI只是把中断请求发出来,真正让CPU响应的是NVIC。配置NVIC时有两个关键点:一是NVIC_IRQChannel必须写对。EXTI0有独立的中断通道EXTI0_IRQn,EXTI1是EXTI1_IRQn,而EXTI5到EXTI9共用EXTI9_5_IRQn,EXTI10到EXTI15共用EXTI15_10_IRQn。通道写错,中断服务函数永远不会执行。
第二个是优先级。标准库里的NVIC_InitStructure需要填抢占优先级和子优先级,这两个值只有在优先级分组确定后才有意义。比如分组2是2位抢占、2位子优先级,抢占优先级0最高、3最低。如果工程里只有EXTI一个中断,随便设都能跑;但一旦有串口中断、定时器中断、Systick,优先级分组就变成坑了——抢占优先级高的会打断低的,低的不但不能打断高的,还会被延迟响应。所以配置EXTI之前,先想清楚这个中断在系统里到底该排第几位。
3. 标准库配置流程:GPIO、AFIO、EXTI、NVIC逐级初始化与参数修改
3.1 时钟:GPIO和AFIO同时打开
初始化外设第一步永远是开时钟。EXTI本身由APB2总线提供时钟,但GPIO复用功能需要额外的AFIO时钟,这两个必须一起开,否则GPIO_EXTILineConfig不会生效,按键按下后中断根本不触发。
/* 开启GPIOA时钟与AFIO时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);这段代码逻辑上是一次把两个时钟都打开。第一个参数用了按位或,把GPIOA和AFIO两个时钟标志合并传入,函数内部会分别置位。如果按键和LED不在同一个GPIO口,比如按键在PA0、LED在PC13,就要把GPIOC也加进去。参数说明:RCC_APB2Periph_GPIOA是GPIOA外设时钟,RCC_APB2Periph_AFIO是复用功能时钟,两者类型都是32位无符号整数,可以用|组合。
3.2 GPIO配置:按键上拉输入,LED推挽输出
GPIO配置分成输入和输出两种,这一步同时影响EXTI的触发电平和LED的驱动能力。
GPIO_InitTypeDef GPIO_InitStructure; /* 按键引脚PA0:上拉输入 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* LED引脚PC13:推挽输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure);逻辑说明:按键配置成上拉输入(GPIO_Mode_IPU),内部上拉电阻把引脚默认拉高,按键按下接地时引脚变低,正好配合下降沿触发。LED用推挽输出(GPIO_Mode_Out_PP),能输出足够的驱动电流点亮LED。参数提醒:GPIO_Speed在输入模式下不生效,写上只是为了结构统一;LED引脚要和板子上的实际接线对应,有的板子LED接在PC13,有的接在PB1,以原理图为准。如果按键另一端接的是3.3V而不是GND,输入模式要改成下拉输入GPIO_Mode_IPD,触发沿同步改成上升沿。
3.3 引脚映射:GPIO_EXTILineConfig把引脚挂到指定中断线
有了GPIO还不行,必须把PA0明确映射到EXTI0这条中断线上。这就是2.1里说的AFIO选择器的作用。
/* 将PA0映射到EXTI0外部中断线 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);这段代码逻辑是把GPIOA的第0号引脚(PA0)连接到EXTI模块的0号线。函数没有返回值,配置出错也不会报错,所以很多人漏写之后完全察觉不到。如果用了PB0做按键,第一个参数要改成GPIO_PortSourceGPIOB,第二个参数保持GPIO_PinSource0不变。漏写这行配置,EXTI0会保持默认状态,按键没反应是很常见的现象。
3.4 EXTI初始化:中断线、触发条件与使能
映射完成之后,初始化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);逻辑说明:EXTI_Line指定使用哪条中断线,PA0对应的是EXTI_Line0;EXTI_Mode设为中断模式而不是事件模式;EXTI_Trigger设为下降沿触发,匹配按键接GND的硬件接法;最后EXIT_LineCmd使能这条线。参数说明:如果把按键换到PB1,这里要同步改成EXTI_Line1,同时3.3节的映射函数也要改成GPIO_PinSource1,两个地方必须保持"线号一致"。触发沿的参数在不同库版本里名字略有区别,有的叫EXTI_Trigger_Rising,有的叫EXTI_Trigger_Rising_Falling,写之前对照一下头文件定义。
3.5 NVIC初始化:把中断送进CPU
EXTI配置完只相当于把线拉到了NVIC门口,门还没开。NVIC的使能决定CPU到底收不收这个中断请求。
NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);逻辑说明:指定中断通道为EXTI0_IRQn,这是EXTI0对应的NVIC中断号;抢占优先级设1、子优先级设0。如果工程里没有其他中断,这两个值随意;如果有串口或定时器中断,要提前规划好优先级分组,避免EXTI被高优先级中断长期打断。参数注意:使用EXTI5到EXTI9之间的任意引脚时,NVIC_IRQChannel要填EXTI9_5_IRQn,EXTI10到EXTI15填EXTI15_10_IRQn。这些中断号在芯片头文件stm32f10x.h里都有定义,编译时报错基本就是写错了。
3.6 中断服务函数:读状态、清挂起位、翻转LED
中断服务函数的名字不能自己随便起,必须和启动文件里的向量表一致。EXTI0的ISR固定叫EXTI0_IRQHandler。
void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { /* 翻转LED */ GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13))); /* 清除挂起标志,否则会反复进入中断 */ EXTI_ClearITPendingBit(EXTI_Line0); } }逻辑说明:进入中断后先用EXTI_GetITStatus确认是EXTI0产生的请求,避免多条线共用ISR时误处理;GPIO_ReadOutputDataBit读取当前LED输出电平,用1 -取反后写回,实现翻转;最后必须用EXTI_ClearITPendingBit清除挂起标志。参数说明:这行的作用很多人不重视——硬件检测到下降沿后,EXTI的挂起位会一直保持1,不清除的话中断返回后立刻再次触发,CPU会被钉在ISR里出不来,表现就是程序"卡死"在中断中。
4. 按键控制LED完整实战:消抖处理、完整main与换引脚时的三处联动
4.1 机械抖动:一次按下为什么会出现多次触发
机械按键的触点闭合不是一次干净的接触。按下按键的瞬间,金属簧片会反复接触、断开几次,持续大约5到20毫秒,这段"抖动"在电气上表现为电平快速跳变。如果用示波器看PA0的波形,下降沿附近会有一串毛刺。每一根毛刺都是完整的下降沿,EXTI会为每一根毛刺触发一次中断。这意味着不做消抖的话,一次物理按键可能翻转LED三四次,看起来就像LED乱闪。
抖动的频率远高于人手按按键的频率,所以消抖的思路是"忽略短时间内的重复触发"。硬件上可以在按键两端并联100nF电容,但PCB已经定型时改不了,软件消抖更灵活。
4.2 做法对比:中断里delay消抖 vs 标志位消抖
最常见的消抖写法是在中断服务函数里立刻延时10毫秒,再读一次引脚电平,确认确实是低电平才执行翻转:
void EXTI0_IRQHandler(void) { delay_ms(10); // 简单消抖,但有隐患 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) { GPIO_WriteBit(...); // 翻转LED } EXTI_ClearITPendingBit(EXTI_Line0); }这个写法能消抖,但在中断里调用delay_ms是隐患。延时函数依赖Systick,而Systick本身也是中断。如果Systick优先级低于EXTI,EXTI中断期间Systick无法进入,delay_ms就可能卡住;如果优先级高于EXTI,则又要处理嵌套。在一些简单的开发板例程里能跑通,纯粹因为优先级配置碰巧合理。工程里只要有串口、定时器、ADC多个中断,这套写法迟早翻车。
更稳妥的是标志位法:中断里不做事,只置一个标志,主循环发现标志后再做时间差判断。CPU没有空转等待,只是定期检查一个变量的状态。
4.3 推荐写法:中断置标志,主循环按时间差判断
标志位法的核心是把"检测到按键事件"和"确认是有效按键"分开。中断服务函数只做最小的事:记录事件、清标志。
volatile uint8_t key_trigger_flag = 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { key_trigger_flag = 1; /* 记录按键事件 */ EXTI_ClearITPendingBit(EXTI_Line0); /* 先清除挂起位 */ } }主循环里检测标志,并做时间差判断:
uint32_t last_press_time = 0; while (1) { if (key_trigger_flag) { key_trigger_flag = 0; /* 距上次有效按键超过20ms才认为是新按键 */ if (get_tick() - last_press_time > 20) { GPIO_WriteBit(LED_GPIO_PORT, LED_GPIO_PIN, (BitAction)(1 - GPIO_ReadOutputDataBit(LED_GPIO_PORT, LED_GPIO_PIN))); last_press_time = get_tick(); } } }逻辑说明:中断里的key_trigger_flag用volatile修饰,防止编译器把它优化进寄存器导致主循环读不到更新。主循环判断get_tick() - last_press_time > 20,只有距上次有效按键超过20毫秒才执行翻转,抖动产生的连续脉冲因为时间差太短被过滤掉。20毫秒这个值不是固定的,按键簧片质量差可以把阈值加到30毫秒,但也不能太大,否则快速连按会被吞掉。get_tick()是工程里基于Systick实现的毫秒计数器,原型是uint32_t get_tick(void),在延时初始化函数中启动。
4.4 完整工程:可直接复制的main.c与EXTI_Config
把前面的配置和消抖逻辑整合成一个完整工程文件,包含初始化函数和主循环:
#include "stm32f10x.h" #include "delay.h" #define KEY_GPIO_PORT GPIOA #define KEY_GPIO_PIN GPIO_Pin_0 #define LED_GPIO_PORT GPIOC #define LED_GPIO_PIN GPIO_Pin_13 volatile uint8_t key_trigger_flag = 0; static void EXTI_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* 1. 时钟:GPIOA、GPIOC、AFIO */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE); /* 2. 按键输入上拉 */ GPIO_InitStructure.GPIO_Pin = KEY_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(KEY_GPIO_PORT, &GPIO_InitStructure); /* 3. LED推挽输出 */ GPIO_InitStructure.GPIO_Pin = LED_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LED_GPIO_PORT, &GPIO_InitStructure); /* 4. 引脚映射 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); /* 5. EXTI */ 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); /* 6. NVIC */ NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { key_trigger_flag = 1; EXTI_ClearITPendingBit(EXTI_Line0); } } int main(void) { uint32_t last_press_time = 0; delay_init(); EXTI_Config(); while (1) { if (key_trigger_flag) { key_trigger_flag = 0; if (get_tick() - last_press_time > 20) { GPIO_WriteBit(LED_GPIO_PORT, LED_GPIO_PIN, (BitAction)(1 - GPIO_ReadOutputDataBit(LED_GPIO_PORT, LED_GPIO_PIN))); last_press_time = get_tick(); } } } }函数组织说明:EXTI_Config是静态函数,只在main里调用,避免外部文件误用;EXTI0_IRQHandler必须保持全局可见,否则链接器找不到中断入口。主循环里没有delay_ms,也没有轮询按键引脚,只有对标志变量的轻量检查,CPU在没按键时可以把时间让给其他任务。这个工程结构在做多个外设同时工作的项目时,比在中断里做耗时的处理干净得多。
4.5 换引脚时的三处联动:GPIO、EXTI线、NVIC
把按键从PA0换到PB1,只改GPIO的初始化是远远不够的,这里有三个地方必须同步。第一,时钟使能里的GPIOA改成GPIOB;第二,GPIO_EXTILineConfig的参数改成GPIO_PortSourceGPIOB, GPIO_PinSource1;第三,EXTI_InitStructure.EXTI_Line从EXTI_Line0改成EXTI_Line1,同时NVIC的NVIC_IRQChannel改成EXTI1_IRQn。中断服务函数也要跟着改,函数名变成EXTI1_IRQHandler,里面判断的线路号同步换成EXTI_Line1。
这套"GPIO、EXTI线、NVIC通道"三联动是EXTI配置里最容易出错的地方。很多人只改了GPIO引脚,中断线没改,按键按下去明明产生了事件,CPU却收到错误通道的中断,甚至直接不响应。我自己的习惯是换引脚前先在纸上把这三个地方列出来,改完对照检查一遍,能省下不少调试时间。
5. 避坑记录:被"伪中断"卡住的五个典型现场
5.1 按下完全没反应:AFIO时钟没开的经典教训
现象:GPIO、EXTI、NVIC全部初始化了,中断服务函数也写了,按键按下LED纹丝不动。
原因:初始化流程里只开了RCC_APB2Periph_GPIOA,漏掉了RCC_APB2Periph_AFIO。没有AFIO时钟,GPIO_EXTILineConfig的寄存器写入不生效,PA0根本没挂到EXTI0上。这类问题不报错,纯粹是"配置了但没起作用"的状态。
解决:时钟使能语句同时加上RCC_APB2Periph_AFIO,重新编译烧录就好了。排查时可以通过调试器读EXTI的挂起寄存器,如果按下按键后挂起位始终为0,先查AFIO时钟,再查映射函数有没有执行。
5.2 中断进了一次就直接卡死:挂起位没清
现象:刚下载程序时LED翻转了一次,之后再按按键没有任何反应,整个程序像死了一样。
原因:中断服务函数里翻转了LED,但漏了EXTI_ClearITPendingBit。第一次中断触发后挂起位保持1,CPU退出ISR后立即再次进入,如此反复,主循环得不到机会执行。表面看像死机,其实是CPU陷在中断里出不来。
解决:ISR末尾补上EXTI_ClearITPendingBit(EXTI_Line0)。养成习惯:写任何EXTI中断服务函数时,先写清标志语句,再写业务逻辑,这个顺序能避免以后踩同样的坑。
5.3 一次按键翻转两次:抖动没消干净
现象:按下一次按键,LED状态连跳两次,看起来像执行了两个翻转操作。
原因:机械按键按下时有约5到20毫秒的抖动,抖动期间出现多个下降沿脉冲,EXTI为每个下降沿都触发了一次中断。中断服务函数里直接翻转LED,两次触发之间只隔几毫秒,肉眼看起来就像连续翻转了两次。
解决:按第4章的时间差法处理,主循环里判断两次有效按键事件间隔大于20毫秒才执行翻转。如果硬件上PCB已经定型,可以把阈值调整到30毫秒再实测手感。
5.4 中断服务函数里用delay,整个程序像死了一样
现象:在EXTI0_IRQHandler里调用delay_ms(10)后,程序在按键触发后完全卡死,连主循环里的其他任务也停了。
原因:延时函数依赖Systick中断更新计时。如果Systick的中断优先级低于EXTI,EXTI中断占用了CPU,Systick中断进不来,延时函数永远等不到时间到达,卡在ISR里。多个中断嵌套的工程里,这个问题更容易暴露。
解决:中断服务函数里不做延时、不做打印、不做任何可能阻塞的操作。需要消抖就置标志位,由主循环去判断和处理。这是嵌入式中断编程的通用原则,不只是EXTI这一个外设。
5.5 用了PA5和PA6两个按键,互相干扰
现象:两个按键分别接PA5和PA6,但按下PA5时,PA6的按键逻辑也执行了。
原因:EXTI5到EXTI9这五条线共用同一个NVIC中断通道EXTI9_5_IRQn,中断服务函数入口只有一个EXTI9_5_IRQHandler。ISR里没有区分具体是哪条线触发的请求,所以任何一个按键按下都会进入同一段代码,把两个按键的逻辑都跑一遍。
解决:在EXTI9_5_IRQHandler里分别调用EXTI_GetITStatus(EXTI_Line5)和EXTI_GetITStatus(EXTI_Line6),确认哪条线置了挂起位,只处理对应的那一路,处理完再清各自的标志。EXTI10到EXTI15的EXTI15_10_IRQn也是同理。
6. 进阶练习:用时间戳识别长按短按,让EXTI把芯片从STOP模式唤醒
基础例程做熟之后,可以试两个更贴近实际项目的进阶玩法。第一个是长按短按识别。中断服务函数里记录按键按下的时间戳,主循环用状态机判断按键持续了多久:
volatile uint32_t press_tick = 0; volatile uint8_t key_down_flag = 0; volatile uint8_t key_state = 0; /* 0:idle 1:pressed 2:long_done */ void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { press_tick = get_tick(); key_down_flag = 1; EXTI_ClearITPendingBit(EXTI_Line0); } } /* 主循环状态机 */ if (key_state == 0 && key_down_flag) { key_down_flag = 0; key_state = 1; } else if (key_state == 1) { if (get_tick() - press_tick >= 1500) { longPressAction(); /* 长按触发 */ key_state = 2; } else if (GPIO_ReadInputDataBit(KEY_GPIO_PORT, KEY_GPIO_PIN) == 1) { shortPressAction(); /* 未到1500ms就松开 */ key_state = 0; } } else if (key_state == 2 && GPIO_ReadInputDataBit(KEY_GPIO_PORT, KEY_GPIO_PIN) == 1) { key_state = 0; /* 长按后松开,回到空闲 */ }这个状态机把"按下"和"松开"分开处理,不需要双边沿触发也能识别按键释放。press_tick记录的是按下发生的那一刻,主循环周期检查当前时刻和它的差值,而不是在中断里干等。长按阈值1500毫秒可以根据实际手感调整,用在菜单确认或唤醒场景很合适。
第二个进阶是把EXTI用作低功耗唤醒源。芯片进入STOP模式后,CPU时钟停止,所有代码暂停,但EXTI的边沿检测电路仍在工作。按键接在PA0上,配置成下降沿触发的中断模式,执行PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI)后,按下按键会让芯片立刻退出STOP模式并进入EXTI0_IRQHandler,在ISR里把系统时钟重新初始化,主循环从暂停处继续运行。两个关键点:唤醒必须用中断模式,事件模式不经过NVIC,唤不醒;进入STOP前先清一次EXTI挂起位,否则历史遗留的挂起标志会立刻把芯片唤醒,表现为"刚睡下就醒了"。
之前做某个低功耗项目时,我在替代方案里选过一个共享中断线的方案,结果两个按键互相触发,查了整整一晚上,最后发现是共用ISR没区分线路号。从那以后,我每做一个EXTI相关的工程,都会先固定走一遍检查流程:时钟里有没有AFIO,映射函数和EXTI线号对不对,NVIC通道是否匹配,ISR里面是否清标志、是否做了线路区分。把这四步走完,再往下写业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取