状态机按键消抖

嵌入式_状态机按键消抖

状态机,FSM(Finite State Machine),也称为同步有限状态机从。指的是在同步电路系统中使用的,跟随同步时钟变化的,状态数量有限的状态机,简称有限状态机。



前言

此代码是在STMF407平台使用标准库函数实现的,需要移植时请根据实际情况进行分析和修改。

按键抖动:按键抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。这是一个很重要的时间参数,在很多场合都要用到。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。键抖动会引起一次按键被误读多次。为确保单片机对按键的一次闭合仅作一次处理,必须去除键抖动。在键闭合稳定时读取键的状态,并且必须判别到按键释放到稳定状态后再去作处理。
在这里插入图片描述

一般软件消抖一般又是直接一个简短延时Delay函数,数据阻塞式消抖效率低下,占用MCU资源,最近研究了一下状态机消抖,不占用MCU资源的非阻塞消抖。
传统硬件消抖:利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。在按键的两端并联一个0.1uf的电容
在这里插入图片描述


一、状态机消抖原理

如图:
初始状态:图以黑色方框表示,此时按键没有按下,MCU会检测到有一个初始电平(可以是高电平也可以是低电平),此时状态没有边沿信号触发,触次数为0,当有手按这个按键时候会产生毛刺,当检测到首次与初始电平不一样的电平信号时,就会变为初始抖动状态。

初始抖动状态:图以右上角灰色方框表示,接上文,按键状态变为初始抖动状态,会持续记录与初始电平不一样的电平信号触发次数,当持续N次时表示已经是持续的反转信号了说明按键确实按下了,此时就会直接跳转到反转状态,表示按键确实被按下。如果某一次检测到的信号还是和初始电平一样,那么就会重新置为初始状态,重新记录触发次数,直至持续N次的反转信号才会跳转到反转状态。

反转状态:图以白色方框表示,接上文,此时按键确实是按下状态,MCU会检测到一个反转电平(与初始状态电平相反),此状态没有边沿信号触发时触次数为0,当有手重新按这个按键或松开按键时候会产生毛刺,当检测到首次与反转电平不一样的电平信号时,就会变为反转抖动状态。

反转抖动状态:图以左下角灰色方框表示,接上文,按键状态变为反转抖动状态,会持续记录与反转电平不一样的电平信号触发次数,当持续N次时表示已经是持续的另一种信号了说明按键确实重新按下或松开,此时就会直接跳转到初始状态,表示按键确实被重新按下或松开。如果某一次检测到的信号还是和反转电平一样,那么就会重新置为反转状态,重新记录触发次数,直至持续N次的反转信号才会跳转到初始状态。

在这里插入图片描述

二、实现步骤

提示:本代码基于STM32F407标准库写的,主义修改及兼容性

1、一共有四个按键,定义一个按键表:

typedef enum 									// 按键表,
{
	KEY0 = 0,
	KEY1,
	KEY2,
	KEY3,
	KEY_NUM, 										// 必须要有的记录按钮数量,必须在最后
}KEY_LIST;

2、再定义一个按键属性结构体

代码如下(示例):

typedef struct
{
	uint8_t	KeysNum;						//序号
	GPIO_TypeDef*	GPIOX;					//端口
	uint16_t GPIO_PinsNum;					//引脚号
	uint16 KEY_TIMECOUNT;					//时间计数器
	GPIO_Pulltype GPIO_Pull;				//初始状态(未按下时候电平 0/1)
	KEY_STATUS key_NowStatus;				//按键当前状态
	uint8_t Key_Event;						//按键事件
}KEY_COMPONENTS;

3、再定义一个按键状态

代码如下(示例):

#define ENUM_ITEM(ITEM) ITEM,					//逗号不可省略
#define ENUM_STRING(ITEM) #ITEM,

#define KEY_STATUS_ENUM(STATUS) 	               \
		STATUS(KS_RELEASE)       		/*稳定松开状态*/       \
		STATUS(KS_PRESS_SHAKE)   		/*按下抖动状态*/       \
		STATUS(KS_PRESS)         		/*稳定按下状态*/       \
		STATUS(KS_RELEASE_SHAKE) 		/*松开抖动状态*/       \
		STATUS(KS_NUM)           		/*状态总数(无效状态)*/  \

4、接口函数

extern void ScanKey(void);								//持续扫描函数
extern KEY_STATUS key_status_check(uint Key_index);		//状态检测函数
extern uint8_t Key_GetPinsVolt(uint8_t Pin_index);		//获取GPIO电平函数

5、按键结构体数据定义以及状态检测切换函数实现

