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;
}

状态转移与验证

IDLE PRESS_CHK PRESSED RELEASE_CHK 电平=0 计数≥阈值 电平=1(抖动) 电平=1 计数≥阈值 电平=0(抖动) → 输出PRESSED事件 → 输出RELEASED事件

编译与运行

# 编译(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)验证抖动被过滤——此时不应产生任何按键事件。

判定标准:一次物理按压仅产生一次按下事件和一次释放事件;抖动期间不产生事件;状态机在任意时刻均处于合法状态。