【stm32单片机基础】按键状态机实现长按和短按

【stm32单片机基础】按键状态机



前言

在单片机的教学例程中,常使用delay延迟的方式消除按键抖动,而delay延迟的方式使CPU处于空等的状态,不能进行其他任务,直到结束delay延时函数,这种阻塞的方式不利于多任务的情形。本文将使用非阻塞的方式消抖,并采用状态机的模式编写按键处理函数。


一、按键的消抖

按键消抖:通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,因而在闭合及断开的瞬间均伴随有一连串的抖动,按键抖动会引起一次按键被误读多次。

在这里插入图片描述
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms

软件消抖:硬件方法将导致系统硬件电路设计复杂化,常采用软件方法进行消抖。

软件方法去抖,即检测出键闭合后执行一个延时程序,5ms~10ms的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。

二、按键状态机实现

0.状态机模式

简单理解为:将一个事件划分为有限个状态,满足相应的条件,在有限个状态之间跳转;可以使用状态图来描述事件处理过程,这种方式使得程序逻辑思路更加清晰严谨。以按键为例,按键检测的过程可以分为三个状态:按键检测状态、按键确认状态、按键释放状态;而在这三个状态之间跳转的条件为当前状态下按键的值。
在单片机中实现状态机最常用的语句便是switch case语句。

【状态机中如何非阻塞消抖】:使用定时器中断,定时每10ms执行一次switch case语句,即两个状态之间跳转的时间为10ms,这样便代替了delay延时。当定时中断发生时,才跳转到中断服务函数执行。

1.单个按键检测

独立按键电路
按键
单个按键的状态转移图如下:
S1状态为按键检测,S2为按键确认,S3为释放按键;状态跳转条件为当前状态下读取到的按键高低电平Key,当Result为1时,表示按键已经成功按下。
在这里插入图片描述

单个按键检测的代码实现:

#ifdef SingleKeyEvent 

typedef enum
{
    KEY_CHECK = 0,
    KEY_COMFIRM = 1,
    KEY_RELEASE = 2 
}KEY_STATE;

KEY_STATE KeyState =KEY_CHECK;  // 初始化按键状态为检测状态
u8 g_KeyFlag = 0;                 // 按键有效标志,0: 按键值无效; 1:按键值有效
/**
 * 单个按键检测事件
 * 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
 * 状态机使用switch case语句实现状态之间的跳转
 * 
 */
void Key_Scan(void)
{
    switch (KeyState)
    {
        //按键未按下状态,此时判断Key的值
        case   KEY_CHECK:    
            if(!Key)   
            {
                KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态
            }
            break;
         //按键按下状态:
        case   KEY_COMFIRM:
            if(!Key)                     //查看当前Key是否还是0,再次确认是否按下
            {
                KeyState =  KEY_RELEASE;  //进入下一个释放状态
                g_KeyFlag = 1;           //按键有效值为1, 按键确认按下,按下就执行按键任务;        
            }   
            else                         //当前Key值为1,确认为抖动,则返回上一个状态
            {
                KeyState =  KEY_CHECK;      //返回上一个状态
            } 
            break;
         //按键释放状态
         case  KEY_RELEASE:
             if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态
             { 
                 KeyState =  KEY_CHECK;
               //  g_KeyFlag = 1;        //如果置于此,则在按键释放状态后,才执行按键任务;
             } 
             break;
         default: break;
    }
}

#endif

2.单个按键实现长按和短按

单个按键实现短按和长按是个很常用的方式,区别单个按键是否是长按和短按依靠检测按键按下的持续时间。

此处将短按时间T设为 10ms < T <1 s;长按时间的T设置为:T > 1s.

在上面的按键实现基础上可继续实现长按和短按判断,具体程序如下:

代码如下(示例):

#ifdef SingleKey_LongShort_Event 
/**
 * 单个按键检测短按和长按事件
 * 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s
 * 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
 * 状态机使用switch case语句实现状态之间的跳转
 * lock变量用于判断是否是第一次进行按键确认状态
 * :按键释放后才执行按键事件
 */
