一、背景
嵌入式按键一般键数不多,按键支持的页面比较多,同一个按键的功能较为复杂,并且按键需要支持多种模式,比如短按、长按,持续长按等。由于产品需要设计了一种高可用的按键处理方案。使用按键状态机,通过定时扫描来检测按键的状态。并且设计了多种模式标志位,当扫描过程中发现相关事件触发时标志位置位,然后在时间处理中获取标志位再清除。
二、按键设计
2.1、按键状态机设计
typedef struct {
// 硬件参数
Int8U id; // 按键ID
bool (*read)(void); // 按键读取函数
// 状态参数
struct
{
bool raw_state : 1; // 原始状态
bool stable_state : 1; // 稳定状态
bool repeat_armed : 1; // 重复触发就绪标志
};
// 时间参数
Int32U timestamp; // 状态变化时间戳
Int32U press_time; // 有效按下起始时间
Int32U repeat_time; // 重复触发时间戳
// 事件标志
Int8U event_flag; // 事件标志位
}KeyState;
固定消抖时间为:20ms
每一个按键单独初始化一个状态机,并且注册每一个按键的单独读取函数。我这儿设计有三种事件标志
短按:100ms触发。对应0x01
长按:1000ms触发。对应0x02
长按不释放(类似于音量键):超过1000ms之后每多按300ms触发一次。对应0x04
#define DEBOUNCE_TIME 20 // 消抖时间20ms
#define SHORT_PRESS_MS 100 // 短按时间阈值
#define LONG_PRESS_MS 1000 // 长按时间阈值
#define REPEAT_INTERVAL_MS 300 // 持续触发间隔
typedef enum
{
KEY_SHORT_PRESS = 0x01,
KEY_LONG_PRESS = 0x02,
KEY_CONTINUOUS_PRESS= 0x04, // 持续长按标志(首次达到阈值后周期触发)
}KeyEventFlag;
2.2、按键初始化和注册
/* 按键对象数组 */
KeyState keys[8];
/**
* 按键状态机初始化
*/
void key_init(void)
{
Int8U i;
for(i = 0; i < 8; i++)
{
keys[i].id = i;
switch (i)
{
case 0:
keys[i].read = key1_read_func;
break;
case 1:
keys[i].read = key2_read_func;
break;
case 2:
keys[i].read = key3_read_func;
break;
case 3:
keys[i].read = key4_read_func;
break;
case 4:
keys[i].read = key5_read_func;
break;
case 5:
keys[i].read = key6_read_func;
break;
case 6:
keys[i].read = key7_read_func;
break;
case 7:
keys[i].read = key8_read_func;
break;
}
keys[i].raw_state = 0;
keys[i].stable_state = 0;
keys[i].repeat_armed = 0;
keys[i].timestamp = 0;
keys[i].press_time = 0;
keys[i].repeat_time = 0;
keys[i].event_flag = 0;
}
}
/********************按键外设相关**************************/
bool key1_read_func(void)
{
return POWER_PRESS ? true:false;
}
bool key2_read_func(void)
{
return RIGHT_PRESS ? true:false;
}
bool key3_read_func(void)
{
return TEST_PRESS ? true:false;
}
bool key4_read_func(void)
{
return CONFIRM_PRESS ? true:false;
}
bool key5_read_func(void)
{
return CONN_PRESS ? true:false;
}
bool key6_read_func(void)
{
return LEFT_PRESS ? true:false;
}
bool key7_read_func(void)
{
return SEL_PRESS ? true:false;
}
bool key8_read_func(void)
{
return BACK_PRESS ? true:false;
}
我这儿一共有8个按键,所以初始化定义了一个大小为8的KeyState数组,用来存储8个按键的状态机变化。并初始化时除了读取按键状态的函数外都初始化为0。
2.3、按键扫描
#define GET_TICK() SYS_GetTick() // 系统时基(1ms一个tick)
/******************** 按键处理核心逻辑 ********************/
void key_scan_task(void)
{
static Int32U last_scan = 0;
Int32U now = GET_TICK();
// 固定周期扫描(建议5-10ms)
if(now - last_scan < 5) return;
last_scan = now;
for(Int8U i = 0; i < sizeof(keys)/sizeof(KeyState); i++)
{
KeyState* key = &keys[i];
bool current = key->read();
/* 状态变化检测 */
if(current != key->raw_state)
{
key->raw_state = current;
key->timestamp = now;
}
/* 消抖处理 */
if((now - key->timestamp) > DEBOUNCE_TIME)
{
bool state_changed = (key->stable_state != current);
key->stable_state = current;
/* 按下事件处理 */
if(state_changed && current)
{
key->press_time = now;
key->repeat_armed = true;
}
/* 释放事件处理 */
if(state_changed && !current)
{
if(key->press_time != 0)
{
// 常规事件处理
uint32_t duration = now - key->press_time;
if(duration >= LONG_PRESS_MS)
{
key->event_flag |= KEY_LONG_PRESS;
}
else
{
key->event_flag |= KEY_SHORT_PRESS;
}
// 释放时清除持续按压标志
key->event_flag &= ~KEY_CONTINUOUS_PRESS;
key->press_time = 0;
key->repeat_armed = false;
key->repeat_time = 0;
}
}
}
/* 持续长按检测 */
if(key->stable_state && key->press_time)
{
uint32_t duration = now - key->press_time;
// 首次达到长按阈值
if(duration >= LONG_PRESS_MS && key->repeat_armed)
{
key->event_flag |= KEY_CONTINUOUS_PRESS;
key->repeat_time = now;
key->repeat_armed = false; // 防止重复首次触发
}
// 后续周期触发
if(!key->repeat_armed && (now - key->repeat_time) >= REPEAT_INTERVAL_MS)
{
key->event_flag |= KEY_CONTINUOUS_PRESS;
key->repeat_time = now;
}
}
}
}
按键扫描一般放在主程序逻辑,每隔5-10ms调用一次。使用的计时函数可以根据系统自定义。我这儿是自定义的函数。定义了一个全局变量,在系统systick计数器中上电即不停累加。
2.4、用户接口
/******************** 用户接口函数 ********************/
// 获取按键事件(自动清除标志)
Int8U key_get_event(Int8U key_id)
{
if(key_id > sizeof(keys)/sizeof(KeyState))
{
return 0;
}
Int8U flag = keys[key_id].event_flag;
keys[key_id].event_flag = 0; // 清除标志
return flag;
}
// 检查是否有事件(非清除方式)
Int8U key_peek_event(Int8U key_id)
{
if(key_id > sizeof(keys)/sizeof(KeyState))
{
return 0;
}
return keys[key_id].event_flag;
}
// 按键清除所有标志位
void key_clear_flag(void)
{
Int8U i;
for(i = 0; i < 8; i++)
{
keys[i].raw_state = 0;
keys[i].stable_state = 0;
keys[i].repeat_armed = 0;
keys[i].timestamp = 0;
keys[i].press_time = 0;
keys[i].repeat_time = 0;
keys[i].event_flag = 0;
}
}
一般来说如果是获取标志位的方法的话,用户一般会处理完相关事件才会清除按键标志位。这种按键状态机设计不是通过按键时间触发并调用回调函数处理,如果调用key_get_event函数在主程序中的话,会每次自动清除标志位。无法满足需要,可以自主调用key_peek_event函数处理完事件后再手动调用key_clear_flag清除标志位
三、具体实现代码
3.1、key.h
#ifndef __KEY_H
#define __KEY_H
#define POWER_PRESS (gpio_input_bit_get(GPIOA,GPIO_PIN_4) != SET)
#define RIGHT_PRESS (gpio_input_bit_get(GPIOA,GPIO_PIN_5) != SET)
#define TEST_PRESS (gpio_input_bit_get(GPIOA,GPIO_PIN_6) != SET)
#define CONFIRM_PRESS (gpio_input_bit_get(GPIOA,GPIO_PIN_7) != SET)
#define CONN_PRESS (gpio_input_bit_get(GPIOC,GPIO_PIN_4) != SET)
#define LEFT_PRESS (gpio_input_bit_get(GPIOC,GPIO_PIN_5) != SET)
#define SEL_PRESS (gpio_input_bit_get(GPIOB,GPIO_PIN_0) != SET)
#define BACK_PRESS (gpio_input_bit_get(GPIOA,GPIO_PIN_0) != SET)
#define GET_TICK() SYS_GetTick() // 系统时基(1ms一个tick)
#define DEBOUNCE_TIME 20 // 消抖时间20ms
#define SHORT_PRESS_MS 100 // 短按时间阈值
#define LONG_PRESS_MS 1000 // 长按时间阈值
#define REPEAT_INTERVAL_MS 300 // 持续触发间隔
typedef enum
{
KEY_SHORT_PRESS = 0x01,
KEY_LONG_PRESS = 0x02,
KEY_CONTINUOUS_PRESS= 0x04, // 持续长按标志(首次达到阈值后周期触发)
}KeyEventFlag;
typedef enum
{
KEY_POWER = 0,
KEY_RIGHT = 1,
KEY_TEST = 2,
KEY_CONFIRM = 3,
KEY_CONN = 4,
KEY_LEFT = 5,
KEY_SEL = 6,
KEY_BACK = 7,
}KEY_NO_ENUM;
typedef struct {
// 硬件参数
Int8U id; // 按键ID
bool (*read)(void); // 按键读取函数
// 状态参数
struct
{
bool raw_state : 1; // 原始状态
bool stable_state : 1; // 稳定状态
bool repeat_armed : 1; // 重复触发就绪标志
};
// 时间参数
Int32U timestamp; // 状态变化时间戳
Int32U press_time; // 有效按下起始时间
Int32U repeat_time; // 重复触发时间戳
// 事件标志
Int8U event_flag; // 事件标志位
}KeyState;
bool key1_read_func(void);
bool key2_read_func(void);
bool key3_read_func(void);
bool key4_read_func(void);
bool key5_read_func(void);
bool key6_read_func(void);
bool key7_read_func(void);
bool key8_read_func(void);
void key_init(void);
void key_scan_task(void);
Int8U key_get_event(Int8U key_id);
Int8U key_peek_event(Int8U key_id);
void key_clear_flag(void);
#endif
3.2、key.c
#include "key.h"
/* 按键对象数组 */
KeyState keys[8];
/********************按键外设相关**************************/
bool key1_read_func(void)
{
return POWER_PRESS ? true:false;
}
bool key2_read_func(void)
{
return RIGHT_PRESS ? true:false;
}
bool key3_read_func(void)
{
return TEST_PRESS ? true:false;
}
bool key4_read_func(void)
{
return CONFIRM_PRESS ? true:false;
}
bool key5_read_func(void)
{
return CONN_PRESS ? true:false;
}
bool key6_read_func(void)
{
return LEFT_PRESS ? true:false;
}
bool key7_read_func(void)
{
return SEL_PRESS ? true:false;
}
bool key8_read_func(void)
{
return BACK_PRESS ? true:false;
}
/**
* 按键状态机初始化
*/
void key_init(void)
{
Int8U i;
for(i = 0; i < 8; i++)
{
keys[i].id = i;
switch (i)
{
case 0:
keys[i].read = key1_read_func;
break;
case 1:
keys[i].read = key2_read_func;
break;
case 2:
keys[i].read = key3_read_func;
break;
case 3:
keys[i].read = key4_read_func;
break;
case 4:
keys[i].read = key5_read_func;
break;
case 5:
keys[i].read = key6_read_func;
break;
case 6:
keys[i].read = key7_read_func;
break;
case 7:
keys[i].read = key8_read_func;
break;
}
keys[i].raw_state = 0;
keys[i].stable_state = 0;
keys[i].repeat_armed = 0;
keys[i].timestamp = 0;
keys[i].press_time = 0;
keys[i].repeat_time = 0;
keys[i].event_flag = 0;
}
}
/******************** 按键处理核心逻辑 ********************/
void key_scan_task(void)
{
static Int32U last_scan = 0;
Int32U now = GET_TICK();
// 固定周期扫描(建议5-10ms)
if(now - last_scan < 5) return;
last_scan = now;
for(Int8U i = 0; i < sizeof(keys)/sizeof(KeyState); i++)
{
KeyState* key = &keys[i];
bool current = key->read();
/* 状态变化检测 */
if(current != key->raw_state)
{
key->raw_state = current;
key->timestamp = now;
}
/* 消抖处理 */
if((now - key->timestamp) > DEBOUNCE_TIME)
{
bool state_changed = (key->stable_state != current);
key->stable_state = current;
/* 按下事件处理 */
if(state_changed && current)
{
key->press_time = now;
key->repeat_armed = true;
}
/* 释放事件处理 */
if(state_changed && !current)
{
if(key->press_time != 0)
{
// 常规事件处理
uint32_t duration = now - key->press_time;
if(duration >= LONG_PRESS_MS)
{
key->event_flag |= KEY_LONG_PRESS;
}
else
{
key->event_flag |= KEY_SHORT_PRESS;
}
// 释放时清除持续按压标志
key->event_flag &= ~KEY_CONTINUOUS_PRESS;
key->press_time = 0;
key->repeat_armed = false;
key->repeat_time = 0;
}
}
}
/* 持续长按检测 */
if(key->stable_state && key->press_time)
{
uint32_t duration = now - key->press_time;
// 首次达到长按阈值
if(duration >= LONG_PRESS_MS && key->repeat_armed)
{
key->event_flag |= KEY_CONTINUOUS_PRESS;
key->repeat_time = now;
key->repeat_armed = false; // 防止重复首次触发
}
// 后续周期触发
if(!key->repeat_armed && (now - key->repeat_time) >= REPEAT_INTERVAL_MS)
{
key->event_flag |= KEY_CONTINUOUS_PRESS;
key->repeat_time = now;
}
}
}
}
/******************** 用户接口函数 ********************/
// 获取按键事件(自动清除标志)
Int8U key_get_event(Int8U key_id)
{
if(key_id > sizeof(keys)/sizeof(KeyState))
{
return 0;
}
Int8U flag = keys[key_id].event_flag;
keys[key_id].event_flag = 0; // 清除标志
return flag;
}
// 检查是否有事件(非清除方式)
Int8U key_peek_event(Int8U key_id)
{
if(key_id > sizeof(keys)/sizeof(KeyState))
{
return 0;
}
return keys[key_id].event_flag;
}
/**
* 按键清除标志位
*/
void key_clear_flag(void)
{
Int8U i;
for(i = 0; i < 8; i++)
{
keys[i].raw_state = 0;
keys[i].stable_state = 0;
keys[i].repeat_armed = 0;
keys[i].timestamp = 0;
keys[i].press_time = 0;
keys[i].repeat_time = 0;
keys[i].event_flag = 0;
}
}
四、调用示例
// 短按
if(key_peek_event(KEY_SEL) & KEY_SHORT_PRESS)
{
key_clear_flag(); // 清除所有标志位
// todo 用户处理事件
}
else if(key_peek_event(KEY_SEL) & KEY_LONG_PRESS)
{
key_clear_flag(); // 清除所有标志位
// todo 用户处理事件
}
else if(key_peek_event(KEY_SEL) & KEY_CONTINUOUS_PRESS)
{
key_clear_flag(); // 清除所有标志位
// todo 用户处理事件
}