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;
}
}
}
运行与集成方式
- 硬件适配:实现
key_io_read(key_id)函数,返回对应引脚电平(按下为0,释放为1)。 - 时间基准:调用
key_module_set_tick_source(your_tick_ms_func)提供毫秒级时间戳。 - 初始化与注册:在系统启动时调用
key_module_init(),传入按键配置表。 - 周期调用:在定时中断或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)
该状态机确保只有电平稳定后才改变状态,且长按与短按逻辑互斥,避免重复上报。