2. 按键扫描的分层设计
任务说明
本案例要求设计一个按键扫描模块,核心目标是将底层IO读取与上层逻辑分离。你需要实现:一个底层驱动接口(负责读取引脚电平)、一个按键扫描逻辑层(负责消抖和状态检测),以及一个演示上层如何消费按键事件的示例。最终代码应能在不修改上层逻辑的前提下,通过替换底层驱动来适配不同硬件。
分层架构与接口定义
整个模块分为三层,依赖方向从上到下:
- 上层应用:调用按键扫描接口,获取按键事件(按下/释放/长按)。
- 按键扫描层:实现消抖、边沿检测和状态机,不直接操作硬件寄存器。
- 底层驱动:提供读取引脚电平的函数指针,由具体平台实现。
关键接口如下:
// 底层驱动:读取指定按键的当前电平(0=按下,1=释放,具体极性由驱动转换)
typedef uint8_t (*key_read_fn)(uint8_t key_id);
// 按键事件类型
typedef enum {
KEY_EVENT_NONE = 0,
KEY_EVENT_PRESSED, // 按下(已消抖确认)
KEY_EVENT_RELEASED, // 释放(已消抖确认)
KEY_EVENT_LONG_PRESS // 长按(持续按住超过阈值)
} key_event_t;
代码实现
// key_scanner.h
#ifndef KEY_SCANNER_H
#define KEY_SCANNER_H
#include <stdint.h>
typedef uint8_t (*key_read_fn)(uint8_t key_id);
typedef enum {
KEY_EVENT_NONE = 0,
KEY_EVENT_PRESSED,
KEY_EVENT_RELEASED,
KEY_EVENT_LONG_PRESS
} key_event_t;
typedef struct {
key_read_fn read_pin; // 底层IO读取函数
uint8_t debounce_ms; // 消抖时间(ms)
uint8_t long_press_ms; // 长按阈值(ms)
uint8_t scan_interval_ms; // 扫描周期(ms)
} key_config_t;
void key_scanner_init(const key_config_t* cfg, uint8_t key_count);
key_event_t key_scanner_scan(uint8_t key_id, uint32_t tick_ms);
uint8_t key_scanner_is_pressed(uint8_t key_id); // 查询当前稳定状态
#endif // KEY_SCANNER_H
// key_scanner.c
#include "key_scanner.h"
#define KEY_STATE_RELEASED 0
#define KEY_STATE_PRESSED 1
typedef struct {
uint8_t raw_level; // 上次原始电平
uint8_t stable_state; // 消抖后的稳定状态
uint8_t debounce_cnt; // 消抖计数
uint8_t debounce_ready; // 消抖完成标志
uint32_t press_start_tick; // 按下起始时间
uint8_t long_press_sent; // 长按事件已发送标志
} key_status_t;
static key_config_t s_cfg;
static uint8_t s_key_count;
static key_status_t s_keys[8]; // 最多支持8个按键
void key_scanner_init(const key_config_t* cfg, uint8_t key_count)
{
uint8_t i;
s_cfg = *cfg;
s_key_count = (key_count > 8) ? 8 : key_count;
for (i = 0; i < s_key_count; i++) {
s_keys[i].raw_level = s_cfg.read_pin(i);
s_keys[i].stable_state = KEY_STATE_RELEASED;
s_keys[i].debounce_cnt = 0;
s_keys[i].debounce_ready = 0;
s_keys[i].press_start_tick = 0;
s_keys[i].long_press_sent = 0;
}
}
key_event_t key_scanner_scan(uint8_t key_id, uint32_t tick_ms)
{
key_status_t* ks;
uint8_t cur_level;
key_event_t evt = KEY_EVENT_NONE;
if (key_id >= s_key_count) return KEY_EVENT_NONE;
ks = &s_keys[key_id];
cur_level = s_cfg.read_pin(key_id);
// 消抖:连续多次读到相同电平才确认状态变化
if (cur_level != ks->raw_level) {
ks->debounce_cnt++;
if (ks->debounce_cnt >= 3) { // 连续3次一致
ks->raw_level = cur_level;
ks->debounce_cnt = 0;
ks->debounce_ready = 1;
}
} else {
ks->debounce_cnt = 0;
}
// 状态机处理
if (ks->debounce_ready) {
ks->debounce_ready = 0;
if (cur_level == 0) { // 按下(低有效)
if (ks->stable_state == KEY_STATE_RELEASED) {
ks->stable_state = KEY_STATE_PRESSED;
ks->press_start_tick = tick_ms;
ks->long_press_sent = 0;
evt = KEY_EVENT_PRESSED;
}
} else { // 释放
if (ks->stable_state == KEY_STATE_PRESSED) {
ks->stable_state = KEY_STATE_RELEASED;
evt = KEY_EVENT_RELEASED;
}
}
}
// 长按检测
if (ks->stable_state == KEY_STATE_PRESSED && !ks->long_press_sent) {
if ((tick_ms - ks->press_start_tick) >= s_cfg.long_press_ms) {
ks->long_press_sent = 1;
evt = KEY_EVENT_LONG_PRESS;
}
}
return evt;
}
uint8_t key_scanner_is_pressed(uint8_t key_id)
{
if (key_id >= s_key_count) return 0;
return (s_keys[key_id].stable_state == KEY_STATE_PRESSED) ? 1 : 0;
}
底层驱动示例(模拟GPIO)
// 模拟底层驱动:使用静态变量模拟引脚电平
#include "key_scanner.h"
static uint8_t sim_pin_level[2] = {1, 1}; // 默认释放
// 模拟设置引脚电平(用于测试)
void sim_set_pin(uint8_t key_id, uint8_t level) {
if (key_id < 2) sim_pin_level[key_id] = level;
}
// 底层读取函数:返回0表示按下,1表示释放
static uint8_t sim_read_pin(uint8_t key_id) {
if (key_id >= 2) return 1;
return sim_pin_level[key_id];
}
// 获取驱动函数指针
key_read_fn get_sim_driver(void) {
return sim_read_pin;
}
上层应用示例
// main.c 示例
#include "key_scanner.h"
#include <stdio.h>
// 模拟时间戳
static uint32_t g_tick = 0;
void tick_advance(uint32_t ms) { g_tick += ms; }
int main(void)
{
key_config_t cfg;
key_event_t evt;
cfg.read_pin = get_sim_driver();
cfg.debounce_ms = 20; // 实际使用时结合扫描周期计算
cfg.long_press_ms = 1000;
cfg.scan_interval_ms = 5;
key_scanner_init(&cfg, 2);
// 模拟按键0按下(持续200ms后释放)
sim_set_pin(0, 0);
for (int i = 0; i < 40; i++) { // 40*5ms = 200ms
tick_advance(5);
evt = key_scanner_scan(0, g_tick);
if (evt == KEY_EVENT_PRESSED) printf("[%u] KEY0 PRESSED\n", g_tick);
if (evt == KEY_EVENT_LONG_PRESS) printf("[%u] KEY0 LONG_PRESS\n", g_tick);
}
sim_set_pin(0, 1);
for (int i = 0; i < 10; i++) {
tick_advance(5);
evt = key_scanner_scan(0, g_tick);
if (evt == KEY_EVENT_RELEASED) printf("[%u] KEY0 RELEASED\n", g_tick);
}
// 模拟按键1长按(持续1200ms)
sim_set_pin(1, 0);
for (int i = 0; i < 240; i++) {
tick_advance(5);
evt = key_scanner_scan(1, g_tick);
if (evt == KEY_EVENT_PRESSED) printf("[%u] KEY1 PRESSED\n", g_tick);
if (evt == KEY_EVENT_LONG_PRESS) printf("[%u] KEY1 LONG_PRESS\n", g_tick);
}
sim_set_pin(1, 1);
return 0;
}
SVG:按键扫描分层架构与状态流
编译与运行
# 将 key_scanner.h, key_scanner.c, 模拟驱动和 main.c 放在同一目录
gcc -o key_demo key_scanner.c main.c -I.
./key_demo
验证方法与判定标准
运行程序后,观察输出是否按以下顺序出现(时间戳随模拟递增):
[5] KEY0 PRESSED // 按下后约15ms消抖确认
[205] KEY0 RELEASED // 释放后约15ms消抖确认
[210] KEY1 PRESSED
[1210] KEY1 LONG_PRESS // 持续按住超过1000ms后触发
判定标准:
- 程序能编译通过并运行,输出与上述顺序一致(时间戳允许因扫描周期略有偏移);
- KEY0在200ms短按时只产生PRESSED和RELEASED事件,不产生LONG_PRESS;
- KEY1在持续1200ms时产生LONG_PRESS事件,且只产生一次;
- 将
sim_read_pin替换为真实硬件读取函数(如寄存器操作),上层代码和扫描层无需修改即可工作,证明分层有效。
注意:上述输出为基于代码逻辑的预期结果,实际运行请以你的终端显示为准。