void Key_Scan(void)
{
    static u8 TimeCnt = 0;
    static u8 lock = 0;
    switch (KeyState)
    {
        //按键未按下状态,此时判断Key的值
        case   KEY_CHECK:    
           if(!Key)   
            {
                KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态 
            }
            TimeCnt = 0;                  //计数复位
            lock = 0;
            break;
            
        case   KEY_COMFIRM:
            if(!Key)                     //查看当前Key是否还是0,再次确认是否按下
            {
                if(!lock)   lock = 1;
               
                TimeCnt++;   
                               
            }   
            else                       
            {
                if(lock)                // 不是第一次进入,  释放按键才执行
                {
                    /*按键时长判断*/
                    if(TimeCnt > 100)            // 长按 1 s
                    {
                        g_KeyActionFlag = LONG_KEY;
                        TimeCnt = 0;  
                    }
                    else                         // Key值变为了1,说明此处动作为短按
                    {
                        g_KeyActionFlag = SHORT_KEY;          // 短按
                    }
                    /*按键时长判断*/
                    
                    KeyState =  KEY_RELEASE;    // 需要进入按键释放状态 
                }
                
                else                          // 当前Key值为1,确认为抖动,则返回上一个状态
                {
                    KeyState =  KEY_CHECK;    // 返回上一个状态
                }
               
            } 
            break;
            
         case  KEY_RELEASE:
             if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态
             { 
                 KeyState =  KEY_CHECK;    
             }
             break;
             
         default: break;
    }    
}
#endif


按键释放之后,才检测不太合理,做如下调整,长按事件需要达到时长就执行,短按可以在按键释放后执行。

/**
 * 单个按键检测短按和长按事件
 * 短按:时间 10ms < T < 1 s, 长按:时间 T >1 s
 * 功能:使用状态机方式,扫描单个按键;扫描周期为10ms,10ms刚好跳过抖动;
 * 状态机使用switch case语句实现状态之间的跳转
 * lock变量用于判断是否是第一次进行按键确认状态
 * :长按键事件提前执行,短按键事件释放后才执行
 */
void Key_Scan(void)
{
    static u8 TimeCnt = 0;
    static u8 lock = 0;
    switch (KeyState)
    {
        //按键未按下状态,此时判断Key的值
        case   KEY_CHECK:    
           if(!Key)   
            {
                KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态 
            }
            TimeCnt = 0;                  //计数复位
            lock = 0;
            break;
            
        case   KEY_COMFIRM:
            if(!Key)                     //查看当前Key是否还是0,再次确认是否按下
            {
                if(!lock)   lock = 1;
               
                TimeCnt++;  
                
                /*按键时长判断*/
                if(TimeCnt > 100)            // 长按 1 s
                {
                    g_KeyActionFlag = LONG_KEY;
                    TimeCnt = 0;  
                    lock = 2;               //防止退出长按时,又执行一次短按
                }                
                               
            }   
            else                       
            {
                if(1==lock)                // 不是第一次进入,  释放按键才执行
                {

                    g_KeyActionFlag = SHORT_KEY;          // 短按
                    KeyState =  KEY_RELEASE;    // 需要进入按键释放状态 
                }
                
                else                          // 当前Key值为1,确认为抖动,则返回上一个状态
                {
                    KeyState =  KEY_CHECK;    // 返回上一个状态
                }
       
            } 
            break;
            
         case  KEY_RELEASE:
             if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态
             { 
                 KeyState =  KEY_CHECK;    
             }
             break;
             
         default: break;
    }    
}

三、长按和短按测试示例

头文件

/**
  * 使用定时器来轮询Key_Scan()函数,定时节拍为2ms,
  * 状态转换时间为10ms,即每次进入switch case语句的时间差为10ms
  * 利用该10ms的间隙跳过按键抖动
  */
#ifndef __BUTTON_H
#define __BUTTON_H
#include "stm32f10x.h"

//按键对应的IO管脚 KEY1  PA.15
#define KEY_IO_RCC        RCC_APB2Periph_GPIOA      
#define KEY_IO_PORT       GPIOA
#define KEY_IO_PIN        GPIO_Pin_15

//Key: 1:高电平,按键未按下, 0:低电平,按键按下
#define Key  GPIO_ReadInputDataBit(KEY_IO_PORT,KEY_IO_PIN)

typedef enum
{
    KEY_CHECK = 0,
    KEY_COMFIRM = 1,
    KEY_RELEASE = 2
}KEY_STATE;

typedef enum 
{
    NULL_KEY = 0,
    SHORT_KEY =1,
    LONG_KEY
}KEY_TYPE;

//extern u8 g_KeyFlag;
//extern KEY_TYPE g_KeyActionFlag; 

//单个按键事件
//#define SingleKeyEvent

//单个按键实现长按和短按
#define SingleKey_LongShort_Event	1
void Key_Init(void);
void Key_Scan(void);

main函数

KEY_STATE KeyState =KEY_CHECK;  // 初始化按键状态为检测状态
u8 g_KeyFlag = 0;                // 按键有效标志,0: 按键值无效; 1:按键值有效

KEY_TYPE g_KeyActionFlag;		//用于区别长按和短按

