Overview
原理
用的平台是STM32F103,有三个按键,原理图分别如下
- WK_UP 连接到PA0
- KEY1连接到PE3
- KEY0连接到PE4
当按键都按下时,WK_UP(PA0) 会输入高电平,KEY0(PE4) 和 **KEY1(PE3)**会输入低电平
所以我们可以对GPIO口的输入进行检测,来判断按键是否被按下。
按键消抖
按键消抖,是为解决机械按键按下 / 松开时,内部金属弹片因弹性会短暂抖动、使电平快速跳变的问题。若不处理,MCU 会误判为多次按键。

为啥要消抖?
机械按键按下 / 松开时,内部弹片会短暂 "抖动"(5 - 20ms),导致电平疯狂跳变。若直接读取,MCU 会把一次按键误判成多次,程序可能会变得混乱。
如何消抖?
- 硬件消抖:在按键回路加小电容(0.1μF - 1μF)"稳住" 电平,或用 RS 触发器电路过滤抖动。
- 软件消抖 :检测到按键变化后,延时 10 - 20ms 再读状态,若状态不变,才认定为有效按键(最常用,简单省成本)。
简单说,消抖就是让 MCU "等一等",跳过按键抖动的不稳定阶段,只认真正的按下 / 松开动作~
例子:
当PA0连接的按键按下的时候,PA0是高电平
- 先检查PA0是否高电平
- 如果第一次是高电平,延时10ms,再度检测是否还是高电平
- 如果10ms后还是高电平,那么可以确认按键被按下
cpp
// 检测按键
uint8_t CheckKey()
{
uint8_t now = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
if (now == 0)
{
// 检测到可能按下
delay_ms(10); // 等10ms
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
return 1; // 确认按下
}
}
return 0;
}
检测方式
有两种方式可以检测按键输入,分别是轮询和中断
1. 轮询方式
- 原理:CPU 不断循环检查按键状态(就像人反复看按钮是否被按)
- 特点:简单易实现,但一直占用 CPU 资源
2. 中断方式
- 原理:按键按下时主动 "打断" CPU,CPU 暂停当前工作先处理按键(类似手机来电打断当前操作)
- 特点:响应快,不占用额外资源,但配置稍复杂
代码:
GPIO配置
无论是轮询还是中断,对于GPIO的配置其实是一样的
因为当按键都按下时:
- WK_UP(PA0) 会输入高电平
- KEY0(PE4) 和 KEY1(PE3) 会输入低电平
所以未按下的时候,我们要有相反的电平:
- WK_UP(PA0)配置为下拉(输出低电平)
- KEY0(PE4) 和 KEY1(PE3) 配置为上拉(输出高电平)
cpp
void KEY_Init(void) //IO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE时钟
// 初始化 WK_UP-->GPIOA.0 下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
// KEY0(PE4) 和 KEY1(PE3) 配置为上拉
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_3;//KEY0-KEY1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE4,3
}
轮询检测:
扫描按键函数
cpp
#define KEY0_PRES 1 //KEY0按下
#define KEY1_PRES 2 //KEY1按下
#define WKUP_PRES 3 //KEY_UP按下(即WK_UP/KEY_UP)
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下
//1,KEY0按下
//2,KEY1按下
//3,KEY3按下 WK_UP
u8 KEY_Scan(u8 mode)
{
static u8 key_up = 1; // 按键按松开标志
if (mode)
{
key_up = 1; // 支持连按
}
if (key_up && (KEY0 == 0 || KEY1 == 0 || WK_UP == 1))
{
delay_ms(10); // 去抖动
key_up = 0;
if (KEY0 == 0)
{
return KEY0_PRES;
}
else if (KEY1 == 0)
{
return KEY1_PRES;
}
else if (WK_UP == 1)
{
return WKUP_PRES;
}
}
else if (KEY0 == 1 && KEY1 == 1 && WK_UP == 0)
{
key_up = 1;
}
return 0; // 无按键按下
}
调用扫描函数
**while(1)**一直调用 KEY_Scan函数
cpp
void Key_task()
{
vu8 key=0;
printf("Key_task start\r\n");
while(1)
{
key=KEY_Scan(0); //得到键值
if(key)
{
switch(key)
{
case WKUP_PRES: //控制蜂鸣器
printf("Task 2 WKUP_PRES\r\n");
break;
case KEY1_PRES:
printf("Task 2 KEY1_PRES\r\n");
break;
case KEY0_PRES:
printf("Task 2 KEY0_PRES\r\n");
break;
}
}
else
{
delay_ms(10);
}
}
}
中断方式检测
我们不应该在中断中使用延时来做消抖(中断处理函数应该快进快出,设立标志就退出)。那么我们可以使用定时器来做这个延时判断。
外部中断配置
- 我把对GPIO的配置也放在这个函数中,这部分代码其实和上面GPIO配置是一样的。
- 这里最主要是配置引脚的外部中断,并且注意触发方式,需要根据原理图来对应设置。
如果按键是按下为低电平,未按下是高电平时,此时触发方式应该是下降沿(高电平->低电平)
cpp
// 按键初始化函数,使用外部中断
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能 GPIO 和 AFIO 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE);
// 初始化 WK_UP --> GPIOA.0 下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // PA0 设置成输入,默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化 GPIOA.0
// 初始化 KEY0-KEY1 --> GPIOE.4, GPIOE.3 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure); // 初始化 GPIOE4,3
// 配置 GPIO 引脚作为外部中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3);
// 配置 EXTI 线路
// 配置 WK_UP 外部中断
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);
// 配置 KEY0 外部中断
EXTI_InitStructure.EXTI_Line = EXTI_Line4;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_Init(&EXTI_InitStructure);
// 配置 KEY1 外部中断
EXTI_InitStructure.EXTI_Line = EXTI_Line3;
EXTI_Init(&EXTI_InitStructure);
// 配置 NVIC 中断优先级
// 配置 WK_UP 中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置 KEY0 中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_Init(&NVIC_InitStructure);
// 配置 KEY1 中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
NVIC_Init(&NVIC_InitStructure);
// 初始化定时器
TIM_Configuration();
}
定时器配置
cpp
// 定时器初始化函数
void TIM_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能定时器时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 定时器基本配置
TIM_TimeBaseStructure.TIM_Period = 10 - 1; // 10ms 定时
TIM_TimeBaseStructure.TIM_Prescaler = SystemCoreClock / 1000000 - 1; // 1MHz 计数频率
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 使能定时器中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 配置 NVIC
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x03;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
外部中断处理函数:
对应每个按键的引脚
这里其实优化的点还有很多,应该是中断服务函数的时候才去开启定时器会更合理
cpp
// KEY0 中断服务函数 (PE4 -> EXTI4)
void EXTI4_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line4) != RESET)
{
key0_debounce_flag = 1; // 置位消抖标志
EXTI_ClearITPendingBit(EXTI_Line4); // 清除中断标志
}
}
// KEY1 中断服务函数 (PE3 -> EXTI3)
void EXTI3_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line3) != RESET)
{
key1_debounce_flag = 1; // 置位消抖标志
EXTI_ClearITPendingBit(EXTI_Line3); // 清除中断标志
}
}
// WK_UP 中断服务函数 (PA0 -> EXTI0)
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
wkup_debounce_flag = 1; // 置位消抖标志
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志
}
}
定时器中断处理函数
cpp
// 定时器 2 中断服务函数
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
// 处理 KEY0 消抖
if (key0_debounce_flag)
{
if (KEY0 == 0 && !key0_pressed)
{
// 确认按键按下
printf("KEY0 pressed!\r\n");
key0_pressed = 1; // 标记按键已按下
}
else if (KEY0 == 1 && key0_pressed)
{
key0_pressed = 0; // 标记按键已释放
}
// 清除消抖标志
key0_debounce_flag = 0;
}
// 处理 KEY1 消抖
if (key1_debounce_flag)
{
if (KEY1 == 0 && !key1_pressed)
{
// 确认按键按下
printf("KEY1 pressed!\r\n");
key1_pressed = 1; // 标记按键已按下
}
else if (KEY1 == 1 && key1_pressed)
{
key1_pressed = 0; // 标记按键已释放
}
// 清除消抖标志
key1_debounce_flag = 0;
}
// 处理 WK_UP 消抖
if (wkup_debounce_flag)
{
if (WK_UP == 1 && !wkup_pressed)
{
// 确认按键按下
printf("WK_UP pressed!\r\n");
wkup_pressed = 1; // 标记按键已按下
}
else if (WK_UP == 0 && wkup_pressed)
{
wkup_pressed = 0; // 标记按键已释放
}
// 清除消抖标志
wkup_debounce_flag = 0;
}
// 清除定时器中断标志
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
相关的变量标志
cpp
// 定义按键状态标志
volatile uint8_t key0_debounce_flag = 0;
volatile uint8_t key1_debounce_flag = 0;
volatile uint8_t wkup_debounce_flag = 0;
// 新增按键按下标志
volatile uint8_t key0_pressed = 0;
volatile uint8_t key1_pressed = 0;
volatile uint8_t wkup_pressed = 0;