使用状态机实现按键消抖
基于STM32F407ZGT6开发板
前言
-
什么是状态机?
有限状态机(Finite-state machine,FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型,其本质是一种算法思想。
-
为什么要使用状态机?
不使用状态机,如果遇到逻辑比较复杂的需求,就极其容易出现大堆的if-else和switch-case。现在大部分的应用场景都要求敏捷开发,这必然伴随着不断的软件版本迭代。
随着需求的变更和功能的扩充,代码往往会变得"一团乱麻"。每次修改bug都需要重新梳理一遍应用逻辑,浪费了大量的时间。
而状态机能将嵌入式开发中应用层的逻辑拆解为独立的"状态"和"事件"。每个任务状态都只关心自己该做什么,以及收到事件后去哪里?
-
什么时候该用状态机?
要是逻辑足够清晰,要求足够简单,比如简单的开关逻辑没有必要使用状态机。不是所有代码都要写成状态机,有如下情况使用状态机会比较合适:
- 需求是分步骤进行的。先...再...然后...最后等等。
- 需要等一段时间判断这件到底做了没有,比如下面举的软件消抖的例子。
- 一词多义,即一个输入按钮在不同的模式下有不同动作要求。
-
状态、事件、条件、动作分别是什么?
-
状态(state):一个状态机至少要包含两个状态。例如面前有一扇门,他有 open 和 closed 两个状态。
-
事件(event):事件就是执行某个操作的触发条件或者口令。对于电动门,"按下按钮"就是一个事件。
-
动作(action):事件发生以后要执行动作。例如事件是"按按钮",动作是"开门"。
-
变换(Transition):也就是从一个状态变化为另一个状态。例如"开门过程"就是一个变换。
这属于状态机的基本概念,写状态机前需要有所了解,这里能理解就好。
-
-
写状态机前要先思考什么?
状态机解决的不是"语法问题",而是"流程问题"。所以写状态机前最好按下面的五步流程走一遍。
- 先写目标现象,把功能用自然语言描述清楚。
- 列出具体出现的状态
- 列出具体的事件
- 写状态跳转表
- 最后开始写代码
使用状态机实现软件消抖示例
下面以按键消抖为例,分析一下状态机该怎么写。之所以使用按键消抖这个例子是因为一方面这个例子比较简单,方便理解;另一个方面是大家从事的方向不同,而按键是比较通用的元器件。
当然,为了照顾嵌入式刚刚入门的朋友,再简单的说一下什么是软件消抖。在按键按下去的那一瞬间,金属触点会像弹簧一样"弹跳"好几次,软件消抖就是用代码忽略掉这些"弹跳",只认那一次真正的按压。
1.先写目标
按键按下后,如果稳定超过10ms,产生PRESS事件。
按键松开后,如果稳定超过10ms,产生RELEASE事件。
这一步很重要,如果不写清楚,代码一定会乱。
2.列出状态
本按键功能只检测最基本的实现松开和按下状态(长按、短按和双击功能示例中不进行实现),那么我们的代码中会出现如下四个状态,分别是:
- 稳定松开状态 IDLE
- 抖动按下状态 DEBOUNCE_DOWN
- 稳定按下状态 PRESSED
- 抖动松开状态 DEBOUNCE_UP
3.列出事件
按键消抖中,外部事件相对简答,只有如下三种:
- 读到按下
- 读到松开
- 时间超过10ms
4.写状态跳转表
这是状态机最关键的一步,表格如下:
| 当前状态 | 条件 | 动作 | 下一状态 |
|---|---|---|---|
| IDLE | 读到按下 | 记录当前时间 | DEBOUNCE_DOWN |
| DEBOUNCE_DOWN | 读到松开 | 抖动失败 | IDLE |
| DEBOUNCE_DOWN | 按下时间超过10ms | 产生PRESS事件 | PRESSED |
| PRESSED | 读到松开 | 记录当前时间 | DEBOUNCE_UP |
| DEBOUNCE_UP | 读到按下 | 抖动失败 | PRESSED |
| DEBOUNCE_UP | 松开时间超过10ms | 产生RELEASE事件 | IDLE |
当状态跳转表出来时,代码其实已经差不多了。
5.写代码
switch (state) {
case IDLE:
...
break;
case DEBOUNCE_DOWN:
...
break;
case PRESSED:
...
break;
case DEBOUNCE_UP:
...
break;
}
具体代码如下:
app_key_fsm.h
c
#define KEY0_GPIO_PORT GPIOE
#define KEY0_GPIO_PIN GPIO_PIN_4
#define KEY0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能 */
typedef enum {
APP_KEY_EVENT_NONE = 0,
APP_KEY_EVENT_PRESS, //按下事件
APP_KEY_EVENT_RELEASE, //松开事件
} App_KeyEvent;
typedef struct {
GPIO_TypeDef *port;
uint16_t pin;
GPIO_PinState active_level;
uint8_t state;
uint32_t state_tick;
App_KeyEvent pending_event;
} App_Key;
void App_Key_Init(App_Key *key,
GPIO_TypeDef *port,
uint16_t pin,
GPIO_PinState active_level);
void App_Key_Tick(App_Key *key);
App_KeyEvent App_Key_GetEvent(App_Key *key);
bool App_Key_IsPressed(const App_Key *key);
void key_init(void);
#endif
app_key_fsm.c
c
#include "app_key_fsm.h"
#ifndef APP_KEY_DEBOUNCE_MS
#define APP_KEY_DEBOUNCE_MS 10u
#endif
typedef enum {
KEY_STATE_IDLE = 0, //稳定松开状态
KEY_STATE_DEBOUNCE_DOWN, //抖动按下状态
KEY_STATE_PRESSED, //稳定按下状态
KEY_STATE_DEBOUNCE_UP //抖动松开状态
} KeyState;
void key_init(void)
{
GPIO_InitTypeDef gpio_init_struct; /* GPIO配置参数存储变量 */
KEY0_GPIO_CLK_ENABLE(); /* KEY0时钟使能 */
gpio_init_struct.Pin = KEY0_GPIO_PIN; /* KEY0引脚 */
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(KEY0_GPIO_PORT, &gpio_init_struct); /* KEY0引脚模式设置,上拉输入 */
}
//读按键口的电平值
static bool key_read_raw(const App_Key *key)
{
return HAL_GPIO_ReadPin(key->port, key->pin) == key->active_level;
}
//切换状态
static void key_set_event(App_Key *key, App_KeyEvent event)
{
if (key->pending_event == APP_KEY_EVENT_NONE)
{
key->pending_event = event;
}
}
void App_Key_Init(App_Key *key,
GPIO_TypeDef *port,
uint16_t pin,
GPIO_PinState active_level)
{
key->port = port;
key->pin = pin;
key->active_level = active_level;
key->state = key_read_raw(key) ? KEY_STATE_PRESSED : KEY_STATE_IDLE;
key->state_tick = HAL_GetTick();
key->pending_event = APP_KEY_EVENT_NONE;
}
void App_Key_Tick(App_Key *key)
{
//见状态跳转表
bool down = key_read_raw(key);
uint32_t now = HAL_GetTick();
switch ((KeyState)key->state)
{
case KEY_STATE_IDLE:
if (down)
{
key->state = KEY_STATE_DEBOUNCE_DOWN;
key->state_tick = now;
}
break;
case KEY_STATE_DEBOUNCE_DOWN:
if (!down)
{
key->state = KEY_STATE_IDLE;
}
else if ((uint32_t)(now - key->state_tick) >= APP_KEY_DEBOUNCE_MS)
{
key->state = KEY_STATE_PRESSED;
key_set_event(key, APP_KEY_EVENT_PRESS);
}
break;
case KEY_STATE_PRESSED:
if (!down)
{
key->state = KEY_STATE_DEBOUNCE_UP;
key->state_tick = now;
}
break;
case KEY_STATE_DEBOUNCE_UP:
if (down)
{
key->state = KEY_STATE_PRESSED;
}
else if ((uint32_t)(now - key->state_tick) >= APP_KEY_DEBOUNCE_MS)
{
key->state = KEY_STATE_IDLE;
key_set_event(key, APP_KEY_EVENT_RELEASE);
}
break;
default:
key->state = KEY_STATE_IDLE;
key->pending_event = APP_KEY_EVENT_NONE;
break;
}
}
//取事件
App_KeyEvent App_Key_GetEvent(App_Key *key)
{
App_KeyEvent event = key->pending_event;
//事件取走一次,就清空。
key->pending_event = APP_KEY_EVENT_NONE;
return event;
}
//判断状态是否为稳定按下状态
bool App_Key_IsPressed(const App_Key *key)
{
return key->state == KEY_STATE_PRESSED ||
key->state == KEY_STATE_DEBOUNCE_UP;
}
main.c
c
#include "app_key_fsm.h"
int main(void)
{
static App_Key s_key0;
static uint32_t s_key_tick = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* LED初始化 */
key_init(); /* 按键初始化 */
/* 按键状态机初始化,按键按下时读出的是低电平 */
App_Key_Init(&s_key0, KEY0_GPIO_PORT, KEY0_GPIO_PIN, GPIO_PIN_RESET);
while(1)
{
if (HAL_GetTick() - s_key_tick >= 10)
{
s_key_tick = HAL_GetTick();
App_Key_Tick(&s_key0); //10毫秒一次周期调用
App_KeyEvent event = App_Key_GetEvent(&s_key0);//取事件,取走就清空。
if (event == APP_KEY_EVENT_PRESS)
{
LED0_TOGGLE(); //LED0电平反转
}
}
}
}
测试结果

在keil调试界面可以看到状态机的状态发生了改变。单片机上也实现了按下按键0,LED0反转的功能。