int main()
{
    Key_Init();
    Timer_init(19,7199);//10Khz的计数频率,计数到20为2ms
    while(1)
    {
         switch(g_KeyActionFlag)
        {
            case SHORT_KEY:
                /*
                执行短按对应的事件
                */
            g_KeyActionFlag = 0;		//状态回到空
            break;
            
            case LONG_KEY:
                 /*
                执行长按对应的事件
                */
            g_KeyActionFlag = 0;		//状态回到空
            default: break;
        }
    }
}


void TIM3_IRQHandler(void)   //TIM3 每 2ms 中断一次
{   
    static u8 cnt;
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源 
		{
		cnt++;
        if(cnt>5)           // 每10ms 执行一次按键扫描程序
        {
            Key_Scan();
            cnt = 0;
        }
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);  //清除TIMx的中断待处理位:TIM 中断源 
		}   
}

四 、多按键检测

同样的,多按键也是一样的;
示例如下:
Buttion.h 头文件中只需要把Key做下修改;


#define KEY0  GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)//读取按键0
#define KEY1  GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)//读取按键1
#define WK_UP   GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键2 
#define Key  (KEY0 && KEY1 && (!WK_UP))

typedef enum
{
    KEY_CHECK = 0,
    KEY_COMFIRM = 1,
    KEY_RELEASE = 2,
}KEY_STATE;

//对应的按键值,
typedef enum
{
    KEY_NULL = 0,
    KEY_0,
    KEY_1,
    KEY_WK_UP
}KEY_VALUE;

对应的状态机中对按键值进行区分即可:

void Key_Scan(void)
{
    switch (KeyState)
    {
        //按键未按下状态,此时判断Key的值
        case   KEY_CHECK:    
            if(!Key)   
            {
                KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态
            }
            break;
         //按键按下状态:
        case   KEY_COMFIRM:
            if(!Key)                     //查看当前Key是否还是0,再次确认是否按下
            {
                KeyState =  KEY_RELEASE;  //进入下一个释放状态
                //g_KeyFlag = 1;           //按键有效值为1, 按键确认按下,按下就执行按键任务; 
                
                // 多按键判断
                if(0 == KEY0)
                    g_Key = KEY_0;
                else if(0 == KEY1)
                    g_Key = KEY_1;
                else if(1 == WK_UP)
                    g_Key = KEY_WK_UP;
            }   
            else                         //当前Key值为1,确认为抖动,则返回上一个状态
            {
                KeyState =  KEY_CHECK;      //返回上一个状态
            } 
            break;
         //按键释放状态
         case  KEY_RELEASE:
             if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态
             { 
                 KeyState =  KEY_CHECK;
             } 
             break;
         default: break;
    }
}

main函数中也是一样,使用switch case 语句执行按键事件即可:

	extern KEY_VALUE g_Key;
  
     switch(g_Key)
    {
        case KEY_0:
            /* 
            KEY 0 按键事件 
            */
        g_Key=KEY_NULL;
        break;
        
        case KEY_1:
             /* 
             KEY 1 按键事件 
             */
        g_Key=KEY_NULL;
        break;
        
        case KEY_WK_UP:
             /* 
             KEY_WK_UP 按键事件 
             */
        g_Key=KEY_NULL;
        break;
        
        default: break;
     }
  

总结

以上便是对按键状态机程序的总结,对长按和短按的判断实现……

版权声明:本文为CSDN博主「Net_Walke」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34142812/article/details/119721386

生成海报
点赞 0

Net_Walke

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

STM32读取编码器数据(STM32-1)

编码器(encoder)是将信号或数据进行编制、转换为可用以通讯、传输和存储的信号形式的设备。按照外形可以分为实心轴和空心轴;按照工作原理编码器可分为增量式和绝对式两类。增量式编码器是将位移转换成周期性

6.串口相关知识

目录 1.发送一个字节的函数 2.发送两个字节的函数 3.发送8位数据的数组 4.发送字符串 5.串口1发送字符串 6.打印函数(printf)  7.发送一个数控制灯 1.发送一个字节的函数 void

RT-Thread Studio移植LAN8720A驱动

RTT网络协议栈驱动移植(霸天虎) 1、新建工程 ​ 工程路径不含中文路径名,工程名用纯英文不含任何符号。 2、用CubeMx配置板子外设 2.1、配置时钟 ​ 按照自己板子配置相应时钟。

Lin总线通信在STM32作为主机代码以及从机程序

距离上次做资料准备已经过去六天了。最近在学车,上周末就没有开电脑。这周开始进行了Lin通信的代码整理,目前是可以正常通信的了,采用的是增强型校验方式。后期再进一步跟进研究。。。更新一博,留