2. 按键扫描与去抖模块

任务目标

设计一个可复用的按键扫描模块,核心解决两个问题:机械按键的抖动消除,以及按键事件与业务逻辑的解耦。模块通过事件回调向上层通知按键状态变化,上层无需关心扫描细节。

模块接口设计

模块采用“扫描-去抖-回调”三层结构。对外仅暴露初始化、注册回调和周期扫描三个接口。去抖采用状态机方式,避免阻塞式延时,适合在定时中断或RTOS tick中周期调用。

关键完整代码

/* key_module.h */
#ifndef KEY_MODULE_H
#define KEY_MODULE_H

#include <stdint.h>

typedef enum {
    KEY_EVENT_PRESSED = 0,
    KEY_EVENT_RELEASED,
    KEY_EVENT_CLICKED,      /* 短按(按下到释放 < 500ms) */
    KEY_EVENT_LONG_PRESSED  /* 长按(持续按住 >= 1000ms) */
} key_event_t;

typedef void (*key_callback_t)(uint8_t key_id, key_event_t event);

typedef struct {
    uint8_t key_id;
    key_callback_t callback;
} key_config_t;

void key_module_init(const key_config_t *configs, uint8_t count);
void key_module_register_callback(uint8_t key_id, key_callback_t cb);
void key_module_scan(void);  /* 周期调用,建议 5ms~10ms 间隔 */

#endif /* KEY_MODULE_H */
/* key_module.c */
#include "key_module.h"
#include <stdbool.h>

#define DEBOUNCE_THRESHOLD   3   /* 连续采样次数,去抖 */
#define LONG_PRESS_MS        1000
#define CLICK_MAX_MS         500

typedef enum {
    ST_IDLE = 0,
    ST_DEBOUNCE_PRESS,
    ST_PRESSED,
    ST_DEBOUNCE_RELEASE
} key_state_t;

typedef struct {
    uint8_t pin_level;          /* 当前实际电平(0按下/1释放,由硬件层提供) */
    key_state_t state;
    uint8_t debounce_cnt;
    uint32_t press_start_ms;
    uint32_t release_ms;
    bool long_press_reported;
    key_callback_t callback;
} key_obj_t;

static key_obj_t key_objs[8];
static uint8_t key_count = 0;
static uint32_t (*get_tick_ms)(void) = NULL;

void key_module_set_tick_source(uint32_t (*tick_func)(void)) {
    get_tick_ms = tick_func;
}

void key_module_init(const key_config_t *configs, uint8_t count) {
    if (count > 8) count = 8;
    key_count = count;
    for (uint8_t i = 0; i < count; i++) {
        key_objs[i].pin_level = 1;   /* 默认释放 */
        key_objs[i].state = ST_IDLE;
        key_objs[i].debounce_cnt = 0;
        key_objs[i].press_start_ms = 0;
        key_objs[i].release_ms = 0;
        key_objs[i].long_press_reported = false;
        key_objs[i].callback = configs[i].callback;
    }
}

void key_module_register_callback(uint8_t key_id, key_callback_t cb) {
    if (key_id < key_count) {
        key_objs[key_id].callback = cb;
    }
}

/* 硬件层需提供此函数:读取按键电平,按下返回0,释放返回1 */
extern uint8_t key_io_read(uint8_t key_id);

static void notify(uint8_t key_id, key_event_t ev) {
    if (key_objs[key_id].callback) {
        key_objs[key_id].callback(key_id, ev);
    }
}

void key_module_scan(void) {
    if (!get_tick_ms) return;
    uint32_t now = get_tick_ms();

    for (uint8_t i = 0; i < key_count; i++) {
        key_obj_t *k = &key_objs[i];
        uint8_t level = key_io_read(i);

        switch (k->state) {
        case ST_IDLE:
            if (level == 0) {
                k->debounce_cnt++;
                if (k->debounce_cnt >= DEBOUNCE_THRESHOLD) {
                    k->state = ST_DEBOUNCE_PRESS;
                    k->debounce_cnt = 0;
                }
            } else {
                k->debounce_cnt = 0;
            }
            break;

        case ST_DEBOUNCE_PRESS:
            if (level == 0) {
                k->state = ST_PRESSED;
                k->press_start_ms = now;
                k->long_press_reported = false;
                notify(i, KEY_EVENT_PRESSED);
            } else {
                k->state = ST_IDLE;  /* 抖动,取消 */
            }
            break;

        case ST_PRESSED:
            if (level == 1) {
                k->state = ST_DEBOUNCE_RELEASE;
                k->debounce_cnt = 0;
                k->release_ms = now;
            } else {
                /* 长按检测 */
                if (!k->long_press_reported &&
                    (now - k->press_start_ms) >= LONG_PRESS_MS) {
                    k->long_press_reported = true;
                    notify(i, KEY_EVENT_LONG_PRESSED);
                }
            }
            break;

        case ST_DEBOUNCE_RELEASE:
            if (level == 1) {
                k->debounce_cnt++;
                if (k->debounce_cnt >= DEBOUNCE_THRESHOLD) {
                    k->state = ST_IDLE;
                    k->debounce_cnt = 0;
                    if (!k->long_press_reported) {
                        /* 短按判定 */
                        if ((k->release_ms - k->press_start_ms) < CLICK_MAX_MS) {
                            notify(i, KEY_EVENT_CLICKED);
                        }
                    }
                    notify(i, KEY_EVENT_RELEASED);
                }
            } else {
                k->state = ST_PRESSED;  /* 回抖,回到按下 */
                k->debounce_cnt = 0;
            }
            break;
        }
    }
}

运行与集成方式

  1. 硬件适配:实现 key_io_read(key_id) 函数,返回对应引脚电平(按下为0,释放为1)。
  2. 时间基准:调用 key_module_set_tick_source(your_tick_ms_func) 提供毫秒级时间戳。
  3. 初始化与注册:在系统启动时调用 key_module_init(),传入按键配置表。
  4. 周期调用:在定时中断或RTOS任务中每5~10ms调用一次 key_module_scan()。

验证方法

编写一个最小测试工程(如STM32裸机或Linux用户态模拟),按下述步骤验证:

/* 测试用例伪代码 */
void test_callback(uint8_t key_id, key_event_t ev) {
    printf("Key%d Event:%d\n", key_id, ev);
}

/* 模拟按键时序 */
key_io_read 返回: 1 1 0 0 0 1 1 1  (间隔10ms)
预期输出序列:
  Key0 Event:0 (PRESSED)
  Key0 Event:2 (CLICKED)
  Key0 Event:1 (RELEASED)

通过条件:

  • 快速抖动(如 0 1 0 1 0 在5ms内)不会触发任何事件;
  • 按住超过1000ms触发一次 LONG_PRESSED,释放时不再触发 CLICKED;
  • 短按(<500ms)释放后依次收到 PRESSED、CLICKED、RELEASED 三个事件。

去抖状态机说明

状态迁移图(以单键为例):

        连续3次读到0         读到1(抖动)
IDLE ──────────────────> DEBOUNCE_PRESS ──────────> IDLE
                              |
                              | 再次读到0
                              v
                           PRESSED ──────────────> 长按超时 → 上报LONG_PRESSED
                              |
                              | 读到1
                              v
                        DEBOUNCE_RELEASE
                              |
                              | 连续3次读到1
                              v
                            IDLE (上报CLICKED/RELEASED)

该状态机确保只有电平稳定后才改变状态,且长按与短按逻辑互斥,避免重复上报。