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;elseKey_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;elseKey_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;//转入状态1Key_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;//转入状态2Key_Buf[i].KeyStatus.KEY_EVENT = KEY_PRESS;//按下事件Key_Buf[i].KeyStatus.KEY_TIMECOUNT = 0;//计数器清零}else{Key_Buf[i].KeyStatus.KEY_STATUS = KEY_NULL;//转入状态0Key_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;//转入状态0Key_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;//转入状态3Key_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;//转入状态0Key_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");}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zs8DVace-1629288147591)(https://pic4.zhimg.com/v2-cbd94ef890e5479c471436b531741474)]

STM32实现按键有限状态机(超详细,易移植)相关推荐

  1. STM32学习笔记(超详细整理144个问题)

    1.AHB系统总线分为APB1(36MHz)和APB2(72MHz),其中2>1,意思是APB2接高速设备: 2.Stm32f10x.h相当于reg52.h(里面有基本的位操作定义),另一个为s ...

  2. STM32学习笔记(超详细整理144个问题)--转

    1.AHB系统总线分为APB1(36MHz)和APB2(72MHz),其中2>1,意思是APB2接高速设备: 2.Stm32f10x.h相当于reg52.h(里面有基本的位操作定义),另一个为s ...

  3. STM32学习笔记(超详细整理145个问题)

    1.AHB系统总线分为APB1(36MHz)和APB2(72MHz),其中2>1,意思是APB2接高速设备: 2.Stm32f10x.h相当于reg52.h(里面有基本的位操作定义),另一个为s ...

  4. VS2022安装教学(超详细易上手)

    今天为大家分享Visual Studio的最新版本VS2022社区版(也就是个人免费使用的),下面就是安装的详细步骤. Step1.打开Visual Studio的官网(附链接) https://vi ...

  5. STM32中断总结(超详细)

    中断类型 系统异常,体现在内核水平 外部中断,体现在外设水平 关于系统异常和外部中断的向量表见最后. NVIC 定义:嵌套向量中断定时器,属于内核外设,管理者包括内核和片上所有外设的中断相关功能 两个 ...

  6. 超详细易理解的HTTPS(易上手哦)

    文章目录 1. 什么是 https 2.为什么要引入https 3.https工作流程 3.1对称加密 3.2非对称加密 3.3引入证书 数据指纹 未使用证书产生的问题 使用证书 4.https传输总 ...

  7. stm32正常运行流程图_STM32单片机学习笔记(超详细整理143个问题,学习必看)...

    原标题:STM32单片机学习笔记(超详细整理143个问题,学习必看) 1.AHB系统总线分为APB1(36MHz)和APB2(72MHz),其中2>1,意思是APB2接高速设备 2.Stm32f ...

  8. stm32移植paho_如何在STM32上移植Linux?超详细的实操经验分享

    原标题:如何在STM32上移植Linux?超详细的实操经验分享 刚从硬件跳槽为嵌软时,没有任何一丝的准备.一入职,领导就交代了一项特难的任务--在stm32上移植linux! 瞬间我就懵了,没办法硬着 ...

  9. stm32怎么加载字库_收藏 | STM32单片机超详细学习汇总资料(二)

    点击"蓝字"关注我们 3110月 收藏 | STM32单片机超详细学习汇总资料(一) ◆41.DMA仲裁器分为软件和硬件两种.软件部分分为4个等级,分别是很高优先级.高优先级.中等 ...

最新文章

  1. 如何快速融入团队(六)
  2. Microsoft MSDN Windows 8 各版本下载
  3. python入门教程非常详细-Python 基础教程
  4. htc desire 10 pro android 8.0,HTC Desire 10 pro手机:可能是Desire系列最好的手机
  5. EOS 智能合约源代码解读 (10)token合约“几种关键操作”
  6. github 运行python_Github Actions教程:运行python代码并Push到远端仓库
  7. JavaScript 使用变量访问对象属性
  8. android slidingmenu 兼容低版本,Android SlidingMenu的使用详解
  9. bibibi 下载_哔哩哔哩下载电脑版_哔哩哔哩官方版下载[bilibili]-下载之家
  10. 小技巧:机械键盘使用技巧
  11. 原来JSON还可这样玩着
  12. python 做深度学习时偶遇的 (0xC0000409)错误
  13. 【无标题】【光纤光缆小知识】多模光纤的分类及应用
  14. POJ原题测试数据合集+使用方法
  15. java征兵系统2.0
  16. 《巴菲特之道》精髓:巴菲特的股神进阶之路和投资方法
  17. 天津计算机专业专科大学排名,天津的计算机专业大学排名
  18. 基础算法 - 树的直径
  19. xxljob默认登录_三千字带你搞懂XXL-JOB任务调度平台
  20. 2020年华为杯第十七届中国研究生数学建模竞赛---回顾记录

热门文章

  1. 一句话概括Kubernetes架构
  2. 自然语言处理学习——文本相似度检测Semantic Textual Similarity之一些资料和研究
  3. 网页不能复制文字?这几招轻松解决
  4. 15-DOM 事件流(事件冒泡)
  5. 对于32位系统利用lui ori 获得32位数的操作
  6. 梦幻诛仙11职业linux架设手游,一款【梦幻诛仙11职业】手游端私服架设+JAVA后台+架设视频教程...
  7. 计算机毕业设计Java计算机类专业考研交流学习平台(源码+系统+mysql数据库+lw文档)
  8. java毕业设计高校智慧校园学生系统mybatis+源码+调试部署+系统+数据库+lw
  9. Java高级----多线程
  10. Nacos-1.3.2之服务注册