4. 中间件层按键扫描模块
任务定义
在MCU嵌入式软件分层架构中,中间件层位于驱动层与应用层之间,为应用提供与硬件无关的通用服务。本案例实现一个独立于具体MCU型号的按键扫描中间件模块,核心目标为:周期性调用扫描函数,完成按键去抖、边沿检测(按下/释放事件)与连击计数,并通过回调函数将事件上报给应用层。模块不直接操作寄存器,所有硬件访问通过驱动层接口函数指针注入,从而保证可移植性与可测试性。
模块接口设计
按键扫描模块对外暴露三个核心接口:初始化、周期扫描、事件回调注册。硬件差异(GPIO读取方式)通过函数指针抽象,由驱动层在初始化时注入。模块内部维护按键状态机,每个按键独立处理去抖与边沿检测。
代码实现
以下为按键扫描模块的完整实现,包含头文件与源文件。模块支持最多4个按键,去抖时间与连击间隔可通过宏配置。驱动层只需提供读取按键电平的函数指针,模块内部完成全部状态处理。
/* key_scan.h */
#ifndef KEY_SCAN_H
#define KEY_SCAN_H
#include <stdint.h>
#define KEY_MAX_NUM 4
#define KEY_DEBOUNCE_MS 20
#define KEY_REPEAT_MS 500
typedef enum {
KEY_EVENT_PRESS = 0,
KEY_EVENT_RELEASE,
KEY_EVENT_REPEAT
} key_event_t;
typedef uint8_t (*key_read_func_t)(uint8_t key_id);
typedef void (*key_callback_t)(uint8_t key_id, key_event_t event);
typedef struct {
uint8_t state; /* 0:IDLE 1:DEBOUNCE_PRESS 2:PRESSED 3:DEBOUNCE_RELEASE */
uint8_t debounce_cnt;
uint16_t press_cnt;
uint16_t repeat_timer;
} key_state_t;
void key_scan_init(key_read_func_t read_func);
void key_scan_register_callback(key_callback_t cb);
void key_scan_tick(uint8_t key_id, uint16_t tick_ms);
#endif /* KEY_SCAN_H */
/* key_scan.c */
#include "key_scan.h"
static key_read_func_t s_read_func = 0;
static key_callback_t s_callback = 0;
static key_state_t s_keys[KEY_MAX_NUM];
void key_scan_init(key_read_func_t read_func)
{
uint8_t i;
s_read_func = read_func;
for (i = 0; i < KEY_MAX_NUM; i++) {
s_keys[i].state = 0;
s_keys[i].debounce_cnt = 0;
s_keys[i].press_cnt = 0;
s_keys[i].repeat_timer = 0;
}
}
void key_scan_register_callback(key_callback_t cb)
{
s_callback = cb;
}
void key_scan_tick(uint8_t key_id, uint16_t tick_ms)
{
uint8_t level;
key_state_t *k;
if (!s_read_func || key_id >= KEY_MAX_NUM) {
return;
}
k = &s_keys[key_id];
level = s_read_func(key_id);
switch (k->state) {
case 0: /* IDLE */
if (level == 0) { /* 按下为低电平 */
k->state = 1;
k->debounce_cnt = 0;
}
break;
case 1: /* DEBOUNCE_PRESS */
if (level == 0) {
if (++k->debounce_cnt >= KEY_DEBOUNCE_MS / tick_ms) {
k->state = 2;
k->press_cnt++;
k->repeat_timer = 0;
if (s_callback) {
s_callback(key_id, KEY_EVENT_PRESS);
}
}
} else {
k->state = 0; /* 抖动,回到IDLE */
}
break;
case 2: /* PRESSED */
if (level != 0) {
k->state = 3;
k->debounce_cnt = 0;
} else {
/* 连击检测 */
k->repeat_timer += tick_ms;
if (k->repeat_timer >= KEY_REPEAT_MS) {
k->repeat_timer = 0;
k->press_cnt++;
if (s_callback) {
s_callback(key_id, KEY_EVENT_REPEAT);
}
}
}
break;
case 3: /* DEBOUNCE_RELEASE */
if (level != 0) {
if (++k->debounce_cnt >= KEY_DEBOUNCE_MS / tick_ms) {
k->state = 0;
if (s_callback) {
s_callback(key_id, KEY_EVENT_RELEASE);
}
}
} else {
k->state = 2; /* 抖动,回到PRESSED */
}
break;
default:
k->state = 0;
break;
}
}
驱动层适配示例
以下为驱动层GPIO读取函数的实现示例,演示如何将具体硬件操作注入中间件模块。实际项目中,该函数内部应包含寄存器读写操作。
/* driver_gpio.c - 驱动层适配示例 */
#include "key_scan.h"
/* 模拟GPIO电平状态,实际项目中替换为寄存器读取 */
static uint8_t s_gpio_level[KEY_MAX_NUM] = {1, 1, 1, 1};
/* 驱动层读取函数:返回0表示按下,1表示释放 */
static uint8_t gpio_read(uint8_t key_id)
{
return s_gpio_level[key_id];
}
/* 供外部测试或中断服务函数调用来更新电平 */
void driver_gpio_set_level(uint8_t key_id, uint8_t level)
{
if (key_id < KEY_MAX_NUM) {
s_gpio_level[key_id] = level;
}
}
void driver_gpio_init(void)
{
key_scan_init(gpio_read);
}
应用层回调示例
应用层通过注册回调函数接收按键事件。以下示例演示了回调函数的典型用法,将按键事件映射到具体业务逻辑。
/* app_main.c - 应用层使用示例 */
#include "key_scan.h"
#include <stdio.h>
static void app_key_callback(uint8_t key_id, key_event_t event)
{
const char *event_str[] = {"PRESS", "RELEASE", "REPEAT"};
printf("Key%d %s\n", key_id, event_str[event]);
}
void app_init(void)
{
key_scan_register_callback(app_key_callback);
}
/* 周期调用示例:假设系统tick为5ms */
void app_loop(void)
{
uint8_t i;
for (i = 0; i < KEY_MAX_NUM; i++) {
key_scan_tick(i, 5);
}
}
验证方法
本模块的验证分为单元测试与集成测试两个层面。单元测试通过直接调用driver_gpio_set_level模拟按键电平变化,观察回调输出是否符合预期。集成测试则需将模块接入实际硬件,通过示波器或逻辑分析仪对比按键输入与事件输出时序。
/* test_key_scan.c - 单元测试示例 */
#include "key_scan.h"
#include "driver_gpio.h"
#include <assert.h>
#include <stdio.h>
static int press_count = 0;
static int release_count = 0;
static int repeat_count = 0;
static void test_callback(uint8_t key_id, key_event_t event)
{
switch (event) {
case KEY_EVENT_PRESS: press_count++; break;
case KEY_EVENT_RELEASE: release_count++; break;
case KEY_EVENT_REPEAT: repeat_count++; break;
}
}
void test_key_press_release(void)
{
uint8_t i;
driver_gpio_init();
key_scan_register_callback(test_callback);
/* 模拟按键0按下 */
driver_gpio_set_level(0, 0);
for (i = 0; i < 5; i++) { /* 5ms * 5 = 25ms > 20ms去抖 */
key_scan_tick(0, 5);
}
assert(press_count == 1);
/* 模拟按键0释放 */
driver_gpio_set_level(0, 1);
for (i = 0; i < 5; i++) {
key_scan_tick(0, 5);
}
assert(release_count == 1);
printf("test_key_press_release PASSED\n");
}
int main(void)
{
test_key_press_release();
return 0;
}
判定标准:单元测试中,按下后经过25ms(5次调用×5ms)应触发1次PRESS事件;释放后经过25ms应触发1次RELEASE事件。若在去抖时间内电平发生抖动(如按下后5ms内释放),则不应触发任何事件。连击测试中,持续按住按键超过500ms后,每500ms应触发一次REPEAT事件。以上断言全部通过,即可判定模块功能正确。