【详解~按键状态机~功能Plus】2.实现单击、双击、长按的功能

按键状态机讲解见:【详解~按键状态机】1.实现短按长按的功能

本文长按短按的基础上,增加了双击功能。

1.问题描述

使用一个按键,实现长按、单击、双击操作。运用状态机思想,提高效率。

  • 外设:io口、定时器

2.单击、双击、长按的定义

  • 长按事件:任何大于 1秒 按下并释放事件(不支持连按,需连按,稍微修改状态机即可)
  • 单击事件:按下时间不超过 1秒 且 释放后 500ms 内无再次按下的操作
  • 双击事件:俩次短按时间间隔小于500ms,俩次短按操作合并为一次双击事件。

特殊情况说明:
1.短按和长按时间间隔小于 500ms,响应一次单击和长按事件,不响应双击事件
2.连续2n次短按,且时间间隔小于 500ms,响应为n次双击
3.连续2n+1次短按,且时间间隔小于 500ms,且最后一次500ms内无操作。
注意:
建议每次事件后,500ms后再操作。

3.代码变更讲解

3.1 宏定义

增加了,等待第二次按下的时间间隔,500ms。

/**************************************************************************************************** 
*                             长按、单击、双击定义
* 长按事件:任何大于 KEY_LONG_PRESS_TIME 
* 单击事件:按下时间不超过 KEY_LONG_PRESS_TIME 且 释放后 KEY_WAIT_DOUBLE_TIME 内无再次按下的操作
* 双击事件:俩次短按时间间隔小于KEY_WAIT_DOUBLE_TIME,俩次短按操作合并为一次双击事件。
* 特殊说明:
*          1.短按和长按时间间隔小于 KEY_WAIT_DOUBLE_TIME,响应一次单击和长按事件,不响应双击事件
*          2.连续2n次短按,且时间间隔小于 KEY_WAIT_DOUBLE_TIME,响应为n次双击
*          3.连续2n+1次短按,且时间间隔小于 KEY_WAIT_DOUBLE_TIME,且最后一次KEY_WAIT_DOUBLE_TIME内无操作,
*				响应为n次双击 和 一次单击事件
****************************************************************************************************/
#define KEY_LONG_PRESS_TIME    50 // 20ms*50 = 1s
#define KEY_WAIT_DOUBLE_TIME   25 // 20ms*25 = 500ms
#define KEY_PRESSED_LEVEL      0  // 按键按下是电平为低

3.2 结构体、枚举型

增加了双击的事件

// 按键事件
typedef enum _KEY_EventList_TypeDef 
{
    
    
	KEY_Event_Null 		   = 0x00,  // 无事件
	KEY_Event_SingleClick  = 0x01,  // 单击
	KEY_Event_DoubleClick  = 0x02,  // 双击
	KEY_Event_LongPress    = 0x04   // 长按
}KEY_EventList_TypeDef;

增加了一些按键的状态:
要实现双击,
KEY_Status_WaiteAgain:等待二次按下的状态
KEY_Status_SecondPress:确认有第二次按下

// 按键状态
typedef enum _KEY_StatusList_TypeDef 
{
    
    
	KEY_Status_Idle = 0				, // 空闲
	KEY_Status_Debounce   		    , // 消抖
	KEY_Status_ConfirmPress		    , // 确认按下	
	KEY_Status_ConfirmPressLong		, // 确认长按着	
	KEY_Status_WaiteAgain		    , // 等待再次按下
	KEY_Status_SecondPress          , // 第二次按下
}KEY_StatusList_TypeDef;

其他不变

// 按键动作,按下、释放
typedef enum
{
    
     
	KEY_Action_Press = 0,
	KEY_Action_Release
}KEY_Action_TypeDef;

// 按键引脚的电平
typedef enum
{
    
     
	KKEY_PinLevel_Low = 0,
	KEY_PinLevel_High
}KEY_PinLevel_TypeDef;


// 按键配置结构体
typedef struct _KEY_Configure_TypeDef 
{
    
    
	uint16_t                        KEY_Count;        //按键长按计数
	KEY_Action_TypeDef             KEY_Action;        //按键动作,按下1,抬起0
	KEY_StatusList_TypeDef         KEY_Status;        //按键状态
	KEY_EventList_TypeDef          KEY_Event;          //按键事件
	KEY_PinLevel_TypeDef          (*KEY_ReadPin_Fcn)(void);    //读IO电平函数
}KEY_Configure_TypeDef;

