STM32F4按键状态机--单击、双击、长按

STM32F4按键状态机--单击、双击、长按

  • 一、状态机的三要素
  • 二、使用状态机原因
    • 2.1资源占用方面
    • 2.2 执行效率方面:
    • 2.3 按键抖动方面:
  • 三、状态机实现
    • 3.1 状态机分析
    • 3.1 程序实现

百度解析的状态机概念如下

状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作、完成特定操作的控制中心。有限状态机简写为FSM(Finite State Machine),主要分为2大类:

第一类,若输出只和状态有关而与输入无关,则称为Moore状态机
第二类,输出不仅和状态有关而且和输入有关系,则称为Mealy状态机
在这里插入图片描述

简而言之,状态机是使不同状态之间的改变以及状态时产生的相应动作的一种机制。

一、状态机的三要素

状态机,或称有限状态机FSM(Finite State Machine),是一种重要的编程思想。

状态机有3要素:状态、事件与响应

状态:系统处在什么状态?

事件:发生了什么事?

响应:此状态下发生了这样的事,系统要如何处理?

状态机编程前,首先要根据需要实现的功能,整理出一个对应的状态转换图(状态机图),然后就可以根据这个状态转换图,套用状态机编程模板,实现对应是状态机代码了。

状态机编程主要有 3 种方法:switch-case 法、表格驱动法、函数指针法,本篇先介绍最简单也最易理解的switch-case 法。

二、使用状态机原因

举一个简单的例子,在实现按键扫描常常有三种方式

(1)轮询方式:main函数大循环中加入按键扫描函数key_scan(),相信大家最开始接触的也是这个
(2)中断方式:在单片机中大多数都支持外部中断,但每一个(或几个)IO口占用一个中断向量,使用非常方便。
(3)状态机方式:在我看来这种方式优于前两者任意一个,为什么呢?我们来来看看。

2.1资源占用方面

​ 轮询方式:在主循环中一直占用CPU的执行。

​ 中断方式:仅仅在时间产生后跳转执行后回调,相对于轮询占用的资源少很多,但很多按键时需要多个中断向量。

​ 状态机方式:在使用状态机实现按键扫描时,我们仅仅需要一个定时器即可实现任意个按键的扫描,效率不低于中断方式。

2.2 执行效率方面:

​ 轮询方式:效率极低,反应且不灵敏,有时候要按很多次才有反应。

​ 中断方式:效率较高,反应灵敏,单独的中断方式不支持长按等状态操作。

​ 状态机方式:效率较高,反应灵敏,支持长按的状态操作。

2.3 按键抖动方面:

​ 轮询方式:需要延时消抖,消抖的同时无法进行其他操作。

​ 中断方式:需要延时消抖,消抖的同时无法进行其他操作。

​ 状态机方式:间接的产生了消抖,为什么这么说呢?这里我们采用一个定时时间为50ms的定时器,每50ms进行一次状态的处理,在上一次处理和下一次处理的50ms中可以跳出定时器中断进行其他操作,也就是消抖的同时在进行其他的操作,大大的提高了运行的效率。

三、状态机实现

3.1 状态机分析

以下相关技术参考:B站-码农爱学习博主相关,如果不妥,请联系博主删文。

先看按实现单击、双击、长按状态图
在这里插入图片描述
状态机分析需要注意事项:
(1)“确认按下”不是短按触发的条件,需要等松开后,经消抖进入到“等待再次按下”一段时间后(200ms),没有再次被按下,才触发短按事件。

(2)“确认按下”不是短按触发的条件,另一个用途是,当此状态继续保持按下状态一段时间后(1s),则会单独触发长按事件,同时进入到“确认长按”状态,这样就解决了本篇开头提到的第2个问题

(3)对于双击事件的检测,首先按下按键进入“确认按下”状态,然后在1s内松开进入“等待再次按下”状态,接着在200ms内再次按下进入“确认第2次按下”状态,然后在1s内松开,即可触发双击事件,并同时进入“稳定松开”状态

注意,在“确认第2次按下”状态下,如果在1s内没有松开,也会进入到“确认长按”状态。

3.1 程序实现

硬件连接
在这里插入图片描述
key_state.h

#ifndef __KEY_STATE_H
#define __KEY_STATE_H#include "stm32f4xx.h"
#include "sys.h"#define KEY0  PAin(0)void key_state_init(void);
u8 key_status_check(void);#endif

key_state.c

