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_scan() 获取按键事件 按键扫描层(key_scanner.c) 消抖处理:连续3次采样一致才确认状态变化 状态机:释放→按下→释放,长按检测 不直接操作硬件寄存器,只调用函数指针 底层驱动层(模拟GPIO) 实现 key_read_fn 函数指针:读取引脚电平 状态流 释放 → 按下 (消抖确认) 按下 → 释放 长按:持续按住超阈值

编译与运行

# 将 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后触发

判定标准:

  1. 程序能编译通过并运行,输出与上述顺序一致(时间戳允许因扫描周期略有偏移);
  2. KEY0在200ms短按时只产生PRESSED和RELEASED事件,不产生LONG_PRESS;
  3. KEY1在持续1200ms时产生LONG_PRESS事件,且只产生一次;
  4. 将sim_read_pin替换为真实硬件读取函数(如寄存器操作),上层代码和扫描层无需修改即可工作,证明分层有效。

注意:上述输出为基于代码逻辑的预期结果,实际运行请以你的终端显示为准。