3.3 按键全局变量

KeyCfg 保存了按键配置信息。

/**************************************************************************************************** 
*                             全局变量
****************************************************************************************************/
KEY_Configure_TypeDef KeyCfg={
    
    		
		0,						//按键长按计数
		KEY_Action_Release,		//虚拟当前IO电平,按下1,抬起0
		KEY_Status_Idle,        //按键状态
		KEY_Event_Null,         //按键事件
		KEY_ReadPin             //读IO电平函数
};

3.4 函数定义

基于STM32F103C8单片机。
使用引脚为PA0,电路图如下:
在这里插入图片描述
代码如下

/**************************************************************************************************** 
*                             函数定义
****************************************************************************************************/
// 按键读取按键的电平函数,更具实际情况修改
static KEY_PinLevel_TypeDef KEY_ReadPin(void) //按键读取函数
{
    
    
  return (KEY_PinLevel_TypeDef) GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);
}
// 获取按键是按下还是释放,保存到结构体
static void KEY_GetAction_PressOrRelease(void) // 根据实际按下按钮的电平去把它换算成虚拟的结果
{
    
    
	if(KeyCfg.KEY_ReadPin_Fcn() == KEY_PRESSED_LEVEL)
	{
    
    
		KeyCfg.KEY_Action = KEY_Action_Press;
	}
	else
	{
    
    
		KeyCfg.KEY_Action =  KEY_Action_Release;
	}
}

//按键初始化函数
void KEY_Init(void) //IO初始化
{
    
     
 	GPIO_InitTypeDef GPIO_InitStructure;
 
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能PORTA时钟
	//初始化 WK_UP-->GPIOA.0	  
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
}
关键函数

读取按键状态机的函数,思路与我的前一篇博客手绘图解的类似。只是多了几个状态而已。
主要思路:

首先读取按键的动作,再在switch case 里面匹配引脚的状态,case下用if判断按键动作或按下的时长,对状态、事件进行赋值。

参考代码:

