STM32有限状态机(FSM)详解,综合应用总结(二)


前言

在嵌入式开发中,我们经常需要让同一个按键在不同操作下触发不同功能:短按执行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 状态机工作流程

  1. IDLE :空闲等待,按键按下后进入 DEBOUNCE 并记录按下时间。
  2. DEBOUNCE :确认是否为真实按下,是则进入 PRESSED,否则退回 IDLE
  3. PRESSED :持续计时,一旦释放:
    • 若持续时间 < 1秒 → 短按,进入 RELEASED 并开启双击窗口。
    • 若持续时间 ≥ 1秒 → 长按,立即输出 EVENT_LONG_PRESS 并返回 IDLE
  4. RELEASED :在 500ms 窗口内等待:
    • 若再次按下 → 进入双击识别流程。
    • 若超时 → 输出 EVENT_SHORT_PRESS 并返回 IDLE

5.3 验证方法

  • 用示波器或逻辑分析仪观察 PA0 波形,配合 LED 指示;
  • 短按(快速按下释放):LED1 翻转;
  • 长按(按住超过 1 秒后释放):LED2 翻转;
  • 双击(在 0.5 秒内完成两次短按):LED3 翻转。

所有现象均与代码预期一致,测试通过。


六、状态机的优势与扩展

6.1 核心优势

特点 说明
逻辑清晰 一张状态图就能表达所有时序,代码结构一目了然
易于扩展 增加"三击"等新事件只需添加状态和迁移条件
健壮性强 每个状态只处理特定输入,杜绝了遗漏边界情况的可能
便于调试 通过打印当前状态即可快速定位问题

6.2 常见扩展场景

状态机不仅适用于按键,更广泛应用于:

  • 通信协议解析(帧头、地址、数据、校验等步骤);
  • 设备工作流程(自动售货机:等待投币→选择商品→出货→找零);
  • 电源管理(正常运行→低功耗→唤醒过渡);
  • 多任务状态机引擎(如 QP 框架)。

七、总结

本文从有限状态机的基本原理出发,以按键短按、长按、双击识别为实际案例,给出了完整、可直接使用的 STM32 标准库代码。通过状态机,原本复杂的时序判断被清晰地分解为状态和转换,代码的可读性、可靠性和可维护性都得到了极大提升。

掌握了状态机思想,你就拥有了一把打开复杂交互逻辑大门的钥匙。若有任何疑问,或想进一步探讨层次状态机、状态模式等话题,欢迎在评论区留言交流!

相关推荐
嵌入式-老费5 小时前
esp开发与应用(继电器的使用)
单片机·嵌入式硬件
CPETW5 小时前
RS-232 Sniffer 嗅探器 ---- UNI-T电子负载通讯协议抓取-A
网络·科技·stm32·单片机·嵌入式硬件·电子
wotaifuzao5 小时前
指针和中断不是魔法:用第一性原理看穿嵌入式底层(万字解析)
stm32·嵌入式开发·内存模型·c语言指针·arm架构·中断机制·rtos内核
xiangw@GZ5 小时前
倒 F 天线 (IFA/MIFA) 原理深度解析
单片机·嵌入式硬件
m0_377108145 小时前
stm32时钟
stm32·单片机·嵌入式硬件
smalming5 小时前
【产品开发】空气波按摩器的一些控制逻辑
嵌入式硬件·嵌入式软件
嗯? 嗯。6 小时前
S32K外设Usart
单片机·嵌入式硬件
星夜夏空996 小时前
STM32单片机学习(24) —— 硬件I2C和软件I2C
stm32·单片机·学习
资深流水灯工程师6 小时前
嵌入式系统中的环形缓冲区:原理、应用与 STM32 实现
网络·stm32·嵌入式硬件