北屋教程网

专注编程知识分享,从入门到精通的编程学习平台

产品级的按键输入系统设计:去抖、识别与状态机实践

在嵌入式产品开发中,按键输入看似简单,但要实现产品级的稳定性和交互体验,需要考虑多个细节:硬件抖动、长按/短按/连击的识别、响应延迟、误触容错等。尤其在一些工业控制或消费电子产品中,按键响应的准确性与用户体验直接相关。

本文将结合实际经验,围绕产品级按键系统的核心问题展开,包括:软件去抖动、按键事件识别(单击、双击、长按)、基于状态机的设计思路,并辅以清晰的代码示例。


一、按键抖动的本质与去抖方法

机械式按键在触发时会产生数十毫秒的抖动信号,如图所示:

高电平 ——┐    ┌────┐   ┌───┐
           └────┘    └───┘
                ↑抖动阶段约5~20ms

若不处理这些抖动,将误触发多次按键事件。典型的软件去抖方法有两种:

1.1 延时法(简单粗暴)

#define KEY_PIN   HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)

bool read_key()
{
    static bool key_last = false;
    bool key_now = KEY_PIN;

    if (key_now != key_last)
    {
        HAL_Delay(20); // 固定延时20ms
        key_now = KEY_PIN;
    }

    key_last = key_now;
    return key_now;
}

适用于轻量任务,但阻塞式HAL_Delay()在多任务或RTOS下不推荐。

1.2 定时器采样 + 滑动窗口法(推荐)

#define KEY_FILTER_TIME 5

typedef struct {
    uint8_t filter_cnt;
    uint8_t stable_state;
    uint8_t last_state;
} KeyFilter_t;

KeyFilter_t key1 = {0};

void key_filter_task() // 每10ms调用一次
{
    uint8_t cur = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
    
    if (cur == key1.last_state)
    {
        if (key1.filter_cnt < KEY_FILTER_TIME)
            key1.filter_cnt++;
        else
            key1.stable_state = cur; // 过滤成功,状态更新
    }
    else
    {
        key1.filter_cnt = 0;
    }

    key1.last_state = cur;
}

该方案适用于RTOS或主循环中周期性调用,无阻塞,去抖效果稳定。


二、按键事件识别(单击、双击、长按)

产品级系统往往需支持复杂交互。例如:

  • 短按:执行基本操作
  • 长按:进入配置/复位模式
  • 双击/多击:执行特殊功能

关键是精确识别不同的按键时序。常用方法是记录按下/释放的时间戳,并在定时任务中分析事件。

2.1 实用结构体定义

typedef enum {
    KEY_IDLE,
    KEY_PRESS,
    KEY_RELEASE,
    KEY_LONG,
    KEY_DOUBLE
} KeyEvent_t;

typedef struct {
    uint8_t stable_state;
    uint8_t last_state;
    uint8_t press_flag;
    uint32_t press_time;
    uint32_t release_time;
    uint8_t click_count;
    KeyEvent_t event;
} KeyCtrl_t;

2.2 状态控制逻辑(每10ms调用)

#define KEY_DOWN_LEVEL     0
#define LONG_PRESS_TIME    100   // 100 * 10ms = 1s
#define DOUBLE_CLICK_TIME  30    // 300ms

void key_scan(KeyCtrl_t *key, uint8_t read_level)
{
    // 状态变化检测
    if (read_level != key->last_state)
    {
        key->last_state = read_level;
        if (read_level == KEY_DOWN_LEVEL)
        {
            key->press_time = 0;
            key->press_flag = 1;
        }
        else
        {
            key->release_time = 0;
            if (key->press_time < LONG_PRESS_TIME)
                key->click_count++;  // 累积点击次数
            key->press_flag = 0;
        }
    }

    // 长按识别
    if (key->press_flag)
    {
        key->press_time++;
        if (key->press_time == LONG_PRESS_TIME)
            key->event = KEY_LONG;
    }

    // 点击识别(释放后计时)
    if (!key->press_flag && key->click_count > 0)
    {
        key->release_time++;
        if (key->release_time > DOUBLE_CLICK_TIME)
        {
            if (key->click_count == 1)
                key->event = KEY_PRESS;
            else if (key->click_count == 2)
                key->event = KEY_DOUBLE;

            key->click_count = 0;
            key->release_time = 0;
        }
    }
}

调用方式:

KeyCtrl_t key1;
void SysTick_Handler() // 每10ms调用
{
    uint8_t key_level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
    key_scan(&key1, key_level);
}

2.3 事件处理

在主循环中读取:

switch (key1.event)
{
    case KEY_PRESS:
        // 短按操作
        do_action();
        break;
    case KEY_LONG:
        // 长按复位
        system_reset();
        break;
    case KEY_DOUBLE:
        // 双击切换模式
        toggle_mode();
        break;
    default:
        break;
}

key1.event = KEY_IDLE; // 清除事件

三、引入状态机的设计优势

在产品级设计中,代码清晰度和可维护性极其重要。直接用变量堆叠判断逻辑容易混乱。状态机设计是一种简洁的思路:每种按键状态对应一个具体行为转换条件。

3.1 状态枚举

typedef enum {
    ST_IDLE,
    ST_WAIT_RELEASE,
    ST_WAIT_SECOND_PRESS,
    ST_LONG_PRESS
} KeyState_t;

3.2 状态切换实现

void key_state_machine(KeyCtrl_t *key)
{
    static KeyState_t state = ST_IDLE;

    switch (state)
    {
        case ST_IDLE:
            if (key->stable_state == KEY_DOWN_LEVEL)
            {
                key->press_time = 0;
                state = ST_WAIT_RELEASE;
            }
            break;

        case ST_WAIT_RELEASE:
            if (key->stable_state != KEY_DOWN_LEVEL)
            {
                if (key->press_time < LONG_PRESS_TIME)
                    state = ST_WAIT_SECOND_PRESS;
                else
                    key->event = KEY_LONG, state = ST_IDLE;
            }
            else
            {
                key->press_time++;
                if (key->press_time >= LONG_PRESS_TIME)
                    key->event = KEY_LONG, state = ST_IDLE;
            }
            break;

        case ST_WAIT_SECOND_PRESS:
            key->release_time++;
            if (key->stable_state == KEY_DOWN_LEVEL)
            {
                key->event = KEY_DOUBLE;
                state = ST_IDLE;
            }
            else if (key->release_time > DOUBLE_CLICK_TIME)
            {
                key->event = KEY_PRESS;
                state = ST_IDLE;
            }
            break;

        default:
            state = ST_IDLE;
            break;
    }
}

四、总结与建议

  1. 去抖是基础:推荐使用定时采样 + 滑动滤波方式,兼顾实时性和准确性。
  2. 事件识别需明确时序:长按、双击等需合理时间窗口与状态标记。
  3. 状态机利于扩展:可读性好,便于多键支持、增加按键组合等高级功能。
  4. 避免阻塞逻辑:无论是delay或while等待,都应尽量避免使用在中断或主循环中。

按键虽然是最基础的输入方式之一,但在产品级别的设计中,它体现的是系统响应能力、用户体验和设计规范的综合考量。

当然也参考一个开源按键网站:
https://github.com/murphyzhao/FlexibleButton

关注我,获取更多技术干货

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言