STM32实现按键有限状态机(超详细,易移植)
一、状态机
简而言之,状态机是使不同状态之间的改变以及状态时产生的相应动作的一种机制。
1.1状态机的四要素
-
现态:状态机当前状态。
-
触发条件:改变当前状态的发生条件。
-
动作:状态改变产生相应的动作。
-
次态:状态机激活触发条件后跳转到的下一状态。
注意:状态和动作是不同的,状态是持续的而动作是间断的,改变状态产生动作,动作完成后,状态依旧持续。
1.2为什么要使用状态机
举一个简单的例子,在实现按键扫描常常有三种方式
- 轮询方式:main函数大循环中加入按键扫描函数key_scan(),相信大家最开始接触的也是这个
- 中断方式:在单片机中大多数都支持外部中断,但每一个(或几个)IO口占用一个中断向量,使用非常方便。
- 状态机方式:在我看来这种方式优于前两者任意一个,为什么呢?我们来来看看。
1.资源占用方面:
轮询方式:在主循环中一直占用CPU的执行。
中断方式:仅仅在时间产生后跳转执行后回调,相对于轮询占用的资源少很多,但很多按键时需要多个中断向量。
状态机方式:在使用状态机实现按键扫描时,我们仅仅需要一个定时器即可实现任意个按键的扫描,效率不低于中断方式。
2.执行效率方面:
轮询方式:效率极低,反应且不灵敏,有时候要按很多次才有反应。
中断方式:效率较高,反应灵敏,单独的中断方式不支持长按等状态操作。
状态机方式:效率较高,反应灵敏,支持长按的状态操作。
3.按键抖动方面:
轮询方式:需要延时消抖,消抖的同时无法进行其他操作。
中断方式:需要延时消抖,消抖的同时无法进行其他操作。
状态机方式:间接的产生了消抖,为什么这么说呢?这里我们采用一个定时时间为20ms的定时器,每20ms进行一次状态的处理,在上一次处理和下一次处理的20ms中可以跳出定时器中断进行其他操作,也就是消抖的同时在进行其他的操作,大大的提高了运行的效率。
1.3 怎么实现状态机
我们已经知道了状态机的四要素和为什么要使用状态机,那这一步就很简单了,我们就用按键状态机来进行实现。既然是状态机就应该存在一个状态表,这个状态表应该描述现态与次态的关系,至于产生什么动作在实际的运用中在不同场合往往是不同的。好,说了这么多,直接上代码:
我定义了几个类型:
首先按键分为共阳和共阴两种,不同的接法,按下时对应不同的电平,下面这个结构体主要用于初始化按下时对应的有效电平和在读取时需要知道的硬件引脚端口。
/* 用于状态机初始化按键 */
typedef struct{
uint32_t GPIO_Pull; //按键的上下拉模式
GPIO_TypeDef* GPIOx; //按键对应的端口
uint16_t GPIO_Pin_x; //按键的引脚
uint8_t key_nox;
}Key_Init;
以对我来说有用的按键的五种状态,其实在实际应用中可能还会用到单击,双击等其他一系列的操作,但我暂时没用到就没有写,在后面我会简述一下我的思路
/* 按键状态机的五种状态 */
typedef enum _KEY_STATUS_LIST{
KEY_NULL = 0x00, // 无动作
KEY_SURE = 0x01, // 确认状态
KEY_RAISE = 0x02, // 按键抬起
KEY_PRESS = 0x04, // 按键按下
KEY_LONG = 0x08, // 长按
}KEY_STATUS_LIST;
下面这些类型是一些状态的标志位,在32中有定义这样的类型,下面只是简单的进行重新的声明,如果移植到其他单片机可以自己实现KEY_ENABLE_STATUS枚举类型
/*按键屏蔽标志*/
typedef FunctionalState KEY_ENABLE_STATUS;//在stm32中有定义 ENABLE和DISABLE两种状态
/*按键IO读取标志*/
#define LOW_LEVEL GPIO_PIN_RESET
#define HIGH_LEVER GPIO_PIN_SET
typedef GPIO_PinState IO_STATUS_LIST;
/*获取IO电平的函数*/
static IO_STATUS_LIST KEY_ReadPin(Key_Init Key) //按键读取函数
{return (IO_STATUS_LIST)HAL_GPIO_ReadPin(Key.GPIOx,Key.GPIO_Pin_x);}
这是状态机类的的一个结构体,描述了状态机类的一系列操作,也是最重要的部分
typedef struct _KEY_COMPONENTS // 状态机类
{
KEY_ENABLE_STATUS KEY_SHIELD; //按键屏蔽,DISABLE(0):屏蔽,ENABLE(1):不屏蔽
uint8_t KEY_TIMECOUNT; //按键长按计数
IO_STATUS_LIST KEY_FLAG; //标志按键按下标志
IO_STATUS_LIST KEY_DOWN_LEVEL; //按下时,按键IO实际的电平
KEY_STATUS_LIST KEY_STATUS; //按键状态
KEY_STATUS_LIST KEY_EVENT; //按键事件
IO_STATUS_LIST (*READ_PIN)(Key_Init Key);//读IO电平函数
}KEY_COMPONENTS;
接下来就是按键类了
typedef struct // 按键类
{
Key_Init Key_Board; // 继承初始化父类
KEY_COMPONENTS KeyStatus; // 继承状态机父类
}Key_Config;
注册表用来表示已经存在的按键数量,方便管理
typedef enum // 按键注册表
{
KEY1,
KEY2,
KEY3,
KEY4,
KEY5,
KEY6,// 用户添加的按钮名称
KEY_NUM, // 必须要有的记录按钮数量,必须在最后
}KEY_LIST;
接下来是c中的代码:
Key_Config Key_Buf[KEY_NUM]; // 创建按键数组
#define KEY_LONG_DOWN_DELAY 20 // 设置20个TIM3定时器中断20*50 = 1s算长按
static void Get_Key_Level(void) // 根据实际按下按钮的电平去把它换算成虚拟的结果
{
uint8_t i;
for(i = 0;i < KEY_NUM;i++)
{
if(Key_Buf[i].KeyStatus.KEY_SHIELD == DISABLE) //如果挂起则不进行按键扫描
continue;
if(Key_Buf[i].KeyStatus.READ_PIN(Key_Buf[i].Key_Board) == Key_Buf[i].KeyStatus.KEY_DOWN_LEVEL)
Key_Buf[i].KeyStatus.KEY_FLAG = HIGH_LEVER;
else
Key_Buf[i].KeyStatus.KEY_FLAG = LOW_LEVEL;
}
}
/*创建按键对象*/
static void Creat_Key(Key_Init* Init)
{
uint8_t i;
for(i = 0;i < KEY_NUM;i++)
{
Key_Buf[i].Key_Board = Init[i]; // Key_Buf按钮对象的初始化属性赋值
Key_Buf[i].Key_Board.key_nox = i;
// 初始化按钮对象的状态机属性
Key_Buf[i].KeyStatus.KEY_SHIELD = ENABLE;
Key_Buf[i].KeyStatus.KEY_TIMECOUNT = 0;
Key_Buf[i].KeyStatus.KEY_FLAG = LOW_LEVEL;
if(Key_Buf[i].Key_Board.GPIO_Pull == GPIO_PULLUP) // 根据模式进行赋值
Key_Buf[i].KeyStatus.KEY_DOWN_LEVEL = LOW_LEVEL;
else
Key_Buf[i].KeyStatus.KEY_DOWN_LEVEL = HIGH_LEVER;
Key_Buf[i].KeyStatus.KEY_STATUS = KEY_NULL;
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_NULL;
Key_Buf[i].KeyStatus.READ_PIN = KEY_ReadPin; //赋值按键读取函数
}
}
/*状态机初始化*/
void KEY_Init(void) //IO初始化
{
Key_Init KeyInit[KEY_NUM]=
{
{GPIO_PULLUP, GPIOC, GPIO_PIN_12}, // 初始化 按键0
{GPIO_PULLUP, GPIOC, GPIO_PIN_13}, // 初始化 按键1
{GPIO_PULLUP, GPIOA, GPIO_PIN_0}, // 初始化 按键2
{GPIO_PULLUP, GPIOA, GPIO_PIN_1}, // 初始化 按键3
{GPIO_PULLUP, GPIOA, GPIO_PIN_2}, // 初始化 按键4
{GPIO_PULLUP, GPIOA, GPIO_PIN_3}, // 初始化 按键5
};
Creat_Key(KeyInit); // 调用按键初始化函数
}
/*状态机的状态转换*/
static void ReadKeyStatus(void)
{
uint8_t i;
Get_Key_Level();
for(i = 0;i < KEY_NUM;i++)
{
switch(Key_Buf[i].KeyStatus.KEY_STATUS)
{
//状态0:没有按键按下
case KEY_NULL:
if(Key_Buf[i].KeyStatus.KEY_FLAG == HIGH_LEVER)//有按键按下
{
Key_Buf[i].KeyStatus.KEY_STATUS = KEY_SURE;//转入状态1
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_NULL;//空事件
}
else
{
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_NULL;//空事件
}
break;
//状态1:按键按下确认
case KEY_SURE:
if(Key_Buf[i].KeyStatus.KEY_FLAG == HIGH_LEVER)//确认和上次相同
{
Key_Buf[i].KeyStatus.KEY_STATUS = KEY_PRESS;//转入状态2
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_PRESS;//按下事件
Key_Buf[i].KeyStatus.KEY_TIMECOUNT = 0;//计数器清零
}
else
{
Key_Buf[i].KeyStatus.KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_NULL;//空事件
}
break;
//状态2:按键按下
case KEY_PRESS:
if(Key_Buf[i].KeyStatus.KEY_FLAG != HIGH_LEVER)//按键释放,端口高电平
{
Key_Buf[i].KeyStatus.KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_RAISE;//松开事件
}
else if((Key_Buf[i].KeyStatus.KEY_FLAG == HIGH_LEVER)
&& (++Key_Buf[i].KeyStatus.KEY_TIMECOUNT >= KEY_LONG_DOWN_DELAY))
//超过KEY_LONG_DOWN_DELAY没有释放
{
Key_Buf[i].KeyStatus.KEY_STATUS = KEY_LONG;//转入状态3
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_LONG;//长按事件
Key_Buf[i].KeyStatus.KEY_TIMECOUNT = 0;//计数器清零
}
else
{
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_NULL;//空事件
}
break;
//状态3:按键连续按下
case KEY_LONG:
if(Key_Buf[i].KeyStatus.KEY_FLAG != HIGH_LEVER)//按键释放,端口高电平
{
Key_Buf[i].KeyStatus.KEY_STATUS = KEY_NULL;//转入状态0
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_RAISE;//松开事件
}
else if((Key_Buf[i].KeyStatus.KEY_FLAG == HIGH_LEVER)
&& (++Key_Buf[i].KeyStatus.KEY_TIMECOUNT >= KEY_LONG_DOWN_DELAY))
//超过KEY_LONG_DOWN_DELAY没有释放
{
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_LONG;//长按事件
Key_Buf[i].KeyStatus.KEY_TIMECOUNT = 0;//计数器清零
}
else
{
Key_Buf[i].KeyStatus.KEY_EVENT = KEY_NULL;//空事件
}
break;
default:
break;
}
}
}
实际上从NULL到SURE状态过度是一个按键消抖的过程,一次按键按下存在两次的电平检测
全部代码已经上传到公众号,可以进入公众号获取如上代码。
二、 实现方式
- Switch-case跳转实现有限状态机
- 使用数据结构实现有限状态机
下面是两种常见的方式实现有限状态机
2.1. switch-case
cur_state = nxt_state;
switch(cur_state) //在当前状态中判断事件
{
case s0: //在s0状态
if(e0_event) //如果发生e0事件,那么就执行a0动作,并保持状态不变;
{
//执行a0动作;
//nxt_state = s0; //因为状态号是自身,所以可以删除此句,以提高运行速度。
}
else if(e1_event) //如果发生e1事件,那么就执行a1动作,并将状态转移到s1态;
{
//执行a1动作;
nxt_state = s1;
}
else if(e2_event) //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
{
//执行a2动作;
nxt_state = s2;
}
else
{
break;
}
case s1: //在s1状态
if(e2_event) //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
{
//执行a2动作;
nxt_state = s2;
}
else
{
break;
}
case s2: //在s2状态
if(e0_event) //如果发生e0事件,那么就执行a0动作,并将状态转移到s0态;
{
//执行a0动作;
nxt_state = s0;
}
}
2.2 数据结构
/*有限状态表*/
typedef struct FsmTable_s
{
int event; //事件(实际可以用枚举来表示)
int CurState; //当前状态(实际可以用枚举来表示)
void (*eventActFun)(); //函数指针
int NextState; //下一个状态(实际可以用枚举来表示)
}FsmTable_t;
/*状态机类型*/
typedef struct FSM_s{
int curState;//当前状态
FsmTable_t * pFsmTable;//状态表
int size;//表的项数
}FSM_t;
/*状态机注册,给它一个状态表*/
void FSM_Regist(FSM_t* pFsm, FsmTable_t* pTable)
{
pFsm->pFsmTable = pTable;
}
/*状态迁移*/
void FSM_StateTransfer(FSM_t* pFsm, int state)
{
pFsm->curState = state;
}
/*事件处理*/
void FSM_EventHandle(FSM_t* pFsm, int event)
{
FsmTable_t* pActTable = pFsm->pFsmTable;
void (*eventActFun)() = NULL; //函数指针初始化为空
int NextState;
int CurState = pFsm->curState;
int g_max_num = pFsm->size;
int flag = 0; //标识是否满足条件
int i;
/*获取当前动作函数*/
for (i = 0; i<g_max_num; i++)
{
//当且仅当当前状态下来个指定的事件,我才执行它
if (event == pActTable[i].event && CurState == pActTable[i].CurState)
{
flag = 1;
eventActFun = pActTable[i].eventActFun;
NextState = pActTable[i].NextState;
break;
}
}
if (flag) //如果满足条件了
{
/*动作执行*/
if (eventActFun)
{
eventActFun();
}
//跳转到下一个状态
FSM_StateTransfer(pFsm, NextState);
}
else
{
printf("there is no match\n");
}
}