/**************************************************************************************************** 
*                             读取按键状态机
****************************************************************************************************/
void KEY_ReadStateMachine(void)
{
    
    
    KEY_GetAction_PressOrRelease();
	
	switch(KeyCfg.KEY_Status)
	{
    
    
		//状态:没有按键按下
		case KEY_Status_Idle:
			if(KeyCfg.KEY_Action == KEY_Action_Press)
			{
    
    
				KeyCfg.KEY_Status = KEY_Status_Debounce;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			else
			{
    
    
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			break;
			
		//状态:消抖
		case KEY_Status_Debounce:
			if(KeyCfg.KEY_Action == KEY_Action_Press)
			{
    
    
				KeyCfg.KEY_Status = KEY_Status_ConfirmPress;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			else
			{
    
    
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			break;	


		//状态:继续按下
		case KEY_Status_ConfirmPress:
			if( (KeyCfg.KEY_Action == KEY_Action_Press) && ( KeyCfg.KEY_Count >= KEY_LONG_PRESS_TIME))
			{
    
    
				KeyCfg.KEY_Status = KEY_Status_ConfirmPressLong;
				KeyCfg.KEY_Event = KEY_Event_Null;
				KeyCfg.KEY_Count = 0;
			}
			else if( (KeyCfg.KEY_Action == KEY_Action_Press) && (KeyCfg.KEY_Count < KEY_LONG_PRESS_TIME))
			{
    
    
				KeyCfg.KEY_Count++;
				KeyCfg.KEY_Status = KEY_Status_ConfirmPress;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			else
			{
    
    
				KeyCfg.KEY_Count = 0;
				KeyCfg.KEY_Status = KEY_Status_WaiteAgain;// 按短了后释放
				KeyCfg.KEY_Event = KEY_Event_Null;

			}
			break;	
			
		//状态:一直长按着
		case KEY_Status_ConfirmPressLong:
			if(KeyCfg.KEY_Action == KEY_Action_Press) 
			{
    
       // 一直等待其放开
				KeyCfg.KEY_Status = KEY_Status_ConfirmPressLong;
				KeyCfg.KEY_Event = KEY_Event_Null;
				KeyCfg.KEY_Count = 0;
			}
			else
			{
    
    
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_LongPress;
				KeyCfg.KEY_Count = 0;
			}
			break;	
			
		//状态:等待是否再次按下
		case KEY_Status_WaiteAgain:
			if((KeyCfg.KEY_Action != KEY_Action_Press) && ( KeyCfg.KEY_Count >= KEY_WAIT_DOUBLE_TIME))
			{
    
       // 第一次短按,且释放时间大于KEY_WAIT_DOUBLE_TIME
				KeyCfg.KEY_Count = 0;
				KeyCfg.KEY_Status = KEY_Status_Idle;  
				KeyCfg.KEY_Event = KEY_Event_SingleClick;// 响应单击
				
			}
			else if((KeyCfg.KEY_Action != KEY_Action_Press) && ( KeyCfg.KEY_Count < KEY_WAIT_DOUBLE_TIME))
			{
    
    // 第一次短按,且释放时间还没到KEY_WAIT_DOUBLE_TIME
				KeyCfg.KEY_Count ++;
				KeyCfg.KEY_Status = KEY_Status_WaiteAgain;// 继续等待
				KeyCfg.KEY_Event = KEY_Event_Null;
				
			}
			else // 第一次短按,且还没到KEY_WAIT_DOUBLE_TIME 第二次被按下
			{
    
    
				KeyCfg.KEY_Count = 0;
				KeyCfg.KEY_Status = KEY_Status_SecondPress;// 第二次按下
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			break;		
		case KEY_Status_SecondPress:
			if( (KeyCfg.KEY_Action == KEY_Action_Press) && ( KeyCfg.KEY_Count >= KEY_LONG_PRESS_TIME))
			{
    
    
				KeyCfg.KEY_Status = KEY_Status_ConfirmPressLong;// 第二次按的时间大于 KEY_LONG_PRESS_TIME
				KeyCfg.KEY_Event = KEY_Event_SingleClick; // 先响应单击
				KeyCfg.KEY_Count = 0;
			}
			else if( (KeyCfg.KEY_Action == KEY_Action_Press) && ( KeyCfg.KEY_Count < KEY_LONG_PRESS_TIME))
			{
    
    
                KeyCfg.KEY_Count ++;
				KeyCfg.KEY_Status = KEY_Status_SecondPress;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
            else 
            {
    
    // 第二次按下后在 KEY_LONG_PRESS_TIME内释放
                KeyCfg.KEY_Count = 0;
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_DoubleClick; // 响应双击
            }
			break;	
		default:
			break;
	}

}

3.5 定时器中断及main函数

定时器函数

初始化一个定时器。中断时间为20ms。
这里使用的时tim3。
定时器中断里面,直接调用KEY_ReadStateMachine()函数即可,将读取到的事件保存到KeyCfg.KEY_Event变量。

extern KEY_Configure_TypeDef KeyCfg;
//定时器3中断服务程序
void TIM3_IRQHandler(void)   //TIM3中断
{
    
    

	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
	{
    
    
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 
            KEY_ReadStateMachine();  //调用状态机
			
			if(KeyCfg.KEY_Event == KEY_Event_SingleClick)
			{
    
    
				printf("单击\r\n");//事件处理
			}
			if(KeyCfg.KEY_Event == KEY_Event_LongPress)
			{
    
    
				printf("长按\r\n");//事件处理
			}
		}
}
main函数

这里main函数十分简洁。初始化即可。
实际应用时,事件处理的代码,建议不要放在定时器里面。

int main(void)
{
    
    	

	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	uart_init(115200); // 用于查看输出
	TIM3_Int_Init(200-1,7200-1); //调用定时器使得20ms产生一个中断
	//按键初始化函数
	KEY_Init();
	
	while(1);

}     

4.实验验证

将程序下载到单片机,并连接好串口。
在这里插入图片描述
快速点击7次按键。产生3次双击,一次单击。
在这里插入图片描述
单击一次后,间隔600ms后又单击一次,再快速双击。
在这里插入图片描述

单击一次后,500ms内长按2s后释放。
在这里插入图片描述

5.总结

打印信息与实际相符。
故,代码是可行的。

后记:
KEY_ReadStateMachine里面的printf用于打印查看。为了读起来轻松,多写了一些没必要的赋值操作。实际应用时可以注释掉,以提高效率。

双击实现了,三击、四击就不难了。。。有兴趣可以试试。
人无完人,代码一定是有不足的,欢迎交流,咱们共同维护。

感谢大家的阅读,码字分享不易。
如果有帮助的,请不要吝啬三连。点赞评论收藏,让更多人看到有用的内容。

猜你喜欢

转载自blog.csdn.net/qq_44078824/article/details/123757354