KEY_COMPONENTS Key_TestGrop[KEYSNUMBERS] =
{
	{0,GPIOE,GPIO_Pin_4,0,PuLL_UP,KS_RELEASE,0},			//key0
	{1,GPIOE,GPIO_Pin_3,0,PuLL_UP,KS_RELEASE,0},			//key1
	{2,GPIOE,GPIO_Pin_2,0,PuLL_UP,KS_RELEASE,0},			//key2
	{3,GPIOA,GPIO_Pin_0,0,PuLL_DOWN,KS_RELEASE,0}			//WE_UP
};
/*分别是:编号 端口 引脚号 触发次数计数器 初始电平 初始状态 触发事件标记位*/
/************************************************************************************
*@fuction	:key_status_check
*@brief		:状态机去抖动核心代码
*@param		:--
*@return	:按键状态
*@author	:_Awen
*@date		:2022-12-10
************************************************************************************/
KEY_STATUS key_status_check(uint Key_index)
{
	switch(Key_TestGrop[Key_index].key_NowStatus)
	{
		case KS_RELEASE:	//按键释放状态
		{
			Key_TestGrop[Key_index].KEY_TIMECOUNT = 0;
			/*判断是否有触发,有触发则进入抖动状态*/
			if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_PRESS_SHAKE;	//变为抖动状态
				Key_TestGrop[Key_index].KEY_TIMECOUNT = 0;							//初始触发次数
			}
		}
		break;
		case KS_PRESS_SHAKE://按键为触发抖动状态
		{
			/*累计触发次数+1*/
			Key_TestGrop[Key_index].KEY_TIMECOUNT++;
			/*判断是否有触发,无持续触发则保持正常释放状态*/
			if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE;
			}
			/*检测到持续触发,触发次数大于最大次数,则变为按下状态*/
			else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_PRESS;
			}
		}
		break;
		case KS_PRESS: //触发状态
 		{	
			/*判断有无,无持续触发则进入释放抖动状态*/
			if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE_SHAKE;
				Key_TestGrop[Key_index].KEY_TIMECOUNT = 0;
			}
		}
		break;
		case KS_RELEASE_SHAKE://释放抖动状态
		{
			/*累计触发次数+1*/
			Key_TestGrop[Key_index].KEY_TIMECOUNT++;
			/*判断是否有释放,无持续持续释放则保持触发状态*/
			if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_PRESS;
			}
			/*检测到持续释放信号,释放次数大于最大次数,则变为释放状态*/
			else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE;
			}
		}
		break;
		
		default:break;
	}
	
	/*当前状态与正常释放状态不一致时,返回触发*/
	if(Key_TestGrop[Key_index].key_NowStatus != KS_RELEASE)	//按键按下
	{
		return KS_PRESS;
	}
	else
	{
		return KS_RELEASE;	
	}
}

三、完整代码

1、.h文件

#ifndef __KEY_H
#define __KEY_H

#include "Config.h"

#define KEYSNUMBERS		(4U)						//按键数量
#define MAXDEBOUNCING_DELAY    (4U) 	//消抖计数

#define	GET_VT(i)		Key_GetPinsVolt(i)

#define ENUM_ITEM(ITEM) ITEM,					//逗号不可省略
#define ENUM_STRING(ITEM) #ITEM,

#define KEY_STATUS_ENUM(STATUS) 	               \
		STATUS(KS_RELEASE)       		/*稳定松开状态*/       \
		STATUS(KS_PRESS_SHAKE)   		/*按下抖动状态*/       \
		STATUS(KS_PRESS)         		/*稳定按下状态*/       \
		STATUS(KS_RELEASE_SHAKE) 		/*松开抖动状态*/       \
		STATUS(KS_NUM)           		/*状态总数(无效状态)*/  \

typedef enum
{
	KEY_STATUS_ENUM(ENUM_ITEM)
}KEY_STATUS;

typedef enum 									// 按键表,
{
	KEY0 = 0,
	KEY1,
	KEY2,
	KEY3,
	KEY_NUM, 										// 必须要有的记录按钮数量,必须在最后
}KEY_LIST;

typedef enum									//未按按键状态下,按键高低电平状态
{
	PuLL_DOWN = 0,
	PuLL_UP = !PuLL_DOWN
}GPIO_Pulltype;		

typedef struct
{
	uint8_t	KeysNum;						//序号
	GPIO_TypeDef*	GPIOX;				//端口
	uint16_t GPIO_PinsNum;			//引脚号
	uint16 KEY_TIMECOUNT;				//时间计数器
	GPIO_Pulltype GPIO_Pull;		//初始状态(未按下时候电平 0/1)
	KEY_STATUS key_NowStatus;		//按键当前状态
	uint8_t Key_Event;					//按键事件
}KEY_COMPONENTS;

