STM32利用状态机实现按键消抖、单击、双击、长按
一、整体思路
算法思路整理:不断扫描引脚电平(松开为1,按下为0),每一次按键状态改变后都会记录该状态持续的时间,并且重新计时以确保下一次状态改变后的时间记录的真实性。当按键松开后延时一段时间确保按键状态不会再次改变后对上一次的操作进行判断,判断的依据就是每一次状态持续的时间
二、定时器配置
头文件
#ifndef User_Timer_H
#define User_Timer_H
/* Precondition */
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdint.h"
#ifdef __cplusplus
extern "C" {
#endif
#ifndef TARGET_GLOBAL
#define TARGET_EXT extern
#else
#define TARGET_EXT
#endif
/* Predefine */
#define Timer_Obj TIM2
#define Timer_Period 50000 //25s
#define Timer_Prescaler 36000-1 //0.5ms
#define Timer_CounterMode TIM_CounterMode_Up
#define Timer_ClockDivision 0
/* Enumeration Declaration */
TARGET_EXT enum
{
Timer_isBusy,
Timer_isFree
}Timer_state;
/* Function Declaration */
TARGET_EXT void User_Timer_functionConfigure();
TARGET_EXT void User_Timer_eventConfigure();
TARGET_EXT void User_Timer_pinConfigure();
TARGET_EXT void User_Timer_Initial();
#ifdef __cplusplus
}
#endif
#endif
源文件
#include "User_Timer.h"
void User_Timer_functionConfigure()
{
//Initialization structure define.
TIM_TimeBaseInitTypeDef Timer_timeBaseInit;
//Initialization structure assignment.
Timer_timeBaseInit.TIM_Period =Timer_Period;
Timer_timeBaseInit.TIM_Prescaler=Timer_Prescaler;
Timer_timeBaseInit.TIM_CounterMode=Timer_CounterMode;
Timer_timeBaseInit.TIM_ClockDivision=Timer_ClockDivision;
//Select source of clock.
TIM_InternalClockConfig(Timer_Obj);
//Configure the time base of Timer.
TIM_TimeBaseInit(Timer_Obj,&Timer_timeBaseInit);
//Enable or disable the timer.
TIM_Cmd(Timer_Obj,ENABLE);
}
void User_Timer_eventConfigure()
{
}
void User_Timer_pinConfigure()
{
}
void User_Timer_Initial()
{
TIM2->CNT=0;
Timer_state=Timer_isFree;
}
三、按键GPIO配置
头文件
#ifndef User_Timer_H
#define User_Timer_H
/* Precondition */
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdint.h"
#ifdef __cplusplus
extern "C" {
#endif
#ifndef TARGET_GLOBAL
#define TARGET_EXT extern
#else
#define TARGET_EXT
#endif
/* Predefine */
#define Timer_Obj TIM2
#define Timer_Period 50000 //25s
#define Timer_Prescaler 36000-1 //0.5ms
#define Timer_CounterMode TIM_CounterMode_Up
#define Timer_ClockDivision 0
/* Enumeration Declaration */
TARGET_EXT enum
{
Timer_isBusy,
Timer_isFree
}Timer_state;
/* Function Declaration */
TARGET_EXT void User_Timer_functionConfigure();
TARGET_EXT void User_Timer_eventConfigure();
TARGET_EXT void User_Timer_pinConfigure();
TARGET_EXT void User_Timer_Initial();
#ifdef __cplusplus
}
#endif
#endif
源文件
#include "KEY_GPIO.h"
void KEY_GPIO_functionConfigure()
{
//Initialize structure definition.
GPIO_InitTypeDef KEY_GPIO_Configure;
//Initialize structure assignment.
KEY_GPIO_Configure.GPIO_Pin =KEY_GPIO_Pin;
KEY_GPIO_Configure.GPIO_Mode =KEY_GPIO_Mode;
KEY_GPIO_Configure.GPIO_Speed =KEY_GPIO_Speed;
//Configure the GPIO of KEY.
GPIO_Init(KEY_Obj,&KEY_GPIO_Configure);
}
void KEY_GPIO_eventConfigure()
{
}
void KEY_GPIO_pinConfigure()
{
}
void KEY_GPIO_Initial()
{
GPIO_SetBits(KEY_Obj,KEY_GPIO_Pin);
KEY_SM.KEY_state=KEY_state_Free;
}
四、状态机实现
头文件
#ifndef KEY_H
#define KEY_H
/* Precondition */
#include "Configure_Device.h" // Device header
#ifdef __cplusplus
extern "C" {
#endif
#ifndef TARGET_GLOBAL
#define TARGET_EXT extern
#else
#define TARGET_EXT
#endif
/* Function Declaration */
TARGET_EXT void KEY_stateMachine();
#ifdef __cplusplus
}
#endif
#endif
源文件
#include "KEY.h"
/*
@name :KEY_stateMachine
@brief :Scan the state of the key using the method of the state machine.
@param :None.
@retval:None.
*/
void KEY_stateMachine()
{
static uint8_t click_isDouble=0;
uint16_t key_value;
uint16_t timer_getTime;
uint16_t High_stableTime=0;
key_value=GPIO_ReadInputDataBit(KEY_Obj,KEY_GPIO_Pin);
switch(KEY_SM.KEY_state)
{
case KEY_state_Free:
{
if(key_value==0)
{
KEY_SM.KEY_state=KEY_state_Unknow;
KEY_SM.KEY_getHighTime=TIM_GetCounter(Timer_Obj);
Timer_Obj->CNT=0;
}
High_stableTime=TIM_GetCounter(Timer_Obj);
break;
}
case KEY_state_Unknow:
{
timer_getTime=TIM_GetCounter(Timer_Obj);
if(timer_getTime>=15 && key_value==0)
{
KEY_SM.KEY_state=KEY_state_Pressed;
Timer_Obj->CNT=0;
}
if(timer_getTime>=15 && key_value==1)
{
KEY_SM.KEY_state=KEY_state_Free;
Timer_Obj->CNT=0;
}
break;
}
case KEY_state_Pressed:
{
if(key_value==1)
{
KEY_SM.KEY_state=KEY_state_Unknow;
KEY_SM.KEY_getLowTime=TIM_GetCounter(Timer_Obj);
Timer_Obj->CNT=0;
}
break;
}
default:
{
break;
}
}
if(KEY_SM.KEY_state==KEY_state_Free && High_stableTime>=200)
{
if(KEY_SM.KEY_getHighTime>=100 && KEY_SM.KEY_getHighTime<=200)
{
KEY_SM.KEY_clickMode=KEY_click_Double;
click_isDouble=1;
}
else
{
click_isDouble=0;
}
if(KEY_SM.KEY_getLowTime>1500 && click_isDouble==0)
{
KEY_SM.KEY_clickMode=KEY_click_Long;
}
if(KEY_SM.KEY_getLowTime<=1500 && click_isDouble==0)
{
KEY_SM.KEY_clickMode=KEY_click_One;
}
}
}
代码解释
/*
按键引脚松开状态为1,按下状态为0
*/
void KEY_stateMachine()
{
static uint8_t click_isDouble=0; //该变量用于对双击的判断,如果系统已经认为是双击就不会对单击和长按进行判断
uint16_t key_value; //该变量用于实时探测按键引脚当前状态
uint16_t timer_getTime; //该变量用于按键在未知状态时对定时器计数值的读取
uint16_t High_stableTime; /*该变量用于按键稳定松开后对松开时间的记录(其实就是利用定时器进行延时,下文会解释)*/
key_value=GPIO_ReadInputDataBit(KEY_Obj,KEY_GPIO_Pin); //实时更新按键引脚的状态
switch(KEY_SM.KEY_state)
{
case KEY_state_Free: //按键处于松开状态
{
if(key_value==0) //如果检测到按键引脚为0(此时不确定按键是否真的被按下)
{
KEY_SM.KEY_state=KEY_state_Unknow; //跳转到未知状态
KEY_SM.KEY_getHighTime=TIM_GetCounter(Timer_Obj); //获取高电平时间(也就是松开的时间)
Timer_Obj->CNT=0; //定时器清零,重新计时
}
High_stableTime=TIM_GetCounter(Timer_Obj); /*当按键松开且状态未变时读取计时器时间,后续用于延时*/
}
break;
}
case KEY_state_Unknow: //未知状态
{
timer_getTime=TIM_GetCounter(Timer_Obj); /*当按键在按下和松开的状态改变后都会重新计时,读取时间*/
if(timer_getTime>=15 && key_value==0) /*延时15ms消抖,再次判断引脚电平,若还是0那就确定了按键被按下*/
{
KEY_SM.KEY_state=KEY_state_Pressed; //跳转到按下状态
Timer_Obj->CNT=0;//重新计时
}
if(timer_getTime>=15 && key_value==1) //延时15ms消抖,若电平为1,返回松开状态
{
KEY_SM.KEY_state=KEY_state_Free;
Timer_Obj->CNT=0;//重新计时
}
break;
}
case KEY_state_Pressed: //松开状态
{
if(key_value==1) //若引脚电平变化
{
KEY_SM.KEY_state=KEY_state_Unknow; //跳转到未知状态
KEY_SM.KEY_getLowTime=TIM_GetCounter(Timer_Obj); //获取按下按键持续的时间
Timer_Obj->CNT=0;//重新计时
}
break;
}
default:
{
break;
}
}
/*
接下来进行单击、双击、长按的判断
*/
/*
KEY_SM.KEY_state==KEY_state_Free && timer_getTime1>=300:当按键松开后并且延时一定的时间后进行单机、双击、长按的判断。
为什么要延时?
答:如果不进行延时,就会发现在进行判断的时候无论是双击还是长按,在你按下按键的一瞬间都会被判定为单击然后才会被跳转到正确的状态。延时的目的就是按下按键后先等一会,确定你接下来不会再进行别的操作了CPU再进行判断。这个延时时间取决于设定的双击间隔(也就是第一次按下和第二次按下,两次按下间的时间间隔)
*/
if(KEY_SM.KEY_state==KEY_state_Free && High_stableTime>=300)
{
/*第一次按下和第二次按下间隔时间在100-300ms时判定为双击。经过调试发现这个数据是比较贴近人手双击速度的,低于100ms后手速基本达不到,高于300ms后需要延时的时间就会比较长,实时性太差*/
if(KEY_SM.KEY_getHighTime>=100 && KEY_SM.KEY_getHighTime<=300)
{
KEY_SM.KEY_clickMode=KEY_click_Double;
click_isDouble=1; //已经判定为双击
}
else
{
click_isDouble=0; //未被判定为双击
}
/*按下持续时间低于1500ms并且未被判定为双击的情况下判定为单击*/
if(KEY_SM.KEY_getLowTime>1500 && click_isDouble==0)
{
KEY_SM.KEY_clickMode=KEY_click_Long;
}
if(KEY_SM.KEY_getLowTime<=1500 && click_isDouble==0) //同上,否则为长按
{
KEY_SM.KEY_clickMode=KEY_click_One;
}
}
}
五、测试
头文件
#ifndef main_H
#define main_H
/* Precondition */
#include "Application_Device.h" //上面写的头文件都包含在这个里面了
/* Function Declaration */
void Initial_Operation(void);
void KEY_Test();
void Timer_Test();
#endif
源文件
随便点个灯测试一下
/* Predefine */
#define TARGET_GLOBAL
/* Precondition */
#include "main.h"
int main()
{
Initial_Operation();
while(1)
{
//KEY_Test();
//Timer_Test();
KEY_stateMachine();
switch(KEY_SM.KEY_clickMode)
{
case KEY_click_One:
{
LED_on(LED1_GPIO_Pin);
LED_off(LED2_GPIO_Pin);
break;
}
case KEY_click_Double:
{
LED_off(LED1_GPIO_Pin);
LED_on(LED2_GPIO_Pin);
break;
}
case KEY_click_Long:
{
LED_on(LED1_GPIO_Pin);
LED_on(LED2_GPIO_Pin);
}
default:break;
}
}
}
/*
@brief:Perform initialization operation.
*/
void Initial_Operation(void)
{
//User System.
User_System_functionConfigure();
User_System_eventConfigure();
User_System_pinConfigure();
User_System_Initial();
//LED GPIO.
LED_GPIO_functionConfigure();
LED_GPIO_eventConfigure();
LED_GPIO_pinConfigure();
LED_GPIO_Initial();
//KEY GPIO.
KEY_GPIO_functionConfigure();
KEY_GPIO_eventConfigure();
KEY_GPIO_pinConfigure();
KEY_GPIO_Initial();
//User Timer
User_Timer_functionConfigure();
User_Timer_eventConfigure();
User_Timer_pinConfigure();
User_Timer_Initial();
}
六、日志
2024-9-8 23:25
方案:
1.按键扫描
原方案:在主函数里对按键状态进行扫描,当发现按键按下后再利用状态机的方式进行按键消抖。但后来发现如果扫描速度过快,在按键状态稳定时当前状态会被重复读取(特指按键被按下后那一段稳定时间,约为80ms)。
改进方案:利用开关语句对按键扫描的速度进行限制。但经过调试这个方案被废除,因为开关语句的判断数值的选定较难,并且随着后续代码的增加,这个数值也需要不断地修改。
改进方案:原方案为了节省单片机的外设资源没有使用外部中断,但在实际的开发中很难较精确捕获到按键状态的改变,所以兜兜转转又回到了外部中断上,本次方案决定使用外部中断捕获按键状态的改变,并在外部中断里打开状态机的开关。在主函数里对状态机的开关状态不断扫描,如果发现状态机开启则跳转到状态机实现函数对按键进行消抖获得按键最终的状态
2024-9-9 22:03
昨天思路完全错误,全部推倒重来
2024-9-10 21:22
经历九九八十一难,终于得到了在只用一个定时器的前提下利用状态机实现按键的消抖、单击、双击、长按的较优解。下面对算法思路进行整理
代码如下:
/*
按键引脚松开状态为1,按下状态为0
*/
void KEY_stateMachine()
{
static uint8_t click_isDouble=0; //该变量用于对双击的判断,如果系统已经认为是双击就不会对单击和长按进行判断
uint16_t key_value; //该变量用于实时探测按键引脚当前状态
uint16_t timer_getTime; //该变量用于按键在未知状态时对定时器计数值的读取
uint16_t timer_getTime1; //该变量用于按键稳定松开后对松开时间的记录(其实就是利用定时器进行延时,下文会解释)
key_value=GPIO_ReadInputDataBit(KEY_Obj,KEY_GPIO_Pin); //实时更新按键引脚的状态
w=TIM_GetCounter(Timer_Obj);
/*
上面这个W的存在是一个玄学问题,这是一个全局变量,在代码初期用来观测定时器是否正常工作的,在后来的代码里没有 用到这个w。但在整个代码写完后玄学问题出现了,如果我把这个w注释掉,上面那个用来延时的变量就不起作用了,目前还 没有搞清原因在哪里。
*/
switch(KEY_SM.KEY_state)
{
case KEY_state_Free: //按键处于松开状态
{
if(key_value==0) //如果检测到按键引脚为0(此时不确定按键是否真的被按下)
{
KEY_SM.KEY_state=KEY_state_Unknow; //跳转到未知状态
KEY_SM.KEY_getHighTime=TIM_GetCounter(Timer_Obj); //获取高电平时间(也就是松开的时间)
Timer_Obj->CNT=0; //定时器清零,重新计时
}
else
{
timer_getTime1=TIM_GetCounter(Timer_Obj); //当按键松开且状态未变时读取计时器时间,后续用于延时
}
break;
}
case KEY_state_Unknow: //未知状态
{
timer_getTime=TIM_GetCounter(Timer_Obj); //当按键在按下和松开的状态改变后都会重新计时,读取时间
if(timer_getTime>=15 && key_value==0) //延时15ms消抖,再次判断引脚电平,若还是0那就确定了按键被按下
{
KEY_SM.KEY_state=KEY_state_Pressed; //跳转到按下状态
Timer_Obj->CNT=0;//重新计时
}
if(timer_getTime>=15 && key_value==1) //延时15ms消抖,若电平为1,返回松开状态
{
KEY_SM.KEY_state=KEY_state_Free;
Timer_Obj->CNT=0;//重新计时
}
break;
}
case KEY_state_Pressed: //松开状态
{
if(key_value==1) //若引脚电平变化
{
KEY_SM.KEY_state=KEY_state_Unknow; //跳转到未知状态
KEY_SM.KEY_getLowTime=TIM_GetCounter(Timer_Obj); //获取按下按键持续的时间
Timer_Obj->CNT=0;//重新计时
}
break;
}
default:
{
break;
}
}
/*
接下来进行单击、双击、长按的判断
*/
/*
KEY_SM.KEY_state==KEY_state_Free && timer_getTime1>=300:当按键松开后并且延时一定的时间后进行单机、双击、长按的判断。
为什么要延时?
答:如果不进行延时,就会发现在进行判断的时候无论是双击还是长按,在你按下按键的一瞬间都会被判定为单击然后才会被跳转到正确的状态。延时的目的就是按下按键后先等一会,确定你接下来不会再进行别的操作了CPU再进行判断。这个延时时间取决于设定的双击间隔(也就是第一次按下和第二次按下,两次按下间的时间间隔)
*/
if(KEY_SM.KEY_state==KEY_state_Free && timer_getTime1>=300)
{
if(KEY_SM.KEY_getHighTime>=100 && KEY_SM.KEY_getHighTime<=300) //第一次按下和第二次按下间隔时间在100-300ms时判定为双击。经过调试发现这个数据是比较贴近人手双击速度的,低于100ms后手速基本达不到,高于300ms后需要延时的时间就会比较长,实时性太差
{
KEY_SM.KEY_clickMode=KEY_click_Double;
click_isDouble=1; //已经判定为双击
}
else
{
click_isDouble=0; //未被判定为双击
}
if(KEY_SM.KEY_getLowTime>1500 && click_isDouble==0) //按下持续时间低于1500ms并且未被判定为双击的情况下判定为单击
{
KEY_SM.KEY_clickMode=KEY_click_Long;
}
if(KEY_SM.KEY_getLowTime<=1500 && click_isDouble==0) //同上,否则为长按
{
KEY_SM.KEY_clickMode=KEY_click_One;
}
}
}
优化后的代码:还是没找到那个玄学问题的原因在哪,不过当我把else去掉后(也就是把时间记录条件放宽,只要按键松开我就记录时间)
void KEY_stateMachine()
{
static uint8_t click_isDouble=0;
uint16_t key_value;
uint16_t timer_getTime;
uint16_t High_stableTime=0; //改了变量名增加一下代码的可读性
key_value=GPIO_ReadInputDataBit(KEY_Obj,KEY_GPIO_Pin);
switch(KEY_SM.KEY_state)
{
case KEY_state_Free:
{
if(key_value==0)
{
KEY_SM.KEY_state=KEY_state_Unknow;
KEY_SM.KEY_getHighTime=TIM_GetCounter(Timer_Obj);
Timer_Obj->CNT=0; //这里把else删了
}
High_stableTime=TIM_GetCounter(Timer_Obj);
break;
}
case KEY_state_Unknow:
{
timer_getTime=TIM_GetCounter(Timer_Obj);
if(timer_getTime>=15 && key_value==0)
{
KEY_SM.KEY_state=KEY_state_Pressed;
Timer_Obj->CNT=0;
}
if(timer_getTime>=15 && key_value==1)
{
KEY_SM.KEY_state=KEY_state_Free;
Timer_Obj->CNT=0;
}
break;
}
case KEY_state_Pressed:
{
if(key_value==1)
{
KEY_SM.KEY_state=KEY_state_Unknow;
KEY_SM.KEY_getLowTime=TIM_GetCounter(Timer_Obj);
Timer_Obj->CNT=0;
}
break;
}
default:
{
break;
}
}
if(KEY_SM.KEY_state==KEY_state_Free && High_stableTime>=200)
{
if(KEY_SM.KEY_getHighTime>=100 && KEY_SM.KEY_getHighTime<=200)
{
KEY_SM.KEY_clickMode=KEY_click_Double;
click_isDouble=1;
}
else
{
click_isDouble=0;
}
if(KEY_SM.KEY_getLowTime>1500 && click_isDouble==0)
{
KEY_SM.KEY_clickMode=KEY_click_Long;
}
if(KEY_SM.KEY_getLowTime<=1500 && click_isDouble==0)
{
KEY_SM.KEY_clickMode=KEY_click_One;
}
}
}
算法思路整理:不断扫描引脚电平(松开为1,按下为0),每一次按键状态改变后都会记录该状态持续的时间,并且重新计时以确保下一次状态改变后的时间记录的真实性。当按键松开后延时一段时间确保按键状态不会再次改变后对上一次的操作进行判断,判断的依据就是每一次状态持续的时间
七、优缺点分析
优点
整个过程只用了一个定时器,节省单片机外设资源。没有使用延时函数,CPU利用率高,不影响CPU实时性
缺点
按键检测的实时性差
八、测试视频
利用状态机实现按键消抖、单击、双击、长按
九、源码链接
我用夸克网盘分享了「State_Machine」,点击链接即可保存。打开「夸克APP」,无需下载在线播放视频,畅享原画5倍速,支持电视投屏。
链接:https://pan.quark.cn/s/f4efe48818d3
提取码:aHKQ
作者:CmeHY