1.前言
在STM32的入门项目中,按键控制LED大概是最常见的练手实验了。但很多初学者习惯在中断服务函数(ISR)里直接做翻转LED、延时消抖等操作——这在工程上其实是不推荐的写法。中断服务函数应该遵循 “短、小、快” 的原则。
本文从一个实际项目出发,逐步讲解如何用 “中断置标志 + 主循环状态机” 的架构,优雅地处理多个按键的中断事件。这套模式在嵌入式开发中非常经典
2.实验器材
STM32F407开发板(标准库)
三个按键:KEY_UP(PA0)、KEY0(PE4)、KEY1(PE3)
两个LED:LED1(PF9)、LED2(PF10)
3.功能需求
KEY_UP 按下 → 两个LED同时翻转
KEY0 按下 → 仅翻转 LED1(PF9)
KEY1 按下 → 仅翻转 LED2(PF10)
4.设计思路
传统的写法是在每个按键的中断服务函数里直接调用 GPIO_ToggleBits() 翻转LED。这样做虽然简单,但有两个问题:
中断函数变“重”:翻转LED虽然不耗时,但如果后续需要加入更复杂的逻辑(比如长按/短按判断、组合键等),中断函数会越来越臃肿。
违反“短小快”原则:中断服务函数应该只做最轻量级的工作。
更优雅的做法是 “中断只负责通知,主循环负责干活”:
中断服务函数:检测到按键按下 → 设置一个全局标志位 → 立即退出
主循环(while(1)):检测标志位 → 执行对应的LED翻转动作 → 清除标志位
这就是 事件驱动架构 的雏形
5.volatile关键字的作用
在中断和主循环之间共享的变量,必须使用 volatile 关键字修饰。
原因:编译器默认会对变量进行优化,可能会把变量的值缓存在寄存器中,而不是每次从内存读取。但在中断程序中,变量的值可能在任意时刻被异步修改——编译器并不知道这一点。volatile 的作用就是告诉编译器:“这个变量随时可能被外部改变,每次使用时都必须从内存重新读取,不要做任何优化”。
// 正确用法volatileuint8_tkey_event=0;// 错误用法(编译器可能优化,导致主循环永远读不到变化)uint8_tkey_event=0;6.完整代码
6.1全局变量定义
#include"stm32f4xx.h"#include"led.h"#include"delay.h"// 全局事件标志(volatile 防止编译器优化)// 0=无事件,1=KEY_UP,2=KEY0,3=KEY1volatileuint8_tkey_event=0;6.2按键GPIO初始化
voidKEY_Init(void){GPIO_InitTypeDef GPIO_InitStructure;// 使能 GPIOA 和 GPIOE 时钟RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,ENABLE);// ----- KEY_UP (PA0) 下拉输入(高电平有效)-----GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_DOWN;// 下拉GPIO_Init(GPIOA,&GPIO_InitStructure);// ----- KEY0 (PE4) 上拉输入(低电平有效)-----GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;// 上拉GPIO_Init(GPIOE,&GPIO_InitStructure);// ----- KEY1 (PE3) 上拉输入(低电平有效)-----GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;// 上拉GPIO_Init(GPIOE,&GPIO_InitStructure);}6.3EXTI配置
voidEXTI_Config(void){EXTI_InitTypeDef EXTI_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;// 使能 SYSCFG 时钟(用于外部中断映射)RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG,ENABLE);// 映射引脚到 EXTI 通道SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource0);// PA0 → EXTI0SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource4);// PE4 → EXTI4SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource3);// PE3 → EXTI3// ----- EXTI0(PA0,上升沿触发)-----EXTI_InitStructure.EXTI_Line=EXTI_Line0;EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising;// 高有效 → 上升沿EXTI_InitStructure.EXTI_LineCmd=ENABLE;EXTI_Init(&EXTI_InitStructure);// ----- EXTI4(PE4,下降沿触发)-----EXTI_InitStructure.EXTI_Line=EXTI_Line4;EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;// 低有效 → 下降沿EXTI_Init(&EXTI_InitStructure);// ----- EXTI3(PE3,下降沿触发)-----EXTI_InitStructure.EXTI_Line=EXTI_Line3;EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;EXTI_Init(&EXTI_InitStructure);// ----- NVIC 优先级配置(分组2:2位抢占 + 2位响应)-----NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// KEY_UP 优先级最高(抢占=0)NVIC_InitStructure.NVIC_IRQChannel=EXTI0_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);// KEY0(抢占=1,响应=0)NVIC_InitStructure.NVIC_IRQChannel=EXTI4_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;NVIC_Init(&NVIC_InitStructure);// KEY1(抢占=1,响应=1)NVIC_InitStructure.NVIC_IRQChannel=EXTI3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);}6.4主函数(状态机事件分发)
intmain(void){// 系统初始化delay_init(168);LED_Init();KEY_Init();EXTI_Config();while(1){// ---- 状态机核心:事件检测与分发 ----if(key_event!=0){switch(key_event){case1:// KEY_UP:两个LED同时翻转LED1_TOGGLE();LED2_TOGGLE();break;case2:// KEY0:仅翻转 LED1LED1_TOGGLE();break;case3:// KEY1:仅翻转 LED2LED2_TOGGLE();break;default:break;}// 事件处理完毕,清除标志key_event=0;}// 主循环可以执行其它任务...}}6.5LED驱动(led.h)
#ifndef__LED_H#define__LED_H#include"stm32f4xx.h"#defineLED1_PINGPIO_Pin_9#defineLED2_PINGPIO_Pin_10#defineLED1_ON()GPIO_SetBits(GPIOF,LED1_PIN)#defineLED1_OFF()GPIO_ResetBits(GPIOF,LED1_PIN)#defineLED1_TOGGLE()GPIO_ToggleBits(GPIOF,LED1_PIN)#defineLED2_ON()GPIO_SetBits(GPIOF,LED2_PIN)#defineLED2_OFF()GPIO_ResetBits(GPIOF,LED2_PIN)#defineLED2_TOGGLE()GPIO_ToggleBits(GPIOF,LED2_PIN)voidLED_Init(void);#endif