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

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

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

二、按键的状态机

1. 按键的状态

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

2. 按键的动作

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

3. 按键的状态转换图

当按键被按下时候,它的状态为按下,当按键松开时,它的状态为松开:
在这里插入图片描述

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

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

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

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

三、状态图与事件

如上所示,可综合绘制状态装换图与事件产生的条件:
在这里插入图片描述

四、程序设计

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

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()即可
在这里插入图片描述

七、改进

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

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

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

  • 17
    点赞
  • 114
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
STM32是一款高性能、低功耗的微控制器,可以通过编程实现单击、双击和按功能。在使用STM32进行轮询时,可以通过以下方式实现这些功能。 首先,我们需要了解STM32的GPIO输入模式和中断模式。GPIO输入模式可以读取引脚的电平变化,而中断模式可以在引脚状态发生变化时立即触发一段程序的执行。 为了实现单击功能,我们可以在轮询中不断读取引脚的电平变化,并使用一个计数器记录按下的持续时间。当电平变为低电平时,计数器开始计时;当电平变为高电平时,计数器停止计时。当计数器达到一定的阈值时,我们可以认为发生了单击事件。 双击功能的实现方式类似单击,不同之处在于我们需要维护两个计数器,一个用于检测第一次单击事件,另一个用于检测第二次单击事件。当检测到第一次单击事件后,我们可以启动第二个计数器,并在一定的时间内检测第二次单击事件是否发生。 按功能可以通过轮询方式判断按键状态的持续时间是否超过设定的阈值。如果按键状态持续时间超过阈值,则可以认为发生了事件。 总结起来,STM32可以通过读取引脚的状态变化来实现单击、双击和按功能。使用计数器来记录按键状态的持续时间,并与设定的阈值进行比较,从而判断是否发生了相应的事件。这种轮询方式可以有效地实现按键功能,并能满足实际应用需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值