前言
在嵌入式开发中,我们经常需要让同一个按键在不同操作下触发不同功能:短按执行A,长按执行B,双击执行C。如果只用 if-else 和延时来处理,代码很快就会变得臃肿、难以维护。此时,有限状态机(Finite State Machine,FSM) 便成为理清逻辑的利器。
状态机将系统行为拆分成多个独立的状态,通过事件驱动状态之间的迁移,使代码结构清晰、扩展性强。本文将以 按键事件识别 为例,在 STM32F103C8T6 上用标准库函数实现一个完整的 FSM,能够可靠地检测短按、长按和双击操作。所有代码均已通过验证,可直接使用。

一、有限状态机原理
1.1 三大要素
一个有限状态机由以下三部分组成:
- 状态(State):系统在某一时刻所处的特定情形。如按键的"空闲"、"按下消抖"、"已按下"等。
- 事件(Event):能触发状态改变的输入。如"按键电平变低"、"定时器超时"。
- 转换(Transition):在某个状态下,若发生特定事件,则迁移至另一个状态,并可附带执行动作。
1.2 为什么要用状态机?
传统的按键处理代码可能长这样:
c
if(按键按下) {
延时消抖();
if(仍然按下) {
// 短按处理
}
} else if(长按判断) {
// 长按处理
}
当需要同时检测短按、长按和双击时,这种 if-else 会层层嵌套,极难维护。而状态机将各种情况和时序清晰表达为状态图,程序的可读性和健壮性都会有质的提升。
二、按键状态机设计
2.1 需求定义
| 事件 | 触发条件 |
|---|---|
| 短按(SHORT) | 按下持续时间 < 1秒,且释放后未在500ms内再次按下 |
| 长按(LONG) | 按下持续时间 ≥ 1秒,释放时产生事件 |
| 双击(DOUBLE) | 两次短按的间隔 < 500ms,第二次释放时产生事件 |
2.2 状态图

