资讯动态

STM32F103外部中断实现按键控制LED:EXTI与NVIC配置详解

发布时间:2026/10/15 17:40:46 来源:尧图企业网站定制
手头这块STM32F103ZET6板子流水灯和按键轮询早就跑通了真正让我停下来重新整理代码的是“外部中断”这个功能。你想想按键在轮询模式下虽然也能控制LED但CPU得一直盯着IO电平哪怕没事干也要一遍遍循环扫描这在简单Demo里没感觉一旦系统里加了显示、通信这类任务轮询按键就成了一种隐形负担。所以真正常用的按键方案尤其是需要快速响应的场景都是靠外部中断来做按下按键的瞬间触发CPU中断程序立刻跳到对应处理函数该翻灯翻灯该置标志位置标志位主循环该干嘛干嘛。这篇正好对应标题里的第03节STM32F103ZET6标准库函数开发外部中断实现按键控制LED。我会从设计思路、寄存器理解、初始化配置、中断服务函数、完整代码到常见踩坑一条龙捋一遍。适合已经会用GPIO输出和轮询读按键、但对EXTI和NVIC还处于“知道一点、没自己配过”阶段的朋友。代码风格基于ST官方标准外设库V3.5编译环境用哪个都行核心逻辑是通用的。1. 整体设计与思路拆解1.1 为什么按键控制LED要选外部中断而不是轮询很多人刚开始学单片机按键都是这么写的while (1) { if (GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_4) 0) { delay_ms(10); GPIO_ToggleBits(GPIOB, GPIO_Pin_0); } }这种写法在只有一个按钮的时候问题不大甚至代码看起来更直观。但你要明白它背后有个致命的缺陷主循环的扫描周期与按键按下时刻是异步的。如果主循环里的其他代码耗时较长比如驱动OLED、读传感器、处理通信协议那一次循环可能要几毫秒甚至几十毫秒。按键按下去的那一瞬间CPU可能还在别的代码段里“转悠”按键状态没法被及时捕获这就叫响应延迟。外部中断的思路完全不同。按下按键产生一个电平跳变硬件自动把中断挂起位置1CPU正在执行的指令会停下来跳转到对应的中断服务函数也就是ISR去处理“按键被按下”这件事。这意味着CPU平时可以专心跑主循环里的活只有在真正发生按键事件时才被打断响应是硬件级别的微秒级就能进中断等待时间取决于中断启动的尾部延迟和你的ISR代码长度。我用一个表格把两种方案摆一起看得更清楚对比项轮询扫描外部中断CPU占用需要周期性主动读IO占用持续存在无事时零开销事件触发才打断响应延迟与主循环周期强相关可能明显滞后硬件事件立刻触发纳秒到微秒级代码结构简单直观需要配置EXTI、NVIC结构稍复杂适合场景高频均匀操作、状态机扫描事件型触发、低功耗唤醒、实时响应按键恰恰是最典型的事件型触发不是周期性信号。按下和释放都是离散事件用中断处理天然合理。1.2 STM32F103ZET6的EXTI和NVIC资源STM32F103ZET6是144引脚的Cortex-M3芯片Flash是512KBSRAM 64KB。GPIO端口很多从GPIOA到GPIOG有7组但外部中断线EXTI只有16条分别对应每个GPIO端口的Pin0到Pin15。换句话说PA0、PB0、PC0这些引脚在“外部中断”层面是共用的同一条EXTI0线同一时刻只能选择其中一个端口的一个引脚作为EXTI0的中断源。这是新手比较容易埋雷的地方你以为PA0配置了中断PB0也能同时用实际上它们争抢的是同一条EXTI0线路配置后者会覆盖前者。再往下外部中断还要经过嵌套向量中断控制器NVIC。NVIC负责决定“哪个中断能打住当前正在执行的代码”“多个中断同时来了谁先执行”。STM32F103的优先级可以分成抢占优先级和子优先级两类具体怎么分配由NVIC_PriorityGroupConfig函数设置分组。我习惯用分组2也就是2位抢占优先级、2位子优先级一共可配置0到3的抢占级别0最高。在代码层面EXTI的初始化结构体里EXti_Line选择16条线路中的一条EXTI_Trigger选择触发方式是上升沿、下降沿还是双沿而NVIC的初始化结构体则把具体的中断通道号、抢占优先级、子优先级和使能开关填进去。这两层配置缺一不可少了NVIC使能EXTI事件产生了但CPU不响应中断函数照样不会执行。这里要先清楚设计思路板子上LED接在PB0、PB1、PB5三个按键分别是PE3、PE4和PA0。PE3、PE4这种按键一端接GND、另一端接IO空闲时用内部上拉到高电平按下变成低电平所以应该用下降沿触发。PA0这一路按键按键按下接高电平空闲时内部下拉用上升沿触发。这样设计的好处是按键按下瞬间产生边沿中断立刻响应释放时不产生误触发。2. 核心配置细节与实操要点2.1 GPIO初始化输入模式的选择逻辑外部中断虽然是由“边沿”触发的但引脚本质上还是输入口所以第一步还是要配置GPIO为输入模式。很多人在这里栽跟头觉得配了EXTI就可以不管GPIO了其实EXTI模块检测的是引脚上的电平变化引脚本身的推挽、开漏、上拉、下拉状态必须先设置好。我常用的按键GPIO配置有两种方向。按键一端接地、引脚初始为高电平时配置为上拉输入GPIO_Mode_IPU按键一端接电源、引脚初始为低电平时配置为下拉输入GPIO_Mode_IPD。如果电路板已经有了外部上拉电阻也可以选择浮空输入GPIO_Mode_IN_FLOATING但我建议尽量使用内部上拉/下拉减少外部器件依赖同时防止引脚悬空导致的不稳定电平误触发中断。void KEY_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE | RCC_APB2Periph_GPIOA, ENABLE); /* KEY0 -- PE4KEY1 -- PE3按下为低电平空闲上拉 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOE, GPIO_InitStructure); /* KEY_UP -- PA0按下为高电平空闲下拉 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; GPIO_Init(GPIOA, GPIO_InitStructure); }这段代码里最关键的是GPIO_Mode的选择。外部中断和外部事件都是在这个输入模式的基础上工作的如果引脚配置成推挽输出那么EXTI检测到的就是你自己输出到引脚上的电平按键按下根本不会有变化中断自然触发不了。这个问题在一些学习板上表现得很隐蔽因为GPIO可能之前被初始化成推挽输出来驱动LED后来复用同一引脚时忘了重新配置模式。2.2 EXTI初始化边沿触发方式怎么选EXTI类的外设配置核心是EXTI_InitTypeDef结构体。它跟GPIO结构体一样有Line、Mode、Trigger、LineCmd四个成员。Mode通常选EXTI_Mode_Interrupt也就是产生中断请求如果你只是想让事件触发别的外设比如ADC转换可以选EXTI_Mode_Event这个一般在高级应用才用。触发方式这一项需要跟你的硬件电路严格对应。按键按下接地空闲高电平那就选下降沿EXTI_Trigger_Falling按键按下接电源空闲低电平选上升沿EXTI_Trigger_Rising。如果你还希望“按下”和“释放”两个动作都响应也可以选双沿EXTI_Trigger_Rising_Falling但要小心一次完整按压动作会产生两次中断在按键控制LED里通常用不到除非做长按短按检测。在配置EXTI之前还必须调用GPIO_EXTILineConfig把GPIO引脚和EXTI线路做一次映射。这一步很容易被忽略因为它属于GPIO库函数不在EXTI函数库里面。而且最容易被坑的是RCC_APB2Periph_AFIO时钟必须同时使能否则GPIO_EXTILineConfig配置无效中断悄悄失效。void EXTI_Key_Config(void) { EXTI_InitTypeDef EXTI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4); GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_LineCmd ENABLE; /* KEY0PE4下降沿 */ EXTI_InitStructure.EXTI_Line EXTI_Line4; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_Init(EXTI_InitStructure); /* KEY1PE3下降沿 */ EXTI_InitStructure.EXTI_Line EXTI_Line3; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_Init(EXTI_InitStructure); /* KEY_UPPA0上升沿 */ EXTI_InitStructure.EXTI_Line EXTI_Line0; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; EXTI_Init(EXTI_InitStructure); }这里我连续复用了同一个EXTI_InitStructure注意每次初始化后结构体里的Line成员都会被改写下一次重新赋值就行。如果你用宏封装建议写成一个结构体一个函数一步到位避免遗漏。2.3 NVIC初始化优先级分组和中断通道NVIC这一层的作用可以理解成“中断的调度室”。外部中断的信号到达CPU后NVIC先判断它有没有资格打断当前任务打断之后又是否会被另一个更高优先级的中断再次打断。这个过程不搞清楚你的程序可能出现中断嵌套异常。在标准库里NVIC的初始化分为两步第一步设置优先级分组第二步配置具体通道。void NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; /* 选择优先级分组22位抢占优先级2位子优先级 */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 外部中断EXTI0用于PA0按键 */ 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); /* 外部中断EXTI3用于PE3按键 */ NVIC_InitStructure.NVIC_IRQChannel EXTI3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 2; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); /* 外部中断EXTI4用于PE4按键 */ NVIC_InitStructure.NVIC_IRQChannel EXTI4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }注意到这里使用了EXTI0_IRQn、EXTI3_IRQn、EXTI4_IRQn而不是EXTI15_10_IRQn这种合并通道。F103的外部中断有分组的通道EXTI5到EXTI9共用一个EXTI9_5_IRQnEXTI10到EXTI15共用一个EXTI15_10_IRQn。如果按键接到这些引脚你在启动文件里写的中断函数名也要改成对应分组的中断函数名字比如EXTI9_5_IRQHandler。这一点在后续引入更多按键时特别容易踩后面我会在常见问题里细说。优先级分组要放在所有外设中断初始化之前因为NVIC分组一旦设定整个系统所有中断的优先级解析方式就定了。实际项目里如果既有串口中断又有定时器中断抢占优先级的设计需要整体考虑不能只想着当前这个按键中断。3. 实操过程与核心代码实现3.1 工程中需要添加哪些标准库源文件标准库开发的工程结构说简单也简单说麻烦也麻烦。如果你只是用寄存器或HAL库可能不会意识到文件依赖这件事。但标准库的所有外设驱动是以源文件为单位的调用了GPIO函数就得把stm32f10x_gpio.c加进工程调用了EXTI就得加stm32f10x_exti.c调用了NVIC_PriorityGroupConfig和NVIC_Init就得加misc.c。你可以直接在工程里加这几个用不到的可以省stm32f10x_gpio.cGPIO初始化、读取、翻转都在这里。stm32f10x_exti.cEXTI初始化、清除挂起位。stm32f10x_rcc.c时钟使能函数。misc.cNVIC相关函数。stm32f10x_it.c中断服务函数实现通常你自己写。core_cm3.cCortex-M3内核相关如系统异常处理。system_stm32f10x.c系统时钟初始化。头文件路径也要配好把标准库的Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x、Libraries/STM32F10x_StdPeriph_Driver/inc、Libraries/CMSIS/CM3/CoreSupport都添加进去不然编译一上来就是一堆头文件找不到的错误。关于工程文件路径的建议我习惯把启动文件、标准库固件库、用户代码分层放用户代码只放在一个文件夹里方便备份和版本管理。模块化方面按键、LED、EXTI这些分别拆成led.c、key.c、exti.c跟main.c分开。刚开始练手可以直接全堆在main.c里但不要养成这个习惯。3.2 LED、按键、EXTI、NVIC的完整初始化代码我直接把工程里的初始化和配置代码贴出来。这里LED用PB0、PB1、PB5作为三个输出配置成推挽输出初始状态熄灭。void LED_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); /* 初始状态全部熄灭 */ GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_5); }然后再封装一个统一的配置函数外部中断相关的东西都放一起方便检查遗漏void EXTI_Key_Config(void) { EXTI_InitTypeDef EXTI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); /* 引脚到EXTI线路的映射 */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4); GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_LineCmd ENABLE; /* PE4 - EXTI4下降沿 */ EXTI_InitStructure.EXTI_Line EXTI_Line4; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_Init(EXTI_InitStructure); /* PE3 - EXTI3下降沿 */ EXTI_InitStructure.EXTI_Line EXTI_Line3; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_Init(EXTI_InitStructure); /* PA0 - EXTI0上升沿 */ EXTI_InitStructure.EXTI_Line EXTI_Line0; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; EXTI_Init(EXTI_InitStructure); }注意按键GPIO初始化也要在EXTI映射之前完成。顺序上推荐是时钟使能 - GPIO初始化 - 调用GPIO_EXTILineConfig - 配置EXTI - 配置NVIC。如果先配EXTI再初始化GPIO虽然大多数情况下也能跑但某些极端上电时序下可能会出现误触发养成规范顺序比什么都重要。3.3 中断服务函数和消抖处理中断服务函数是外部中断的主体。之前配置了三条EXTI线路对应三个中断函数。在STM32标准库里中断向量名是固定的不能自己改。EXTI0对应EXTI0_IRQHandlerEXTI3对应EXTI3_IRQHandlerEXTI4对应EXTI4_IRQHandler。按键中断最容易遇到的问题就是抖动。机械按键在按下和释放的瞬间金属触点会来回弹跳几十毫秒导致电平在高低之间快速抖动。如果不做消抖一次按键可能触发好几次外部中断。解决办法有两种常见思路一种是在中断里延时再确认电平一种是配合定时器做去抖。我先给出最直观的延时消抖写法适合学习和简单场景void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { /* 延时消除机械抖动 */ delay_ms(10); /* KEY_UP按下为高电平 */ if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 1) { GPIO_ToggleBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_5); } /* 清除中断挂起位否则中断会一直触发 */ EXTI_ClearITPendingBit(EXTI_Line0); } } void EXTI3_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line3) ! RESET) { delay_ms(10); /* KEY1按下为低电平 */ if (GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_3) 0) { GPIO_ToggleBits(GPIOB, GPIO_Pin_1); } EXTI_ClearITPendingBit(EXTI_Line3); } } void EXTI4_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line4) ! RESET) { delay_ms(10); /* KEY0按下为低电平 */ if (GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_4) 0) { GPIO_ToggleBits(GPIOB, GPIO_Pin_0); } EXTI_ClearITPendingBit(EXTI_Line4); } }这里有个很重要的细节延时消抖虽然简单但ISR里阻塞式延时其实不是好习惯。如果你后面要搞复杂的系统想彻底一点就应该在主循环里用定时器状态机做消抖ISR只置标志位。我在这个Demo里用delay_ms是为了让代码能直接跑理解清楚“中断电平确认”这个思路之后再慢慢改成更高级的消抖方式。另外EXTI_ClearITPendingBit这一步不能省。外部中断触发时EXTI挂起寄存器对应位会被硬件置1。不清除的话中断返回后同样的挂起条件还会再次触发中断程序就会死循环式地不断进中断主循环根本跑不动。我在调试的时候也遇到过中断里明明什么都没执行程序还是卡在ISR里出不来的情况后来一查就是忘了清挂起位。3.4 main函数与延时函数main函数反而简单了。先配置系统时钟再初始化LED和按键然后配置EXTI和NVIC最后进入死循环。主循环里可以什么任务都不放就是空等着中断打断你。int main(void) { /* 系统时钟初始化为72MHz标准库通常在system_stm32f10x.c里配置 */ SystemInit(); /* 这部分依赖自己的硬延时实现用SysTick或简单循环都行 */ delay_init(); LED_GPIO_Config(); KEY_GPIO_Config(); EXTI_Key_Config(); NVIC_Config(); while (1) { /* 主循环不需要轮询按键可以放其他任务 */ } }如果想让现象更明显可以让LED1由KEY0控制、LED2由KEY1控制KEY_UP一键全亮全灭。中断服务函数里已经写了对应逻辑。这样你的整个程序就是“外部事件驱动型”而不是“循环扫描型”了。4. 常见问题与排查技巧实录4.1 中断完全没有进来LED没有任何反映先说最基础的排查路径。第一步确认中断服务函数名字写对了没。标准库里EXTI0_IRQHandler、EXTI3_IRQHandler、EXTI4_IRQHandler是固定的如果你手滑写成了EXTI_IRQHandler或者EXIT0_IRQHandler这种拼写错误编译器不会报错因为函数声明可能没有严格检查但中断向量表找不到它中断自然就不进。第二步看有没有使能AFIO时钟。GPIO_EXTILineConfig这个函数内部要操作AFIO事件控制寄存器AFIO时钟不使能映射配置等于白写。这是我见过的最常见问题没有之一。第三步确认中断使能。EXTI_Init结构体里EXTI_LineCmd要设为ENABLENVIC_Init结构体里NVIC_IRQChannelCmd也要设为ENABLE。你可能会觉得都配了实际检查时发现某个结构体成员忘赋值这种零碎错误占了调试的大半时间。第四步看边沿方向。按键是低电平有效却配了上升沿触发那要等释放按键才触发一次而且按下瞬间没有响应表现上看就像是“按了没反应”。4.2 按一下按键却翻转了两次甚至多次这是机械抖动导致的典型现象。按下按键的几十毫秒内触点抖动会产生多个上升沿或下降沿每次跳变都可能满足边沿触发条件于是同一个物理按键动作被解释成多次中断。最简单的验证方法是把按键按下去保持不动LED如果在你按住期间自己来回跳基本就是抖动造成的。解决方式就是我前面说的延时消抖在ISR里延时后再读一次引脚电平确认状态。如果嫌延时阻塞太长可以用定时器启动一个10ms到20ms的窗口窗口内忽略同线路的其他中断窗口结束再统一处理。另外还要考虑内部上拉/下拉是否真的生效。如果按键IO悬空引脚电平可能一直在临界状态微小的噪声就能触发中断。此时应该把GPIO配置成IPU或IPD而不是浮空输入。4.3 换了一个引脚之后原来引脚上的中断行为还在这个坑我印象很深。STM32的外部中断线路不是按端口分的而是按引脚编号分的。EXTI0线路可以来自PA0、PB0、PC0、PD0等所有端口的Pin0但同一时刻只能选一个端口。你配置PB0并做了GPIO_EXTILineConfig后如果后来又想到PA0去必须重新调用GPIO_EXTILineConfig把EXTI0映射到GPIOA而不是只在EXTI_Init里改一下Line就行。假如你在调试时发现PA0的按键触发后PB0对应的LED也跟着亮了十有八九就是旧的GPIO_EXTILineConfig还残留在代码里或者你映射的是同一个EXTI线路但PIN Source参数写错了。4.4 中断能进但主循环不跑了这个现象基本可以锁定在ISR里。中断服务函数如果执行时间过长或者函数内部有死循环主循环就被永久打断。常见的诱因有两个一个是忘了清中断挂起位导致中断退出后立刻再次进入形成死循环另一个是在ISR里调用了串口发送这种阻塞型函数等待时间一长表现出来就是主循环卡顿。还有一个容易忽视的问题是中断里调用了非可重入函数。标准库的某些延时函数如果用循环精准等待来写的在中断里调用问题不大但如果你用了操作系统的延时或者调用了printf就可能破坏系统状态。我的原则是ISR里只做标志位置位和非常短的IO翻转实际逻辑尽量挪到主循环。4.5 问题排查速查表现象可能原因排查方向完全无响应中断向量名拼写错误对照启动文件确认ISR名称完全无响应AFIO时钟未使能检查RCC_APB2Periph_AFIO完全无响应EXTI_LineCmd或NVIC_IRQChannelCmd未设为ENABLE检查两个结构体赋值完全无响应触发边沿方向错误测量按键按下/释放电平变化多次触发机械抖动未消抖加延时或定时器消抖多次触发引脚悬空噪声干扰配置内部上拉或下拉旧引脚还触发EXTI线路映射冲突检查GPIO_EXTILineConfig是否重复配置主循环卡死ISR中死循环检查EXTI_ClearITPendingBit是否调用主循环卡顿ISR里执行了耗时操作把逻辑移到主循环ISR只置标志位5. 经验扩展与个人手记5.1 用“标志位主循环处理”替代ISR里做逻辑上面的示例代码为了演示方便在ISR里直接翻转了LED。实际项目里我更推荐另一种模式ISR里只设置一个volatile标志位主循环检测到这个标志位后再执行LED翻转、蜂鸣器动作、状态切换等。volatile uint8_t key0_flag 0; volatile uint8_t key1_flag 0; volatile uint8_t key_up_flag 0; void EXTI4_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line4) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line4); key0_flag 1; } }这样做的核心好处是ISR的占用时间被压到极短不会影响其他中断的响应也不会阻塞主循环。你在主循环里if (key0_flag) { key0_flag 0; GPIO_ToggleBits(GPIOB, GPIO_Pin_0); }这种结构在按键数量变多或者系统变复杂之后会非常省心。消抖也可以放到主循环里做用状态机记录按下时刻比在ISR里延时优雅得多。5.2 知道外部中断的“16条线路”局限少走弯路外部中断线路只有EXTI0到EXTI15按键数量大于16个就明显不够用了。这时候一般会改用矩阵键盘扫描或者用定时器输入捕获来检测边沿。还有一个思路是让多个按键通过与门、或门电路汇聚到一个外部中断中断里再去扫描具体是哪个按键触发的。另外要注意EXTI5到EXTI9共用EXTI9_5_IRQHandlerEXTI10到EXTI15共用EXTI15_10_IRQHandler。不等于你配置了非分组的按键。比如你用了PA8和PA9两个按键两个中断都会进EXTI9_5_IRQHandler这个函数必须在函数里通过EXTI_GetITStatus分别判断到底是哪一个线路触发的。这个细节如果忽略前面所有初始化做得再好都没用因为中断向量表里根本没有EXTI9_IRQHandler这种单独函数。5.3 一点真实体会从标准库的第一课GPIO到这一课外部中断最大的变化不是代码多了两个结构体而是思维模式切到了“事件驱动”。刚开始用轮询写按键的时候我觉得中断不过是个跳转而已实际调完才发现中断系统的难点在于理解触发条件、优先级和标志位之间的关系。调试外部中断不要只盯着C代码看强烈建议拉出寄存器窗口看看EXTI挂起寄存器和NVIC当前状态确认一下到底是谁在“拦路”。多数所谓诡异现象最后都能归结到某个使能位没打开或者挂起位没清除。还有一个顺手的小技巧调试阶段可以在ISR里接个IO口翻转输出然后用逻辑分析仪或示波器看这个IO的波形。如果中断真的进来了波形翻转频率会非常直观如果波形毫无动静说明中断还在门外省得反复用串口打印。中断这套东西一旦理清映射、触发、优先级和清除流程后面再做按键、编码器、外部脉冲计数都是同一套思路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