4. 中间件层按键扫描模块

任务定义

在MCU嵌入式软件分层架构中,中间件层位于驱动层与应用层之间,为应用提供与硬件无关的通用服务。本案例实现一个独立于具体MCU型号的按键扫描中间件模块,核心目标为:周期性调用扫描函数,完成按键去抖、边沿检测(按下/释放事件)与连击计数,并通过回调函数将事件上报给应用层。模块不直接操作寄存器,所有硬件访问通过驱动层接口函数指针注入,从而保证可移植性与可测试性。

模块接口设计

按键扫描模块对外暴露三个核心接口:初始化、周期扫描、事件回调注册。硬件差异(GPIO读取方式)通过函数指针抽象,由驱动层在初始化时注入。模块内部维护按键状态机,每个按键独立处理去抖与边沿检测。

按键扫描中间件模块结构 应用层 事件回调处理 中间件层 按键扫描模块 驱动层 GPIO读取函数 单按键状态机 IDLE ──按下──▶ DEBOUNCE_PRESS ──稳定──▶ PRESSED ──释放──▶ DEBOUNCE_RELEASE ──稳定──▶ IDLE │ │ │ └─抖动─▶ 回到IDLE └─长按─▶ 连击计数+1 └─抖动─▶ 回到PRESSED 去抖时间: 20ms | 连击间隔: 500ms | 事件类型: PRESS / RELEASE / REPEAT

代码实现

以下为按键扫描模块的完整实现,包含头文件与源文件。模块支持最多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事件。以上断言全部通过,即可判定模块功能正确。