嵌入式key状态机设计(通用)

一、背景

嵌入式按键一般键数不多,按键支持的页面比较多,同一个按键的功能较为复杂,并且按键需要支持多种模式,比如短按、长按,持续长按等。由于产品需要设计了一种高可用的按键处理方案。使用按键状态机,通过定时扫描来检测按键的状态。并且设计了多种模式标志位,当扫描过程中发现相关事件触发时标志位置位,然后在时间处理中获取标志位再清除。

二、按键设计

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 用户处理事件
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容