2. 按键事件检测与响应
任务说明
本案例实现一个基于事件驱动的按键检测与响应机制。系统需要完成:轮询读取按键引脚状态,检测按下与释放边沿,消抖后生成按键事件,并将事件分发至对应回调函数执行响应动作。核心目标是建立“事件产生—事件分发—事件处理”的解耦链路,而非单纯读取GPIO。
事件驱动架构设计
整个系统分为三层:驱动层(读取引脚电平)、事件层(消抖与边沿检测,产生事件)、应用层(注册回调,响应事件)。事件层通过函数指针维护事件回调表,按键事件产生后按事件类型查表调用对应处理函数。该设计使按键检测逻辑与业务响应完全解耦,新增按键响应时无需修改检测代码。
代码实现
// key_event.h
#ifndef KEY_EVENT_H
#define KEY_EVENT_H
#include <stdint.h>
typedef enum {
KEY_EVENT_NONE = 0,
KEY_EVENT_PRESSED,
KEY_EVENT_RELEASED,
KEY_EVENT_LONG_PRESS
} key_event_type_t;
typedef struct {
key_event_type_t type;
uint32_t timestamp; // 事件发生时刻(ms)
} key_event_t;
typedef void (*key_callback_t)(key_event_t *ev);
void key_driver_init(void);
void key_scan_loop(void); // 周期性调用(建议1ms周期)
void key_register_callback(key_event_type_t type, key_callback_t cb);
#endif
// key_event.c
#include "key_event.h"
#include <stdbool.h>
// ---- 硬件抽象(示例:按键接PA0,低电平有效)----
#define KEY_GPIO_PORT 0x40010800UL // 示例地址
#define KEY_GPIO_PIN 0
static uint8_t key_read_pin(void) {
// 实际项目中替换为寄存器读取或HAL函数
// 此处模拟:返回1表示按下(低电平有效取反后)
static uint8_t sim = 0;
sim = !sim; // 模拟电平变化,实际应读取硬件
return sim;
}
// ---- 消抖与事件检测 ----
#define DEBOUNCE_MS 20
#define LONG_PRESS_MS 1000
static uint8_t raw_level = 0;
static uint8_t stable_level = 0;
static uint8_t debounce_cnt = 0;
static uint8_t prev_stable = 0;
static uint32_t press_start_ms = 0;
static uint32_t tick_ms = 0;
static key_callback_t cb_table[3] = {0}; // 索引对应事件类型-1
void key_driver_init(void) {
raw_level = key_read_pin();
stable_level = raw_level;
prev_stable = stable_level;
debounce_cnt = 0;
}
void key_register_callback(key_event_type_t type, key_callback_t cb) {
if (type >= KEY_EVENT_PRESSED && type <= KEY_EVENT_LONG_PRESS) {
cb_table[type - 1] = cb;
}
}
static void dispatch_event(key_event_type_t type) {
key_event_t ev;
ev.type = type;
ev.timestamp = tick_ms;
if (cb_table[type - 1]) {
cb_table[type - 1](&ev);
}
}
void key_scan_loop(void) {
tick_ms++;
raw_level = key_read_pin();
// 消抖:连续DEBOUNCE_MS次采样一致才更新稳定电平
if (raw_level == stable_level) {
debounce_cnt = 0;
} else {
debounce_cnt++;
if (debounce_cnt >= DEBOUNCE_MS) {
stable_level = raw_level;
debounce_cnt = 0;
}
}
// 边沿检测
if (stable_level != prev_stable) {
if (stable_level == 1) { // 按下(低有效取反后为1)
press_start_ms = tick_ms;
dispatch_event(KEY_EVENT_PRESSED);
} else { // 释放
uint32_t press_dur = tick_ms - press_start_ms;
if (press_dur >= LONG_PRESS_MS) {
dispatch_event(KEY_EVENT_LONG_PRESS);
}
dispatch_event(KEY_EVENT_RELEASED);
}
prev_stable = stable_level;
}
}
// main.c 示例
#include "key_event.h"
#include <stdio.h>
void on_pressed(key_event_t *ev) {
printf("[%u] 按键按下\n", ev->timestamp);
}
void on_released(key_event_t *ev) {
printf("[%u] 按键释放\n", ev->timestamp);
}
void on_long_press(key_event_t *ev) {
printf("[%u] 长按触发\n", ev->timestamp);
}
int main(void) {
key_driver_init();
key_register_callback(KEY_EVENT_PRESSED, on_pressed);
key_register_callback(KEY_EVENT_RELEASED, on_released);
key_register_callback(KEY_EVENT_LONG_PRESS, on_long_press);
// 模拟主循环,周期1ms
while (1) {
key_scan_loop();
// 实际项目中此处延时1ms
}
return 0;
}
编译与运行
gcc -o key_demo key_event.c main.c
./key_demo
验证方法
在真实硬件上,将按键一端接GND、另一端接MCU引脚(内部上拉),按下时引脚读到低电平。代码中key_read_pin()返回1表示按下(已做电平取反)。运行后观察串口输出:按下按键约20ms后输出“按键按下”,释放后输出“按键释放”;若按住超过1秒再释放,则先输出“长按触发”再输出“按键释放”。
判定标准:快速按动按键(间隔小于20ms)不应产生误触发;长按1秒后释放必须出现长按事件;所有事件时间戳单调递增。若在无硬件环境下验证逻辑,可在key_read_pin()中按时间序列返回预设电平值,观察事件输出是否符合预期序列。