extern KEY_COMPONENTS Key_TestGrop[];

extern void ScanKey(void);
extern KEY_STATUS key_status_check(uint Key_index);
extern uint8_t Key_GetPinsVolt(uint8_t Pin_index);

#endif

2、.c文件

#include "KEY_Dev.h"

uint8_t KK;

KEY_COMPONENTS Key_TestGrop[KEYSNUMBERS] =
{
	{0,GPIOE,GPIO_Pin_4,0,PuLL_UP,KS_RELEASE,0},			//key0
	{1,GPIOE,GPIO_Pin_3,0,PuLL_UP,KS_RELEASE,0},			//key1
	{2,GPIOE,GPIO_Pin_2,0,PuLL_UP,KS_RELEASE,0},			//key2
	{3,GPIOA,GPIO_Pin_0,0,PuLL_DOWN,KS_RELEASE,0}			//WE_UP
};

/************************************************************************************
*@fuction	:key_status_check
*@brief		:状态机去抖动核心代码
*@param		:--
*@return	:按键状态
*@author	:_Awen
*@date		:2022-12-10
************************************************************************************/
KEY_STATUS key_status_check(uint Key_index)
{
	switch(Key_TestGrop[Key_index].key_NowStatus)
	{
		case KS_RELEASE:	//按键释放状态
		{
			Key_TestGrop[Key_index].KEY_TIMECOUNT = 0;
			/*判断是否有触发,有触发则进入抖动状态*/
			if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_PRESS_SHAKE;	//变为抖动状态
				Key_TestGrop[Key_index].KEY_TIMECOUNT = 0;							//初始触发次数
			}
		}
		break;
		case KS_PRESS_SHAKE://按键为触发抖动状态
		{
			/*累计触发次数+1*/
			Key_TestGrop[Key_index].KEY_TIMECOUNT++;
			/*判断是否有触发,无持续触发则保持正常释放状态*/
			if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE;
			}
			/*检测到持续触发,触发次数大于最大次数,则变为按下状态*/
			else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_PRESS;
			}
		}
		break;
		case KS_PRESS: //触发状态
 		{	
			/*判断有无,无持续触发则进入释放抖动状态*/
			if (GET_VT(Key_index) == Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE_SHAKE;
				Key_TestGrop[Key_index].KEY_TIMECOUNT = 0;
			}
		}
		break;
		case KS_RELEASE_SHAKE://释放抖动状态
		{
			/*累计触发次数+1*/
			Key_TestGrop[Key_index].KEY_TIMECOUNT++;
			/*判断是否有释放,无持续持续释放则保持触发状态*/
			if (GET_VT(Key_index) != Key_TestGrop[Key_index].GPIO_Pull)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_PRESS;
			}
			/*检测到持续释放信号,释放次数大于最大次数,则变为释放状态*/
			else if (Key_TestGrop[Key_index].KEY_TIMECOUNT > MAXDEBOUNCING_DELAY)
			{
				Key_TestGrop[Key_index].key_NowStatus = KS_RELEASE;
			}
		}
		break;
		
		default:break;
	}
	
	/*当前状态与正常释放状态不一致时,返回触发*/
	if(Key_TestGrop[Key_index].key_NowStatus != KS_RELEASE)	//按键按下
	{
		return KS_PRESS;
	}
	else
	{
		return KS_RELEASE;	
	}
}


uint8_t Key_GetPinsVolt(uint8_t Pin_index)
{
	return	GPIO_ReadInputDataBit(Key_TestGrop[Pin_index].GPIOX,Key_TestGrop[Pin_index].GPIO_PinsNum);
}

/************************************************************************************
*@fuction	:ScanKey
*@brief		:
*@param		:--
*@return	:按键状态
*@author	:_Awen
*@date		:2022-12-10
************************************************************************************/
void ScanKey(void)
{
	uint8_t i = 0;
	for(i = 0;i < KEY_NUM;i++)
	{
		if(key_status_check(i) == KS_PRESS)
		{
			KK = i;
		}
	}
}

四、使用方法

1、根据实际需求配置按键表和Key_TestGrop结构体。

2、在while循环或子任务中调用void ScanKey(void)函数.

3、在需要判断按键是否按下或释放的位置调用KEY_STATUS key_status_check(uint Key_index),判断其返回值即可.


总结

1、这里只对反转两种电平信号作判断,扩展一下可以做按下按键、长按按键、短按按键、双击或三击按键做判断。
2、状态机应用非常实用,尤其是Switch case语句常用来作为多状态切换,不同状态使用不同功能的一种常用框架,必须掌握。
如有错误,欢迎指正,原创不易,转载留名!