单片机状态机与定时器实现按键事件检测:短按,长按,单击,长单击(轮询方式)

文章目录

  • 一、有限状态机(FSM)简介
  • 二、按键的状态机
    • 1. 按键的状态
    • 2. 按键的动作
    • 3. 按键的状态转换图
    • 2. 按键事件与定时器应用
  • 三、状态图与事件
  • 四、程序设计
    • 1. 枚举变量:按键状态与事件
    • 2. 定义状态
    • 3. 事件的检测
    • 4. 事件处理(禁止/启用重复触发)
  • 五、程序源码
  • 六、测试
  • 七、改进

一、有限状态机(FSM)简介

有限状态机FSM(Finite State Machine),通常指任意一个时刻在一种状态之中,不同状态的转移是通过动作来触发的,不同状态下,不同动作将触发不同的状态转移,当然也可以不发生转移。

二、按键的状态机

1. 按键的状态

常用的按键只有2个状态,按下(press)和抬起(release)

2. 按键的动作

常用的按键只有2个动作:按下抬起

3. 按键的状态转换图

当按键被按下时候,它的状态为按下,当按键松开时,它的状态为松开:
单片机状态机与定时器实现按键事件检测:短按,长按,单击,长单击(轮询方式)_第1张图片

2. 按键事件与定时器应用

通常按键事件有短按和长按,本文定义了五种按键事件:

事件(action) 描述
无事件 按键没有动作发生
按下 按键被按下(按下时间<1s)
短按(单击) 按键被按下又抬起(按下时间<1s)
长按(长单击) 按键被按下又抬起(按下时间>1s)
长按中 按键被按下(按下时间>1s)

可以发现,按键只有按下抬起两种动作和状态,但是可以产生五种事件,甚至可以更多,这些事件是根据按下的时长来区分的。所以,本文需要一个定时器,在按键第一次被按下时,记录时间,通过不断与后续的动作(按下或抬起)比较,即可区分各种状态

三、状态图与事件

如上所示,可综合绘制状态装换图与事件产生的条件:
单片机状态机与定时器实现按键事件检测:短按,长按,单击,长单击(轮询方式)_第2张图片

四、程序设计

为简化程序,本文使用电容触摸模块,固不讨论按键抖动,程序也无滤波部分。另外,当电容被触摸时候,输出高电平,当电容未被触摸时,输出低电平。

1. 枚举变量:按键状态与事件

//按键状态
enum btn_sta{
    RELEASE = 0,
    PRESSED = 1
};

//按键事件
enum {
    ACT_NO        = 0,
    ACT_PRESS,
    ACT_SHORT_CLICKED,
    ACT_LONG_CLICKED,
    ACT_LONG_PRESSING,
}_btn_evt;

首先将按键状态与按键事件通过枚举变量的形式表达,提高程序的可读性。

2. 定义状态

static enum btn_sta _cur_sta  = RELEASE,
                    _last_sta = RELEASE;

在状态机中,状态的转移需要当前状态,所以定义_cur_sta来存储,同时,事件的产生需要和上一状态对比,所以需要_last_sta来存储,在程序中,当一次读取按键结束后,当前的状态值,就成为了上次状态的值。

3. 事件的检测

关键变量定义完成以后,就需要具体的程序逻辑实现,通常状态机可以用swich caseif语句来实现:

    _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;                                       //本次状态更新为上一次状态,
                                                                //为下次扫描做准备

4. 事件处理(禁止/启用重复触发)

当获取一次事件以后,需要对各个事件进行处理,首先需要定一个各个处理函数:

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()即可
单片机状态机与定时器实现按键事件检测:短按,长按,单击,长单击(轮询方式)_第3张图片

七、改进

这个程序最明显的问题是移植不够简便:

  1. 它使用了系统函数HAL_GetTick(),对于不同的工程,获取计数的函数通常是不同的,其值的单位并非1ms。
  2. 用户需要在button.c 中添加自己的函数处理
  3. 长按间隔时间未使用宏定义,若要修改间隔时间,需要每处都进行修改

对于第1,第2个问题,通常可以使用函数指针,或者说回调函数来解决,其思路是设计一个指针,指向一个函数,这个指针在按键程序模块初始化的时候被传入,此时用户只需要将函数指针传入即可,无需改动此文件。这边降低了程序的耦合性。

你可能感兴趣的:(#,STM32,应用与功能实现)