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 = 1MHZ
TIM_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);
//配置NVIC
NVIC_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)
{
//判断串口接收标志位是否置1
if(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~3
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Delay_Init();
Led_Init();
Usart1_Init(115200);
key_state_init();
while(1)
{
delay_s(2);
}
return 0;
}
实验效果