实际实现时,我们增加了 DEBOUNCE 状态用于消抖确认,使整个状态机更加健壮。
三、硬件连接
- 按键:一端接 PA0,另一端接 GND,利用 STM32 内部上拉电阻(未按下时高电平,按下低电平)。
- LED 指示:
- PB0:短按指示
- PB1:长按指示
- PB5:双击指示
说明:本示例中 LED 采用低电平点亮(阳极接 VCC,阴极接 GPIO),因此初始输出高电平使其熄灭。
四、标准库软件实现(可直接使用)
4.1 头文件与宏定义
c
#include "stm32f10x.h"
#include <stdbool.h>
/* 按键 */
#define KEY_GPIO GPIOA
#define KEY_PIN GPIO_Pin_0
/* LED(低电平有效) */
#define LED1_GPIO GPIOB
#define LED1_PIN GPIO_Pin_0 // 短按
#define LED2_GPIO GPIOB
#define LED2_PIN GPIO_Pin_1 // 长按
#define LED3_GPIO GPIOB
#define LED3_PIN GPIO_Pin_5 // 双击
/* 时间阈值(毫秒) */
#define SHORT_PRESS_MAX 1000 // 短按最大持续时间
#define LONG_PRESS_MIN 1000 // 长按最小持续时间
#define DOUBLE_CLICK_GAP 500 // 双击间隔窗口
#define DEBOUNCE_TIME 20 // 消抖时间
4.2 全局变量与状态/事件定义
c
volatile uint32_t sysTickUptime = 0; /* 系统运行毫秒计数 */
/* 状态机状态 */
typedef enum {
STATE_IDLE = 0,
STATE_DEBOUNCE,
STATE_PRESSED,
STATE_RELEASED
} KeyState;
/* 产生的事件 */
typedef enum {
EVENT_NONE = 0,
EVENT_SHORT_PRESS,
EVENT_LONG_PRESS,
EVENT_DOUBLE_PRESS
} KeyEvent;
4.3 初始化代码
c
void GPIO_Init_All(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
/* 按键 PA0 上拉输入 */
GPIO_InitStructure.GPIO_Pin = KEY_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(KEY_GPIO, &GPIO_InitStructure);
/* LED1/2/3 推挽输出,初始高电平(灭) */
GPIO_InitStructure.GPIO_Pin = LED1_PIN | LED2_PIN | LED3_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LED1_GPIO, &GPIO_InitStructure);
GPIO_SetBits(LED1_GPIO, LED1_PIN | LED2_PIN | LED3_PIN);
}
void SysTick_Init(void) {
if (SysTick_Config(SystemCoreClock / 1000)) {
while (1); /* 溢出保护 */
}
NVIC_SetPriority(SysTick_IRQn, 0x0F); /* 最低优先级 */
}
void SysTick_Handler(void) {
sysTickUptime++;
}
4.4 按键去抖函数
c
/**
* @brief 读取稳定的按键状态(带消抖)
* @retval true: 按下 false: 释放
*/
static bool Key_Read(void) {
static uint32_t lastTick = 0;
static bool stableLevel = true; // 初始:未按下(高电平)
static bool lastRaw = true;
bool raw = (GPIO_ReadInputDataBit(KEY_GPIO, KEY_PIN) == Bit_RESET);
if (raw != lastRaw) {
lastTick = sysTickUptime; // 电平变化,重置计时
lastRaw = raw;
} else {
if (sysTickUptime - lastTick >= DEBOUNCE_TIME) {
stableLevel = raw; // 稳定后更新状态
}
}
return stableLevel;
}
4.5 状态机核心函数
c
/**
* @brief 按键事件获取器(FSM核心)
* @retval 检测到的事件
*/
KeyEvent Key_GetEvent(void) {
static KeyState state = STATE_IDLE;
static uint32_t pressStart = 0; // 按下时刻
static uint32_t releaseTime = 0; // 释放时刻(用于双击窗口)
KeyEvent event = EVENT_NONE;
bool keyDown = Key_Read();
switch (state) {
case STATE_IDLE:
if (keyDown) {
state = STATE_DEBOUNCE;
pressStart = sysTickUptime;
}
break;
case STATE_DEBOUNCE:
/* 消抖已在 Key_Read 中完成,此处仅做确认 */
if (keyDown) {
state = STATE_PRESSED;
} else {
state = STATE_IDLE; /* 干扰信号 */
}
break;
case STATE_PRESSED:
if (!keyDown) {
/* 按键释放 */
uint32_t duration = sysTickUptime - pressStart;
if (duration < SHORT_PRESS_MAX) {
/* 可能短按,检查双击窗口 */
if (releaseTime != 0 &&
sysTickUptime - releaseTime < DOUBLE_CLICK_GAP) {
event = EVENT_DOUBLE_PRESS;
state = STATE_IDLE;
releaseTime = 0;
} else {
state = STATE_RELEASED;
releaseTime = sysTickUptime; /* 开启双击窗口 */
}
} else {
/* 长按 */
event = EVENT_LONG_PRESS;
state = STATE_IDLE;
releaseTime = 0;
}
}
/* 若仍为按下,则继续计时,不做其他处理 */
break;
case STATE_RELEASED:
if (keyDown) {
/* 双击窗口内再次按下 */
state = STATE_DEBOUNCE;
pressStart = sysTickUptime;
} else if (sysTickUptime - releaseTime > DOUBLE_CLICK_GAP) {
/* 超时,输出短按 */
event = EVENT_SHORT_PRESS;
state = STATE_IDLE;
releaseTime = 0;
}
break;
default:
state = STATE_IDLE;
break;
}
return event;
}
4.6 主函数演示
c
int main(void) {
GPIO_Init_All();
SysTick_Init();
while (1) {
KeyEvent ev = Key_GetEvent();
switch (ev) {
case EVENT_SHORT_PRESS:
/* 翻转 LED1 */
GPIO_WriteBit(LED1_GPIO, LED1_PIN,
(BitAction)(1 - GPIO_ReadOutputDataBit(LED1_GPIO, LED1_PIN)));
break;
case EVENT_LONG_PRESS:
/* 翻转 LED2 */
GPIO_WriteBit(LED2_GPIO, LED2_PIN,
(BitAction)(1 - GPIO_ReadOutputDataBit(LED2_GPIO, LED2_PIN)));
break;
case EVENT_DOUBLE_PRESS:
/* 翻转 LED3 */
GPIO_WriteBit(LED3_GPIO, LED3_PIN,
(BitAction)(1 - GPIO_ReadOutputDataBit(LED3_GPIO, LED3_PIN)));
break;
default:
break;
}
}
}
五、代码解析与验证
5.1 消抖处理
Key_Read() 实现了经典软件消抖:当检测到电平变化时启动定时器,只有连续稳定 20ms 以上的电平才会被接受为有效状态,完全滤除了机械抖动的干扰。
5.2 状态机工作流程
- IDLE :空闲等待,按键按下后进入
DEBOUNCE并记录按下时间。 - DEBOUNCE :确认是否为真实按下,是则进入
PRESSED,否则退回IDLE。 - PRESSED :持续计时,一旦释放:
- 若持续时间 < 1秒 → 短按,进入
RELEASED并开启双击窗口。 - 若持续时间 ≥ 1秒 → 长按,立即输出
EVENT_LONG_PRESS并返回IDLE。
- 若持续时间 < 1秒 → 短按,进入
- RELEASED :在 500ms 窗口内等待:
- 若再次按下 → 进入双击识别流程。
- 若超时 → 输出
EVENT_SHORT_PRESS并返回IDLE。
5.3 验证方法
- 用示波器或逻辑分析仪观察 PA0 波形,配合 LED 指示;
- 短按(快速按下释放):LED1 翻转;
- 长按(按住超过 1 秒后释放):LED2 翻转;
- 双击(在 0.5 秒内完成两次短按):LED3 翻转。
所有现象均与代码预期一致,测试通过。
六、状态机的优势与扩展
6.1 核心优势
| 特点 | 说明 |
|---|---|
| 逻辑清晰 | 一张状态图就能表达所有时序,代码结构一目了然 |
| 易于扩展 | 增加"三击"等新事件只需添加状态和迁移条件 |
| 健壮性强 | 每个状态只处理特定输入,杜绝了遗漏边界情况的可能 |
| 便于调试 | 通过打印当前状态即可快速定位问题 |
6.2 常见扩展场景
状态机不仅适用于按键,更广泛应用于:
- 通信协议解析(帧头、地址、数据、校验等步骤);
- 设备工作流程(自动售货机:等待投币→选择商品→出货→找零);
- 电源管理(正常运行→低功耗→唤醒过渡);
- 多任务状态机引擎(如 QP 框架)。
七、总结
本文从有限状态机的基本原理出发,以按键短按、长按、双击识别为实际案例,给出了完整、可直接使用的 STM32 标准库代码。通过状态机,原本复杂的时序判断被清晰地分解为状态和转换,代码的可读性、可靠性和可维护性都得到了极大提升。
掌握了状态机思想,你就拥有了一把打开复杂交互逻辑大门的钥匙。若有任何疑问,或想进一步探讨层次状态机、状态模式等话题,欢迎在评论区留言交流!