简介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引脚同一时刻生效EXTI0PA0、PB0、PC0、PD0……只能选一个EXTI1PA1、PB1、PC1、PD1……只能选一个EXTI15PA15、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检测到边沿后会把请求送到NVICCPU停止当前任务跳进中断服务函数这是写按键控制LED必须用的模式。事件模式不经过NVIC边沿信号直接以脉冲形式输出给其他外设比如触发ADC采样、定时器捕获。CPU全程不参与所以也别指望用事件模式唤醒CPU——它压根到不了中断控制器。我见过有人把EXTI_Mode误配成事件模式结果按键怎么按程序都没反应查了半天才发现是模式选错。对比项中断模式事件模式是否经过NVIC是否CPU是否需要执行ISR需要不需要典型用途按键、外部唤醒触发ADC采样、定时器同步2.4 NVIC使能、优先级分组与抢占关系EXTI只是把中断请求发出来真正让CPU响应的是NVIC。配置NVIC时有两个关键点一是NVIC_IRQChannel必须写对。EXTI0有独立的中断通道EXTI0_IRQnEXTI1是EXTI1_IRQn而EXTI5到EXTI9共用EXTI9_5_IRQnEXTI10到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_Line0EXTI_Mode设为中断模式而不是事件模式EXTI_Trigger设为下降沿触发匹配按键接GND的硬件接法最后EXIT_LineCmd使能这条线。参数说明如果把按键换到PB1这里要同步改成EXTI_Line1同时3.3节的映射函数也要改成GPIO_PinSource1两个地方必须保持线号一致。触发沿的参数在不同库版本里名字略有区别有的叫EXTI_Trigger_Rising有的叫EXTI_Trigger_Rising_Falling写之前对照一下头文件定义。3.5 NVIC初始化把中断送进CPUEXTI配置完只相当于把线拉到了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_IRQnEXTI10到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优先级低于EXTIEXTI中断期间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。第一次中断触发后挂起位保持1CPU退出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的中断优先级低于EXTIEXTI中断占用了CPUSystick中断进不来延时函数永远等不到时间到达卡在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里面是否清标志、是否做了线路区分。把这四步走完再往下写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取