2. 状态机架构之按键消抖
任务定义
机械按键在按下与释放瞬间会产生电平抖动,持续时间通常为5~20ms。若直接读取电平判断按键状态,一次物理按压可能被识别为多次触发。本案例使用有限状态机(FSM)实现按键消抖,将按键输入抽象为空闲、按下确认、释放确认三个稳定状态,通过时间阈值过滤抖动,仅在状态稳定跳转时输出有效按键事件。
状态机设计
定义四个状态:IDLE(空闲)、PRESS_CHK(按下确认中)、PRESSED(按下稳定)、RELEASE_CHK(释放确认中)。状态转移条件基于按键电平采样值与连续稳定计数:
- IDLE:检测到低电平(按下),进入
PRESS_CHK并清零计数;否则保持。 - PRESS_CHK:若持续采样到低电平,计数累加;计数达到消抖阈值(如10次,每次间隔1ms),确认按下,进入
PRESSED并输出按下事件;若中途出现高电平,则返回IDLE。 - PRESSED:检测到高电平(释放),进入
RELEASE_CHK并清零计数;否则保持。 - RELEASE_CHK:若持续采样到高电平,计数累加;达到阈值后确认释放,进入
IDLE并输出释放事件;若中途出现低电平,返回PRESSED。
代码实现
// key_debounce_fsm.h
#ifndef KEY_DEBOUNCE_FSM_H
#define KEY_DEBOUNCE_FSM_H
#include <stdint.h>
typedef enum {
KEY_IDLE = 0,
KEY_PRESS_CHK,
KEY_PRESSED,
KEY_RELEASE_CHK
} KeyState_t;
typedef enum {
KEY_EVENT_NONE = 0,
KEY_EVENT_PRESSED,
KEY_EVENT_RELEASED
} KeyEvent_t;
typedef struct {
KeyState_t state;
uint8_t debounce_cnt;
uint8_t debounce_thresh; // 消抖阈值(采样次数)
} KeyFSM_t;
void KeyFSM_Init(KeyFSM_t *fsm, uint8_t thresh);
KeyEvent_t KeyFSM_Scan(KeyFSM_t *fsm, uint8_t raw_level);
#endif
// key_debounce_fsm.c
#include "key_debounce_fsm.h"
void KeyFSM_Init(KeyFSM_t *fsm, uint8_t thresh) {
fsm->state = KEY_IDLE;
fsm->debounce_cnt = 0;
fsm->debounce_thresh = thresh;
}
KeyEvent_t KeyFSM_Scan(KeyFSM_t *fsm, uint8_t raw_level) {
KeyEvent_t evt = KEY_EVENT_NONE;
switch (fsm->state) {
case KEY_IDLE:
if (raw_level == 0) { // 检测到按下
fsm->state = KEY_PRESS_CHK;
fsm->debounce_cnt = 0;
}
break;
case KEY_PRESS_CHK:
if (raw_level == 0) {
if (++fsm->debounce_cnt >= fsm->debounce_thresh) {
fsm->state = KEY_PRESSED;
fsm->debounce_cnt = 0;
evt = KEY_EVENT_PRESSED; // 确认按下
}
} else {
fsm->state = KEY_IDLE; // 抖动,回到空闲
fsm->debounce_cnt = 0;
}
break;
case KEY_PRESSED:
if (raw_level == 1) { // 检测到释放
fsm->state = KEY_RELEASE_CHK;
fsm->debounce_cnt = 0;
}
break;
case KEY_RELEASE_CHK:
if (raw_level == 1) {
if (++fsm->debounce_cnt >= fsm->debounce_thresh) {
fsm->state = KEY_IDLE;
fsm->debounce_cnt = 0;
evt = KEY_EVENT_RELEASED; // 确认释放
}
} else {
fsm->state = KEY_PRESSED; // 抖动,回到按下稳定
fsm->debounce_cnt = 0;
}
break;
default:
fsm->state = KEY_IDLE;
break;
}
return evt;
}
// main.c 示例调用(假设1ms定时中断调用KeyFSM_Scan)
#include <stdio.h>
#include "key_debounce_fsm.h"
int main(void) {
KeyFSM_t key;
KeyFSM_Init(&key, 10); // 10ms消抖(每次采样间隔1ms)
// 模拟输入序列:按下抖动 + 稳定按下 + 释放抖动 + 稳定释放
// 0=按下电平,1=释放电平
uint8_t raw_seq[] = {0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};
uint8_t seq_len = sizeof(raw_seq) / sizeof(raw_seq[0]);
for (uint8_t i = 0; i < seq_len; i++) {
KeyEvent_t evt = KeyFSM_Scan(&key, raw_seq[i]);
if (evt == KEY_EVENT_PRESSED)
printf("t=%dms: KEY_PRESSED event\n", i);
else if (evt == KEY_EVENT_RELEASED)
printf("t=%dms: KEY_RELEASED event\n", i);
}
return 0;
}
状态转移与验证
编译与运行
# 编译(Linux/macOS或Windows + GCC)
gcc -Wall -o key_demo main.c key_debounce_fsm.c
# 运行
./key_demo
预期输出与判定标准
根据上述模拟输入序列(前3个采样为按下抖动,第4~20个采样为稳定按下;第21~23个采样为释放抖动,第24~40个采样为稳定释放),代码逻辑可唯一推出以下输出:
t=19ms: KEY_PRESSED event
t=39ms: KEY_RELEASED event
验证方法:编译运行后观察终端输出。若在约第19ms处输出一次KEY_PRESSED、第39ms处输出一次KEY_RELEASED,且中间无重复触发,则说明状态机消抖逻辑正确。可调整raw_seq中的抖动长度(如将前3个采样改为0,1,0)验证抖动被过滤——此时不应产生任何按键事件。
判定标准:一次物理按压仅产生一次按下事件和一次释放事件;抖动期间不产生事件;状态机在任意时刻均处于合法状态。