在嵌入式开发中,高效、可靠地处理按键输入是一个常见需求。许多传统的按键扫描程序(如一些开发板提供的例程)通常只支持单击事件,实现双击、长按等复杂逻辑时代码会变得臃肿。更重要的是,它们大多采用在main函数中循环扫描或使用delay函数的方式,这很容易阻塞主程序运行,导致系统响应迟钝。本文将介绍一种基于定时器中断驱动的状态机方法,实现按键单击、双击、长按、连按的识别,整个识别过程无延时、不阻塞,极大地提升了系统的实时性。
核心功能与设计思路
本程序的核心是一个可配置的按键状态机,具备以下特性:
- 功能全面:支持单击、双击、长按、连按事件的识别。
- 高度可配置:消抖时间、长按判定时间、双击间隔、连击频率等参数均可通过宏定义灵活调整。
- 模式可选:通过配置结构体,可以自由选择使能或禁用长按、连按、双击功能。
- 非阻塞:所有状态转移和计时均在定时器中断服务例程中完成,主循环仅需查询事件标志并执行相应操作。
- 易于移植:只需适配
KEY_ReadPin函数读取GPIO,并初始化按键配置数组即可。
关键设计逻辑:
- 事件触发时机:状态机在定时器中断中判定并设置事件标志,主函数检测到标志后执行相应操作并清除标志,实现事件处理与状态判定的解耦。
- 单击:作为基础事件,在按键按下并经过消抖后,于释放时刻触发。
- 长按:若使能长按功能,当按键按下持续时间超过设定阈值并在释放时,触发长按事件;若未使能,则释放时仍触发单击事件。
- 连按:若使能连按功能,在按键长按不释放的情况下,达到长按时间后会触发第一次连按事件,之后以固定间隔持续触发,直到按键释放。释放时,根据是否使能长按来决定最终触发长按或单击事件。这本质上是利用状态机管理复杂时序的典型应用。
- 双击:若使能双击功能,在第一次单击释放后开始计时,若在设定时间窗口内再次按下并释放,则触发双击事件(第二次释放时)。若第二次按下行为变为长按,则逻辑会退化为:先触发第一次的单击事件,再根据长按使能情况触发相应事件。
代码实现详解
1. 头文件定义 (my_key.h)
头文件定义了状态机所需的所有枚举类型、宏和结构体。
#ifndef ___MY_KEY_H__
#define ___MY_KEY_H__
#include "main.h"
#define ARR_LEN(arr) ((sizeof(arr)) / (sizeof(arr[0]))) // 数组大小宏函数
// 时间参数配置(单位:ms,取决于定时器中断周期)
#define KEY_DEBOUNCE_TIME 10 // 消抖时间
#define KEY_LONG_PRESS_TIME 500 // 长按判定时间
#define KEY_QUICK_CLICK_TIME 100 // 连按时间间隔
#define KEY_DOUBLE_CLICK_TIME 200 // 双击判定时间
#define KEY_PRESSED_LEVEL 0 // 按键被按下时的电平
// 按键动作
typedef enum {
KEY_Action_Press, // 按住
KEY_Action_Release, // 松开
} KEY_Action_TypeDef;
// 按键状态(状态机核心)
typedef enum {
KEY_Status_Idle, // 空闲
KEY_Status_Debounce, // 消抖
KEY_Status_ConfirmPress, // 确认按下
KEY_Status_ConfirmPressLong, // 确认长按
KEY_Status_WaitSecondPress, // 等待再次按下(用于双击)
KEY_Status_SecondDebounce, // 再次消抖
KEY_Status_SecondPress, // 再次按下
} KEY_Status_TypeDef;
// 按键事件
typedef enum {
KEY_Event_Null, // 空事件
KEY_Event_SingleClick, // 单击
KEY_Event_LongPress, // 长按
KEY_Event_QuickClick, // 连击
KEY_Event_DoubleClick, // 双击
} KEY_Event_TypeDef;
// 按键模式使能选择(位掩码方式)
typedef enum {
KEY_Mode_OnlySinge = 0x00, // 只有单击
KEY_Mode_Long = 0x01, // 单击+长按
KEY_Mode_Quick = 0x02, // 单击+连按
KEY_Mode_Long_Quick = 0x03, // 单击+长按+连按
KEY_Mode_Double = 0x04, // 单击+双击
KEY_Mode_Long_Double = 0x05, // 单击+长按+双击
KEY_Mode_Quick_Double = 0x06, // 单击+连按+双击
KEY_Mode_Long_Quick_Double = 0x07, // 单击+长按+连按+双击
} KEY_Mode_TypeDef;
// 单个按键的配置与状态结构体
typedef struct {
uint8_t KEY_Label; // 按键标号,用于索引
KEY_Mode_TypeDef KEY_Mode; // 按键模式
uint16_t KEY_Count; // 按键按下计时器
KEY_Action_TypeDef KEY_Action; // 当前读取的按键动作
KEY_Status_TypeDef KEY_Status; // 当前按键状态
KEY_Event_TypeDef KEY_Event; // 触发的事件
} KEY_Configure_TypeDef;
extern KEY_Configure_TypeDef KeyConfig[];
extern KEY_Event_TypeDef key_event[];
void KEY_ReadStateMachine(KEY_Configure_TypeDef *KeyCfg);
#endif
2. 源文件实现 (my_key.c)
源文件包含状态机的核心逻辑和按键硬件读取接口。
#include "my_key.h"
#include <string.h> // 用于memset
// 硬件抽象层:读取指定按键的引脚电平(需根据实际硬件修改)
static uint8_t KEY_ReadPin(uint8_t key_label) {
switch (key_label) {
case 0: return (uint8_t)HAL_GPIO_ReadPin(K0_GPIO_Port, K0_Pin);
case 1: return (uint8_t)HAL_GPIO_ReadPin(K1_GPIO_Port, K1_Pin);
case 2: return (uint8_t)HAL_GPIO_ReadPin(K2_GPIO_Port, K2_Pin);
case 3: return (uint8_t)HAL_GPIO_ReadPin(K3_GPIO_Port, K3_Pin);
case 4: return (uint8_t)HAL_GPIO_ReadPin(K4_GPIO_Port, K4_Pin);
// 可根据需要扩展更多按键
// case X: return (uint8_t)HAL_GPIO_ReadPin(KX_GPIO_Port, KX_Pin);
}
return 1; // 默认返回未按下
}
// 全局按键配置数组(支持多按键)
KEY_Configure_TypeDef KeyConfig[] = {
// {按键标号, 工作模式, 计数初值, 初始动作, 初始状态, 初始事件}
{0, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
{1, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
{2, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
{3, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
{4, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
// {X, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
};
// 全局按键事件数组,主循环查询此数组
KEY_Event_TypeDef key_event[ARR_LEN(KeyConfig)] = {KEY_Event_Null};
// 按键状态机处理函数(需在定时器中断中周期调用)
void KEY_ReadStateMachine(KEY_Configure_TypeDef *KeyCfg) {
static uint16_t tmpcnt[ARR_LEN(KeyConfig)] = {0}; // 临时保存计时,用于双击逻辑
// 1. 读取当前物理按键动作
if (KEY_ReadPin(KeyCfg->KEY_Label) == KEY_PRESSED_LEVEL)
KeyCfg->KEY_Action = KEY_Action_Press;
else
KeyCfg->KEY_Action = KEY_Action_Release;
// 2. 状态机核心逻辑
switch (KeyCfg->KEY_Status) {
// 状态:空闲 ----------------------------------------------
case KEY_Status_Idle:
if (KeyCfg->KEY_Action == KEY_Action_Press) {
KeyCfg->KEY_Status = KEY_Status_Debounce; // 进入消抖状态
}
KeyCfg->KEY_Event = KEY_Event_Null;
break;
// 状态:消抖 ----------------------------------------------
case KEY_Status_Debounce:
if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DEBOUNCE_TIME)) {
// 消抖完成,确认按下
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_ConfirmPress;
} else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DEBOUNCE_TIME)) {
// 消抖中,计时
KeyCfg->KEY_Count++;
} else {
// 中途释放,视为抖动,回到空闲
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_Idle;
}
KeyCfg->KEY_Event = KEY_Event_Null;
break;
// 状态:确认按下 ------------------------------------------
case KEY_Status_ConfirmPress:
if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_LONG_PRESS_TIME)) {
// 达到长按时间
KeyCfg->KEY_Count = KEY_QUICK_CLICK_TIME; // 准备第一次连按触发
KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong;
} else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_LONG_PRESS_TIME)) {
// 未达到长按时间,持续计时
KeyCfg->KEY_Count++;
} else {
// 在达到长按时间前释放
if ((uint8_t)(KeyCfg->KEY_Mode) & 0x04) { // 如果使能了双击
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_WaitSecondPress; // 进入等待第二次按下状态
} else {
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_Idle;
KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 触发单击事件 ***
}
}
break;
// 状态:确认长按 ------------------------------------------
case KEY_Status_ConfirmPressLong:
if (KeyCfg->KEY_Action == KEY_Action_Press) {
// 持续按住
if ((uint8_t)KeyCfg->KEY_Mode & 0x02) { // 如果使能了连按
if (KeyCfg->KEY_Count >= KEY_QUICK_CLICK_TIME) {
// 连按间隔时间到,触发连按事件
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Event = KEY_Event_QuickClick; // *** 触发连按事件 ***
} else {
KeyCfg->KEY_Count++;
}
}
} else {
// 长按后释放
if ((uint8_t)KeyCfg->KEY_Mode & 0x01) { // 如果使能了长按
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_Idle;
KeyCfg->KEY_Event = KEY_Event_LongPress; // *** 触发长按事件 ***
} else {
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_Idle;
KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 未使能长按时,触发单击事件 ***
}
}
break;
// 状态:等待是否再次按下(用于双击判断)-------------------
case KEY_Status_WaitSecondPress:
if ((KeyCfg->KEY_Action != KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DOUBLE_CLICK_TIME)) {
// 等待超时,无第二次按下,触发第一次的单击事件
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_Idle;
KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 触发单击事件 ***
} else if ((KeyCfg->KEY_Action != KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DOUBLE_CLICK_TIME)) {
// 等待中,计时
KeyCfg->KEY_Count++;
} else {
// 在等待时间内再次按下
tmpcnt[KeyCfg->KEY_Label] = KeyCfg->KEY_Count; // 保存已等待的时间
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_SecondDebounce; // 进入第二次按下的消抖
}
KeyCfg->KEY_Event = KEY_Event_Null;
break;
// 状态:再次消抖 ------------------------------------------
case KEY_Status_SecondDebounce:
// 逻辑与第一次消抖类似
if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DEBOUNCE_TIME)) {
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_SecondPress;
} else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DEBOUNCE_TIME)) {
KeyCfg->KEY_Count++;
} else {
// 第二次按下消抖失败,恢复等待状态
KeyCfg->KEY_Count = KeyCfg->KEY_Count + tmpcnt[KeyCfg->KEY_Label];
KeyCfg->KEY_Status = KEY_Status_WaitSecondPress;
}
KeyCfg->KEY_Event = KEY_Event_Null;
break;
// 状态:再次按下 ------------------------------------------
case KEY_Status_SecondPress:
if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_LONG_PRESS_TIME)) {
// 第二次按下变为长按,则本次行为不构成双击
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong;
KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 先触发第一次的单击事件 ***
} else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_LONG_PRESS_TIME)) {
KeyCfg->KEY_Count++;
} else {
// 第二次按下后释放,且未变成长按,成功构成双击
KeyCfg->KEY_Count = 0;
KeyCfg->KEY_Status = KEY_Status_Idle;
KeyCfg->KEY_Event = KEY_Event_DoubleClick; // *** 触发双击事件 ***
}
break;
}
// 3. 将产生的事件记录到全局数组,供主循环查询
if (KeyCfg->KEY_Event != KEY_Event_Null) {
key_event[KeyCfg->KEY_Label] = KeyCfg->KEY_Event;
}
}
3. 定时器中断与主函数调用示例
假设定时器中断周期设置为1ms。
// 在定时器中断回调函数中调用状态机(1ms一次)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == htim1.Instance) { // 假设是TIM1
// 遍历处理所有已配置的按键
for (uint8_t i = 0; i < ARR_LEN(KeyConfig); i++) {
KEY_ReadStateMachine(&KeyConfig[i]);
}
}
}
// 主函数中的事件处理逻辑
int main(void) {
// 硬件初始化...
while (1) {
// 1. 执行其他后台任务...
// 2. 检查并处理按键事件(非阻塞)
if (key_event[0] == KEY_Event_SingleClick) {
// 处理按键0的单击事件
// function_for_key0_click();
}
if (key_event[1] == KEY_Event_LongPress) {
// 处理按键1的长按事件
// function_for_key1_longpress();
}
if (key_event[2] == KEY_Event_QuickClick) {
// 处理按键2的连按事件,连按可视为快速重复的单击
// function_for_key2_quickclick();
}
if (key_event[3] == KEY_Event_DoubleClick) {
// 处理按键3的双击事件
// function_for_key3_doubleclick();
}
// 3. 清除已处理的事件标志
memset(key_event, KEY_Event_Null, sizeof(key_event));
// 状态机的思想不仅适用于外设驱动,在复杂的网络协议解析或业务逻辑处理中也是核心的[软件设计模式](https://yunpan.plus/f/35-1)。
}
}
总结与资源
通过上述基于定时器中断的状态机实现,我们成功构建了一个健壮、高效且功能丰富的按键处理模块。该方法将耗时的事件识别过程放入中断,确保了主循环的流畅性,特别适合在实时性要求较高的嵌入式应用中使用。