#include "key_state.h"typedef enum
{KE_SHORT_PRESS,KE_DOUBLE_PRESS,KE_LONG_PRESS,KE_OTHER,
}KEY_EVENT;typedef enum
{KS_RELEASE,				//0-稳定松开状态KS_SHAKE,				//1-抖动KS_AFFIRM_SHORT_PRESS,	//2-确认按下KS_WAIT_PRESS_AGAIN,	//3-等待再次按下KS_AFFIRM_PRESS_AGAIN,	//4-确认第2次按下KS_AFFIRM_LONG_PRESS,   //5-确认长按}KEY_STATUS;KEY_STATUS g_keyStatus 		= KS_RELEASE; //当前循环结束的(状态机的)状态
KEY_STATUS g_nowKeyStatus 	= KS_RELEASE; //当前状态(每次循环后与g_keyStatus保持一致)
KEY_STATUS g_lastKeyStatus 	= KS_RELEASE; //上次状态(用于记录前一状态以区分状态的来源)u16 g_PressTimeCnt = 0;  		//第一次按下时间计数
u16 g_Press2TimeCnt = 0;		//第二次按下时间计数
u16 g_WaitPressAgainCnt = 0;	//再次按下时间计数
u16 g_LongPressTimeCnt = 0;		//长按时间计数u8 g_value = 5;/******************************************
定时器说明TIM3 -- APB1(16位定时器)TIM3定时器频率:84MHZ
*******************************************/
void Tim3_state_Init(void)
{TIM_TimeBaseInitTypeDef		TIM_TimeBaseInitStruct;NVIC_InitTypeDef   			NVIC_InitStructure;//1、能定时器时钟。RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);TIM_TimeBaseInitStruct.TIM_Prescaler	= (84-1);  //84分频 84MHZ/84 = 1MHZTIM_TimeBaseInitStruct.TIM_Period		= (50000-1);//计1000个数产生中断, 在1MHZ,1ms产生中断TIM_TimeBaseInitStruct.TIM_CounterMode  = TIM_CounterMode_Up; //向上计数TIM_TimeBaseInitStruct.TIM_ClockDivision= TIM_CKD_DIV1;//分频因子//2、初始化定时器,配置ARR,PSC。TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);//配置NVICNVIC_InitStructure.NVIC_IRQChannel 					 = TIM3_IRQn; //中断通道,中断的通道只能在stm32f4xx.h查找NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01; //抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority 		 = 0x01; //响应优先级NVIC_InitStructure.NVIC_IRQChannelCmd 				 = ENABLE; //通道使能NVIC_Init(&NVIC_InitStructure);//4、设置 TIM3_DIER  允许更新中断TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);//5、使能定时器。TIM_Cmd(TIM3, ENABLE);}/***********************************
按PA0做为状态机按键TIM3每隔10ms检测一次按键状态***********************************/void key_state_init(void)
{//5ms中断一次Tim3_state_Init();GPIO_InitTypeDef  GPIO_InitStruct;//使能GPIOA组时钟RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);GPIO_InitStruct.GPIO_Pin	= GPIO_Pin_0; 		//引脚GPIO_InitStruct.GPIO_Mode	= GPIO_Mode_IN;		//输出GPIO_InitStruct.GPIO_PuPd	= GPIO_PuPd_UP;		//上拉GPIO_Init(GPIOA, &GPIO_InitStruct);GPIO_InitStruct.GPIO_Pin	= GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4; 		//引脚GPIO_InitStruct.GPIO_Mode	= GPIO_Mode_IN;		//输出GPIO_InitStruct.GPIO_PuPd	= GPIO_PuPd_UP;		//上拉GPIO_Init(GPIOE, &GPIO_InitStruct);}//50ms检测一次
u8 key_status_check(void)
{switch(g_keyStatus){//按键释放(初始状态)case KS_RELEASE:  {//检测到低电平,先进行消抖if (KEY0 == 0){g_keyStatus = KS_SHAKE;//	g_lastKeyStatus = KS_SHAKE;}}break;//抖动case KS_SHAKE:{if (KEY0 == 1) //松开判断{//从松开状态来的抖动if (KS_RELEASE == g_lastKeyStatus){g_keyStatus = KS_RELEASE;}//从等待再次按下状态来的抖动else if (KS_WAIT_PRESS_AGAIN == g_lastKeyStatus){g_keyStatus = KS_WAIT_PRESS_AGAIN;}//从确认按下状态来else if (KS_AFFIRM_SHORT_PRESS == g_lastKeyStatus){g_WaitPressAgainCnt = 0;g_keyStatus = KS_WAIT_PRESS_AGAIN;}//从确认再次按下状态来else if (KS_AFFIRM_PRESS_AGAIN == g_lastKeyStatus){printf("=====> key double press\r\n");g_value = 1;g_keyStatus = KS_RELEASE;return g_value;}//从确认长按状态来else if (KS_AFFIRM_LONG_PRESS == g_lastKeyStatus){g_keyStatus = KS_RELEASE;}else{printf("err!\r\n");}}else{//从确认按下状态来的抖动if (KS_AFFIRM_SHORT_PRESS == g_lastKeyStatus){g_keyStatus = KS_AFFIRM_SHORT_PRESS;}//从第2次按下状态来的抖动else if (KS_AFFIRM_PRESS_AGAIN == g_lastKeyStatus){g_keyStatus = KS_AFFIRM_PRESS_AGAIN;}//从确认长按状态来的抖动else if (KS_AFFIRM_LONG_PRESS == g_lastKeyStatus){g_keyStatus = KS_AFFIRM_LONG_PRESS;}//从松开状态而来else if (KS_RELEASE == g_lastKeyStatus){g_PressTimeCnt = 0;g_keyStatus = KS_AFFIRM_SHORT_PRESS;//printf("=====> key short press\r\n");//return KE_SHORT_PRESS;}//从等待再次看下(的松开)状态而来else if (KS_WAIT_PRESS_AGAIN == g_lastKeyStatus){g_Press2TimeCnt = 0;g_keyStatus = KS_AFFIRM_PRESS_AGAIN;}else{printf("err!\r\n");}}}break;//确认按下case KS_AFFIRM_SHORT_PRESS:{//检测到高电平,先进行消抖if (KEY0 == 1){g_keyStatus = KS_SHAKE;}else{if (g_LongPressTimeCnt % 20 == 0) //每隔1000ms打印一次{g_value = 2;printf("=====> vt key long press:%d\r\n", g_LongPressTimeCnt/20);g_keyStatus = KS_AFFIRM_LONG_PRESS;return g_value;}g_LongPressTimeCnt++;}}break;//等待再次按下case KS_WAIT_PRESS_AGAIN:{//检测到低电平,先进行消抖if (KEY0 == 0){g_keyStatus = KS_SHAKE;}g_WaitPressAgainCnt++;if (g_WaitPressAgainCnt == 4) //200ms没有再次按下{//	g_WaitPressAgainCnt = 0;printf("=====> key single press\r\n");g_value = 0;g_keyStatus = KS_RELEASE;return g_value;}}break;//确认第2次按下case KS_AFFIRM_PRESS_AGAIN:{//检测到高电平,先进行消抖if (KEY0 == 1){g_keyStatus = KS_SHAKE;}g_Press2TimeCnt++;if (g_Press2TimeCnt == 20) //1000ms{g_LongPressTimeCnt = 0;g_keyStatus = KS_AFFIRM_LONG_PRESS;}}break;//确认长按case KS_AFFIRM_LONG_PRESS:{//检测到高电平,先进行消抖if (KEY0 == 1){g_keyStatus = KS_SHAKE;break;}g_LongPressTimeCnt++;if (g_LongPressTimeCnt % 20 == 0) //每隔1000ms打印一次{g_WaitPressAgainCnt = 0;g_value = 2;printf("=====> key long press:%d\r\n", g_LongPressTimeCnt/20);return g_value;}}break;default:break;}if (g_keyStatus != g_nowKeyStatus){g_lastKeyStatus = g_nowKeyStatus;g_nowKeyStatus = g_keyStatus;g_value = 3;//	printf("new key status:%d(%s)\r\n", g_keyStatus, key_status_name[g_keyStatus]);}return 8; //返回其它的值,也就是按键未有动作的值
}//定时器3中断服务程序
void TIM3_IRQHandler(void)   //TIM3中断
{if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否{TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志g_value = key_status_check();switch (g_value){case 0:  printf("检测到单击\r\n"); break;case 1: printf("检测到双击\r\n"); break;case 2:   printf("检测到长按\r\n"); break;default:break;}}
}

main.c

#include "stm32f4xx.h"
#include "led.h"
#include "key.h"
#include "exti.h"
#include "delay.h"
#include "tim.h"
#include "pwm.h"
#include "usart.h"
#include "string.h"
#include "sr04.h"
#include "dht11.h"
#include "key_state.h"u8 g_data;
u8 g_flag = 0, g_count = 0;
u8 g_buffer[32] = {0};
u8 g_rxbuffer[32] = {0};void USART1_IRQHandler(void)
{//判断串口接收标志位是否置1if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET){USART_ClearITPendingBit(USART1, USART_IT_RXNE);	//从串口1接收数据g_buffer[g_count++] = USART_ReceiveData(USART1); //判断接受的字符是否为':'if(g_buffer[g_count-1] == ':'){//数据重新存放在g_rxbuffer,并过滤结束符':'for(int i = 0; i < g_count-1; i++){g_rxbuffer[i] = g_buffer[i];}memset(g_buffer, 0, sizeof(g_buffer));g_flag = 1;  //表示一帧数据(HCL11:或者 HCL10:)接受完毕g_count = 0; //一帧数据结束后,g_count置为0,下一帧数据从g_buffer[0]开始接受数据}}}//粗延时
void delay(int n)
{int i, j;for(i=0; i<n; i++){for(j=0; j<30000; j++){}}}int main(void)
{int ret;//设置NVIC分组(一个项目只能配置一次)//第2分组,抢占优先级范围:0~3  响应优先级范围:0~3NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);Delay_Init();Led_Init();Usart1_Init(115200);key_state_init();while(1){delay_s(2);}return 0;
}

实验效果
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/417865.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习 --- VGG16能让某个指定的feature map激活值最大化图片的可视化(JupyterNotebook实战)

VGG16能让某个指定的feature map激活值最大化图片的可视化 在前面的文章中&#xff0c;我用jupyter notebook分别实现了&#xff0c;预训练好的VGG16模型各层filter权重的可视化和给VGG16输入了一张图像&#xff0c;可视化VGG16各层的feature map。深度学习 --- VGG16卷积核的可…

Python 优雅编程:会报恩的代码(五)

文章目录 引言从文本搜索指定单词&#xff0c;不区分单词的大小写使用 str.lower()使用 re 模块 从文本搜索多个单词&#xff0c;依旧不区分单词的大小写使用 str.lower() 和循环使用 re 模块 反复执行 re.compile&#xff0c;re 是否会缓存编译结果&#xff1f;结语 引言 在 …

day47——面向对象特征之继承

一、继承&#xff08;inhert&#xff09; 面向对象三大特征&#xff1a;封装、继承、多态 继承&#xff1a;所谓继承&#xff0c;是类与类之间的关系。就是基于一个已有的类&#xff0c;来创建出一个新类的过程叫做继承。主要提高代码的复用性。 1.1 继承的作用 1> 实现…

【一嗨租车-注册安全分析报告-滑动验证加载不正常导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

UE4_后期处理_后期处理材质及后期处理体积三—遮挡物体描边显示

一、效果&#xff1a; 在很多游戏中为了玩家能看到墙面背后是否有敌人&#xff0c;会给被遮挡的敌人增加描边显示&#xff0c;效果如下&#xff1a; 参考&#xff1a; https://zhuanlan.zhihu.com/p/81310476 https://zhuanlan.zhihu.com/p/358140547 二、所需知识 知识点…

Java笔试面试题AI答之JDBC(3)

文章目录 13. 编写JDBC连Oracle的程序?14. 简述JDBC的主要组件有哪些 &#xff1f;15. JDBC中如何防止SQL注入攻击&#xff1f;1. 使用预处理语句&#xff08;PreparedStatement&#xff09;2. 避免在SQL查询中直接拼接用户输入的数据总结 16. JDBC的脏读是什么&#xff1f;哪…

Spring01——Spring简介、Spring Framework架构、Spring核心概念、IOC入门案例、DI入门案例

为什么要学 spring技术是JavaEE开发必备技能&#xff0c;企业开发技术选型命中率>90%专业角度 简化开发&#xff1a;降低企业开发的复杂度框架整合&#xff1a;高效整合其他技术&#xff0c;提高开发与运行效率 学什么 简化开发 IOCAOP 事务处理 框架整合 MyBatis 怎…

深度学习的基础_多层感知机的手动实现

多层感知机&#xff08;Multilayer Perceptron&#xff0c;简称MLP&#xff09;是一种前馈人工神经网络。它包含至少三层节点&#xff1a;一个输入层、一个或多个隐藏层以及一个输出层。除输入节点外&#xff0c;每个节点都是一个带有非线性激活函数的神经元&#xff08;或称为…

Word快速重复上一步操作的三种高效方法

在日常工作、学习和生活中&#xff0c;我们经常需要执行一系列重复性的操作。这些操作可能简单如复制粘贴、调整图片大小&#xff0c;也可能复杂如编辑文档、处理数据等。为了提高效率&#xff0c;掌握快速重复上一步操作的方法显得尤为重要。本文将介绍三种高效的方法&#xf…

给力!Python配置文件,这一篇就够了!

在开发过程中&#xff0c;我们常常会用到一些固定参数或者是常量。对于这些较为固定且常用到的部分&#xff0c;往往会将其写到一个固定文件中&#xff0c;避免在不同的模块代码中重复出现从而保持核心代码整洁。 这里插播一条粉丝福利&#xff0c;如果你在学习Python或者有计划…

【C题成品论文已出】24数学建模国赛C题成品论文(附参考代码)免费分享

24高教社杯数学建模国赛C题成品论文 一、问题一模型建立与求解 1.1模型建立 &#xff08;1&#xff09;决策变量设计 表示一个26158的矩阵&#xff0c;其中26是平旱地梯田和山坡地的总数&#xff0c;15是在这几类土地上可以种植的农作物数量&#xff0c;8则表示从2023到203…

KCP实现原理探析

KCP 是一个轻量级的、高效的、面向 UDP 的传输协议库&#xff0c;专为需要低延迟和高可靠性的实时应用设计。本文针对 KCP 的主要机制和实现与原理进行分析。 1. 术语 术语 全称 说明 TCP Transmission Control Protocol 传输控制协议 RTT Round Trip Time 往返时延 …

【鸿蒙HarmonyOS NEXT】调用后台接口及List组件渲染

【鸿蒙HarmonyOS NEXT】调用后台接口及List组件渲染 一、环境说明二、调用后台接口及List组件渲染三、总结 一、环境说明 DevEco Studio 版本&#xff1a; API版本&#xff1a;以12为主 二、调用后台接口及List组件渲染 后台接口及返回数据分析 JSON数据格式如下&#xf…

Git创建项目

方法一 1.在gitee中新建仓库demo01&#xff0c;并勾选开源许可证&#xff0c;完成后gitee上面的项目demo01里只包含一个LICENSE文件 2.直接在本地电脑中新建项目文件夹demo01&#xff0c;双击进入这个文件夹&#xff0c;右键Git bash here&#xff0c;输入 git clone https:…

跨域问题(CORS)

文章目录 介绍解决一、添加跨域头&#xff0c;允许跨域1.后端配置CORS策略(4种方法)2.配置nginx 二、代理 介绍 跨域资源共享&#xff08;CORS, Cross-Origin Resource Sharing&#xff09;是浏览器的一个安全机制&#xff0c;用来防止来自一个域的网页对另一个域下的资源进行…

Linux操作系统在虚拟机VM上的安装【CentOS版本】

目录 准备工作 "CPU虚拟化"的方法 VMware的安装 Linux镜像文件的下载 开始安装 声明 新建虚拟机 安装CentOS7.6 配置Linux(CentOS7.6)操作系统 配置分区【学习者可以直接点击自动配置分区&#xff0c;不过还是建议学习一下手动分区】 分区原则 添加分区 …

提示工程颠覆:DSPy 引领全新范式革命

几个月前,我清楚地记得,Prompt Engineering 还是热门话题。就业市场上充斥着提示工程师的岗位,仿佛这是未来的必备技能。 然而,现在情况已经大不相同了。提示工程并不是一门艺术或科学,更像是“聪明的汉斯”现象——人类为系统提供了必要的背景,以便系统能更好地作出回应…

Maven聚合与继承

聚合 当我们一次想要构建多个项目时&#xff0c;而不是到每一个模块的目录下分别执行mvn命令。这个时候就需要使用到maven的聚合特性 这里第一个特殊的地方是packaging&#xff0c;值设置为pom。我们正常开发的其他模块中都没有声明packaging&#xff0c;默认使用了默认值jar&a…

【Qt】仿照qq界面的设计

widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDebug>//QT中信息调试类&#xff0c;用于输出数据&#xff0c;无需使用该类的实例化对象&#xff0c;直接使用成员函数即可 #include <QIcon>//图标类 #include <QPushButton&…

代码随想录——回文子串(Leetcode 647)

题目链接 我的题解&#xff08;双指针&#xff09; 思路&#xff1a; 当然&#xff0c;以下是对您提供的代码的解释&#xff1a; class Solution {public int countSubstrings(String s) {// 初始化回文子字符串的数量int count 0;// 遍历字符串的每个字符&#xff0c;使用…