有限状态机FSM(Finite State Machine),通常指任意一个时刻在一种状态之中,不同状态的转移是通过动作来触发的,不同状态下,不同动作将触发不同的状态转移,当然也可以不发生转移。
常用的按键只有2个状态,按下
(press)和抬起
(release)
常用的按键只有2个动作:按下
和抬起
当按键被按下时候,它的状态为按下,当按键松开时,它的状态为松开:
通常按键事件有短按和长按,本文定义了五种按键事件:
事件(action) | 描述 |
---|---|
无事件 | 按键没有动作发生 |
按下 | 按键被按下(按下时间<1s) |
短按(单击) | 按键被按下又抬起(按下时间<1s) |
长按(长单击) | 按键被按下又抬起(按下时间>1s) |
长按中 | 按键被按下(按下时间>1s) |
可以发现,按键只有按下抬起两种动作和状态,但是可以产生五种事件,甚至可以更多,这些事件是根据按下的时长来区分的。所以,本文需要一个定时器,在按键第一次被按下时,记录时间,通过不断与后续的动作(按下或抬起)比较,即可区分各种状态
为简化程序,本文使用电容触摸模块,固不讨论按键抖动,程序也无滤波部分。另外,当电容被触摸时候,输出高电平,当电容未被触摸时,输出低电平。
//按键状态
enum btn_sta{
RELEASE = 0,
PRESSED = 1
};
//按键事件
enum {
ACT_NO = 0,
ACT_PRESS,
ACT_SHORT_CLICKED,
ACT_LONG_CLICKED,
ACT_LONG_PRESSING,
}_btn_evt;
首先将按键状态与按键事件通过枚举变量的形式表达,提高程序的可读性。
static enum btn_sta _cur_sta = RELEASE,
_last_sta = RELEASE;
在状态机中,状态的转移需要当前状态,所以定义_cur_sta
来存储,同时,事件的产生需要和上一状态对比,所以需要_last_sta
来存储,在程序中,当一次读取按键结束后,当前的状态值,就成为了上次状态的值。
关键变量定义完成以后,就需要具体的程序逻辑实现,通常状态机可以用swich case
和if
语句来实现:
_cur_sta = (enum btn_sta)GET_BTN_STA(); //获取当前按键的动作,读取IO的值
if(_last_sta == RELEASE) //上次状态释放
{
switch (_cur_sta)
{
case RELEASE: //当前状态为释放
_btn_evt = ACT_NO; //一直释放状态:无事件
break;
case PRESSED: //当前状态为按下
_btn_evt = ACT_PRESS; //释放到按下:按下动作
time_last = HAL_GetTick(); //记录时刻
break;
}
}
else if(_last_sta == PRESSED) //上次状态为抬起
{
switch (_cur_sta)
{
case RELEASE: //当前状态为释放
if(HAL_GetTick() - time_last > 1000)
{
_btn_evt = ACT_LONG_CLICKED; //间隔<1s,短按(单击)
}
else
{
_btn_evt = ACT_SHORT_CLICKED; //间隔>1s,长按(长单击)
}
break;
case PRESSED: //当前事件为按下
if(HAL_GetTick() - time_last > 1000)
{
_btn_evt = ACT_LONG_PRESSING; //事件间隔>1s,长按中
}
break;
}
}
_last_sta = _cur_sta; //本次状态更新为上一次状态,
//为下次扫描做准备
当获取一次事件以后,需要对各个事件进行处理,首先需要定一个各个处理函数:
void btn_evt_proc_short_click(void)
{
printf("%s\r\n",__FUNCTION__);
}
void btn_evt_proc_long_click(void)
{
printf("%s\r\n",__FUNCTION__);
}
void btn_evt_proc_long_pressing(void)
{
printf("%s\r\n",__FUNCTION__);
}
void btn_evt_proc_press(void)
{
printf("%s\r\n",__FUNCTION__);
}
接着使用switch
语句可以很简单的处理各个事件:
switch(_btn_evt)
{
case ACT_NO:
break;
case ACT_SHORT_CLICKED:
btn_evt_proc_short_click();
break;
case ACT_LONG_CLICKED:
btn_evt_proc_long_click();
break;
case ACT_PRESS:
btn_evt_proc_press();
break;
case ACT_LONG_PRESSING:
btn_evt_proc_long_pressing();
break;
}
return;
由于程序会不断的扫描按键,考虑一种情况:当用户按下按键时候,程序就会不断的检测到按下事件,用户若长时间不松开,程序还将一直检测到ACT_LONG_PRESSING(长按中)
事件,那么程序将不断的调用btn_evt_proc_long_pressing();
,若你需要在这个过程中,只调用一次函数,那么可以设计程序,禁止重复触发,注意到以下程序片段需要添加到按键事件处理之前:
#define BAN_DECT_REPET 1
#if BAN_DECT_REPET
uint8_t static last_evt = 0;
if(last_evt == _btn_evt)
{
last_evt = _btn_evt; //若本次事件和上次相同
return; //程序返回
}
last_evt = _btn_evt;
#endif
可以看到,当 BAN_DECT_REPET
为1时候,程序将会被程序,若2次事件移植,程序将返回(提前返回),此时,程序处理函数将不会被触发。
程序完整源码(File:button.c):
/******************************************************************************************
* @File: button.c
* @Data:2020年7月2日
* @by :YonasLuo
* @ver :1.0
******************************************************************************************/
#include "button.h"
#include "stdio.h"
#include "main.h"
#include "gpio.h"
/******************************************************************************************
* @buttonCode
******************************************************************************************/
#define GET_BTN_STA() (HAL_GPIO_ReadPin(btn_GPIO_Port, btn_Pin))
#define BAN_DECT_REPET (1) //1:repet dectect 0: ban repet dectect
/******************************************************************************************
* @API
******************************************************************************************/
void btn_evt_proc_short_click(void)
{
printf("%s\r\n",__FUNCTION__);
}
void btn_evt_proc_long_click(void)
{
printf("%s\r\n",__FUNCTION__);
}
void btn_evt_proc_long_pressing(void)
{
printf("%s\r\n",__FUNCTION__);
}
void btn_evt_proc_press(void)
{
printf("%s\r\n",__FUNCTION__);
}
/******************************************************************************************
* @buttonCode
******************************************************************************************/
enum btn_sta{
RELEASE = 0,
PRESSED = 1
};
enum {
ACT_NO = 0,
ACT_PRESS,
ACT_SHORT_CLICKED,
ACT_LONG_CLICKED,
ACT_LONG_PRESSING,
}_btn_evt;
static enum btn_sta _cur_sta = RELEASE,
_last_sta = RELEASE;
static uint16_t time_last = 0;
void btn_proc_poll(void)
{
_cur_sta = (enum btn_sta)GET_BTN_STA();
if(_last_sta == RELEASE)
{
switch (_cur_sta)
{
case RELEASE:
_btn_evt = ACT_NO;
break;
case PRESSED:
_btn_evt = ACT_PRESS;
time_last = HAL_GetTick();
break;
}
}
else if(_last_sta == PRESSED)
{
switch (_cur_sta)
{
case RELEASE:
if(HAL_GetTick() - time_last > 1000)
{
_btn_evt = ACT_LONG_CLICKED;
}
else
{
_btn_evt = ACT_SHORT_CLICKED;
}
break;
case PRESSED:
if(HAL_GetTick() - time_last > 1000)
{
_btn_evt = ACT_LONG_PRESSING;
}
break;
}
}
_last_sta = _cur_sta;
#if BAN_DECT_REPET
uint8_t static last_evt = 0;
if(last_evt == _btn_evt)
{
last_evt = _btn_evt;
return;
}
last_evt = _btn_evt;
#endif
switch(_btn_evt)
{
case ACT_NO:
break;
case ACT_SHORT_CLICKED:
btn_evt_proc_short_click();
break;
case ACT_LONG_CLICKED:
btn_evt_proc_long_click();
break;
case ACT_PRESS:
btn_evt_proc_press();
break;
case ACT_LONG_PRESSING:
btn_evt_proc_long_pressing();
break;
}
return;
}
/***************************** END OF FILE *****************************/
...
void main(void)
{
....
extern void btn_proc_poll(void );
while (1)
{
btn_proc_poll();
}
....
}
测试函数时,在main()
中不断调用处理函数btn_proc_poll()
即可
这个程序最明显的问题是移植不够简便:
HAL_GetTick()
,对于不同的工程,获取计数的函数通常是不同的,其值的单位并非1ms。button.c
中添加自己的函数处理对于第1,第2个问题,通常可以使用函数指针,或者说回调函数来解决,其思路是设计一个指针,指向一个函数,这个指针在按键程序模块初始化的时候被传入,此时用户只需要将函数指针传入即可,无需改动此文件。这边降低了程序的耦合性。