STC15单片机-按键检测单击、双击和长按(状态机)

按键检测(状态机)

传统的按键检测

在单片机的应用中,利用按键实现与用户的交互功能是相当常见的,同时按键的检测也是很讲究的,众所周知,在有键按下后,数据线上的信号出现一段时间的抖动,然后为低,当按键释放时,信号抖动一段时间后变高,然而这段抖动时间要维持10ms~50ms,这个与按键本身的材质有一定的关系,在这个范围内基本上都可以确定的。如果按键检测的不好,单片机的运行效率将会大打折扣,严重影响到系统的性能,导致系统的运行出现异常,在教科书中,我们见到的按键处理程序都是以下这样的结构:

if(KEY_IO != 0xFF)		//检测到有按键按下
{
	DelayNms (20);		//延时20毫秒(严重影响单片机的运行效率)
	if (KEY_IO !=0xFF)	//确认按键按下
    {
    	switch (KEY_IO)
    	{
    		case 0xFE: KeyValue=1 ; break;
    		case 0xFD: KeyValue=2 ; break;
    		default: KeyValue=0 ; break;
    	}
    }
}

弊端:

像这样的程序经常出现在大学的教科书中,在按键的扫描中,单片机的资源全部用来做按键的扫描,特别是当中的延时程序,对单片机来说,这个一个漫长的过程。例如,我们需要用动态扫描数码管来做一个电子时钟,如果在按键持续按下的过程中,由于延时程序对单片机资源的占用,单片机这个时候就不能做动态扫描,数码管的显示就会有问题;

除非当前程序搭载了实时系统,一旦当前任务要进行延时操作,系统会自动进行任务调度,执行其他任务,当之前的任务延时完毕,系统会自动执行之前的任务。遗憾的是传统8051系列单片机不推荐搭载实时系统的,毕竟其资源有限,而且又增加额外的成本,比如搭载ucos实时系统,传统的8051系列单片机完全不能满足该系统的要求,必须拓展外部存储器才能满足,这样就间接上增加了成本,同时ucos用于商业上要收费的,成本大大地增加了。因此当没有搭载实时系统做按键检测使用软件延时是不现实的,严重影响性能

这样的教科书的按键处理程序是不实用的,在实际应用中是不可取的。所以这里介绍采用“状态机”的思想进行检测按键,不仅可以正确检测到按键,而且不会影响其他周边外设器件的运作。

有限状态机思想

​ 有限状态机是一种概念思想,把复杂的控制逻辑分解成有限个稳定状态,组成闭环系统,通过事件触发,让状态机按设定的顺序处理事务。

​ 状态机是软件编程中的一个重要概念,比这个概念更里要的定对它的灵活应用。在一个思路清晰而且高效的程序中,必然有状态机的身影浮现。

​ 比如说一个按键命令解析程序,就可以被看做状态机:本来在A状态下,触发一个按键后切换到了B状态:再触发另一个键后切换到C状态,或者返回到A状态。这就是最简单的按键状态机例子。实际的按键解析程序会比这更复杂些,但这不影响我们对状态机的认识。

​ 进一步看,击键动作本身也可以看做一个状态机。一个细小的击键动作包含了:按下、抖动、释放等状态。其实状态机思想不单只用在按键方面,数码管显示动态扫描、LED亮灭都是存在状态机的思想如亮与灭的状态。

​ 使用状态机思想去进行单片机编程,比较通用的方法就是用swtich 的选择性分支语句来进行状态跳转,既然可以 switch 来判断,那么使用 if 同样可以,但是使用 switch 来判断状态可以使代码更加清晰。

按键动作示意图

在这里插入图片描述

状态图

在这里插入图片描述

说明:

整个状态机使用定时器来驱动,每隔10ms进入一次状态机进行判断,检测消抖的时间通常也是10ms;多任务时可以避免其他任务占用CPU 过多的时间;

状态1:按键处于弹起状态,为高电平,定时器每隔10ms扫描一次,如果检测到按键的IO口为低电平了,则切换到状态2

状态2:按键抖动检测,检测到IO口低电平从状态1切换到状态2后,下一个10ms进来如果检测到IO口变回了高电平,说明低电平是由抖动引起的,按键没有被完全按下,状态2切回到状态1;如果检测到IO口任然是低电平,说明按键被按下,状态2切换到状态3

状态3:此状态下按键已确认被按下,IO口会一直是低电平,如果长按,也一直是处于这个状态3;这个状态可以做一些按键动作的检测,如长按、双击;该状态下如果检测到IO口为高电平,则切换到状态4

状态4:进行弹起抖动检测,从状态3切到该状态时,在下一个10ms如果检测到IO口又变为低电平了,则说明上一步高电平是有抖动引起的,再将状态切回到状态3;如果检测到IO口继续为高电平,说明按键松开了,将状态切换到状态1

又开始下一个循环……

程序

实现效果

按键2单击指示灯电平翻转,长按2秒则闪一下,双击则闪三下

按键检测程序思路

单击与双击:按键第一次检测到单击时,将单击状态缓存,同时启动双击定时器,超过一定时间,比如 200ms,认定按键为单击;如果没超过 200ms,又检测到了单击,认定按键为双击。

长按:检测到按键按下后,启动长按定时器,过了一段时间,比如 2s,按键依然为按下状态 ,认定按键为长按。

文件结构

在这里插入图片描述

main.c -> 主函数文件,包含 main 函数等;

Public.c -> 公共函数文件,包含 Delay 延时函数等;

Sys_init -> 系统初始化函数,包含 GPIO 初始化函数等;

LED.c -> LED 外设函数,包含 LED 打开、关闭函数等;

Timer0.c -> 定时器函数,包含定时器初始化,中断函数等;

KEY1.c -> 按键 1 函数,包含按键检测,中断函数等;

KEY2.c -> 按键 2 函数,包含按键状态机检测函数等;

KEY2.h:

用枚举定义状态机的4种状态,用结构体定义扫描定时器,将结构体变量声明为外部可调用

#ifndef __KEY2_H_
#define __KEY2_H_

//定义状体机使用的枚举类型
typedef enum
{
  STA1_KEY_Up             = (uint8_t)0x01,    //按键弹起
  STA2_KEY_DownShake      = (uint8_t)0x02,    //按下抖动
  STA3_KEY_Down           = (uint8_t)0x03,    //按键按下
  STA4_KEY_UpShake        = (uint8_t)0x04     //弹起抖动
}STA_Machine_Status_t;

//定义结构体类型
typedef struct
{
  STA_Machine_Status_t  ucSTA_Machine_Status;       //状态机状态
  uint16_t volatile     ucSTA_Machine_Scan_Timer;   //状态机扫描定时器
  uint16_t volatile     usKEY2_Double_Click_Timer;  //KEY2双击定时器
  uint16_t volatile     usKEY2_Press_Timer;         //KEY2长按定时器
}STA_Machine_t;

/* extern variables-----------------------------------------------------------*/
extern KEY_t          KEY2;
extern STA_Machine_t  STA_Machine;
/* extern function prototypes-------------------------------------------------*/ 

#endif
/********************************************************
  End Of File
********************************************************/
KEY2.c:
1.先搭好状态机的框架:
/* Includes ------------------------------------------------------------------*/
#include <main.h>

/* Private define-------------------------------------------------------------*/
#define KEY2_State P33

#define Set_Press_TIME          TIMER_2S            //设置长按时间
#define Set_Double_Click_TIME   TIMER_200MS         //设置双击时间

/* Private variables----------------------------------------------------------*/
static uint8_t Click_Buf = FALSE;                 //单击状态缓存
static void KEY_Detect();

/* Public variables-----------------------------------------------------------*/
KEY_t KEY2 = {FALSE,FALSE,FALSE,FALSE,KEY_Detect};		//标志位初始化
STA_Machine_t  STA_Machine = {STA1_KEY_Up,0,0,0};		//状态和定时器初始化

/* Private function prototypes------------------------------------------------*/

/*
* @name   KEY_Detect
* @brief  按键2检测(状态机)
* @param  None
* @retval None   
*/
static void KEY_Detect()
{
    //状态机扫描定时器计时大于或等于10ms,进入一次状态机
    if(STA_Machine.ucSTA_Machine_Scan_Timer >= TIMER_10MS)
    {
      switch (STA_Machine.ucSTA_Machine_Status)
      {
        //按键弹起
        case STA1_KEY_Up:
        {
          if(KEY2_State == 0)                                          
          {
            //切换到状态2
            STA_Machine.ucSTA_Machine_Status = STA2_KEY_DownShake;  
          }
          break;
        }
        //按下抖动
        case STA2_KEY_DownShake:
        {
          if(KEY2_State == 0)
          {
            //切换到状态3
            STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;
          }
          else
          {
            //如果检测到高电平说明是抖动,切回到状态1
            STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             
          }
          break;
        }
        //按键按下
        case STA3_KEY_Down:
        {
          if(KEY2_State == 1)
          {
            //切换到状态4
            STA_Machine.ucSTA_Machine_Status = STA4_KEY_UpShake;      
          }
          break;
        }
        //弹起抖动
        case STA4_KEY_UpShake:
        {
          if(KEY2_State == 1)
          {
            //切换到状态1,完成一次按键动作
            STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;
          }
          else
          {
             //否则判断为抖动,切回到状态3
             STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;
          }
          break;
        }
        default:
            //默认情况都切换到状态1
            STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             
          break;
      }

      //状态机扫描定时器清零,开始下一次扫描
      STA_Machine.ucSTA_Machine_Scan_Timer = 0;
    }
}
/********************************************************
  End Of File
********************************************************/
2.完成按键单击、双击、长按的检测
/* Includes ------------------------------------------------------------------*/
#include <main.h>

/* Private define-------------------------------------------------------------*/
#define KEY2_State P33

#define Set_Press_TIME          TIMER_2S            //设置长按时间
#define Set_Double_Click_TIME   TIMER_200MS         //设置双击时间

/* Private variables----------------------------------------------------------*/
static uint8_t Click_Buf = FALSE;                 //单击状态缓存
static void KEY_Detect();

/* Public variables-----------------------------------------------------------*/
KEY_t KEY2 = {FALSE,FALSE,FALSE,FALSE,KEY_Detect};		//标志位初始化
STA_Machine_t  STA_Machine = {STA1_KEY_Up,0,0,0};		//状态和定时器初始化

/* Private function prototypes------------------------------------------------*/

/*
* @name   KEY_Detect
* @brief  按键2检测(状态机)
* @param  None
* @retval None   
*/
static void KEY_Detect()
{
    //状态机扫描定时器计时大于或等于10ms,进入一次状态机
    if(STA_Machine.ucSTA_Machine_Scan_Timer >= TIMER_10MS)
    {
      switch (STA_Machine.ucSTA_Machine_Status)
      {
        //按键弹起
        case STA1_KEY_Up:
        {
          if(KEY2_State == 0)                                          
          {
            //切换到状态2
            STA_Machine.ucSTA_Machine_Status = STA2_KEY_DownShake;  
          }
          else
          {
            //按键没被按下,则判断是否有单击缓存
            if(Click_Buf == TRUE)
            {
              //如果双击定时器大于200ms后,说明没在规定时间内进行双击,判断上一次按下是单击操作
              /*如果双击定时器小于200ms,则下面判断不成立,说明还有时间完成双击操作,再按下按键后
              就从状体1到状态2再到状态3,状态3里判断是双击操作*/
              if(STA_Machine.usKEY2_Double_Click_Timer >= Set_Double_Click_TIME)
              {
                KEY2.KEY_Flag = TRUE;
                KEY2.Click    = TRUE;

                //清除单击缓存
                Click_Buf = FALSE;
              }
            }
          }
          break;
        }
        //按下抖动
        case STA2_KEY_DownShake:
        {
          if(KEY2_State == 0)
          {
            //切换到状态3
            STA_Machine.ucSTA_Machine_Status = STA3_KEY_Down;
            //长按定时器清0,开始计算长按时间
            STA_Machine.usKEY2_Press_Timer = 0;
          }
          else
          {
            //如果检测到高电平说明是抖动,切回到状态1
            STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             
          }
          break;
        }
        //按键按下
        case STA3_KEY_Down:
        {
          if(KEY2_State == 1)
          {
            //切换到状态4
            STA_Machine.ucSTA_Machine_Status = STA4_KEY_UpShake;
            //不是长按操作,则判断是不是双击操作
            if(KEY2.Press == FALSE)
            {
              //双击检测
              //有单击缓存,说明前面已经单击一次,这次就判断为双击操作
              if(Click_Buf == TRUE)
              {
                KEY2.KEY_Flag     = TRUE;
                KEY2.Double_Click = TRUE;
                //清除单击缓存,为下一次双击准备
                Click_Buf = FALSE;
              }
              else
              {
                //没有单击缓存,说明这是双击的第一次点击,则进行缓存
                Click_Buf = TRUE;
                /*双击定时器清零,开始计算双击时间,如果在双击时间内,再次进入到状态3,则判断是双击操作,
                如果在状态1里超时了,则状态1里检测为单击*/
                STA_Machine.usKEY2_Double_Click_Timer = 0;
              }
            }   
          }
          else
          {
            //长按检测
            if(KEY2.Press == FALSE)
            {
              /*如果长按定时器超过两秒,认为是长按,进入判断体内将长按标志位置TRUE;如果在两秒内松开了按键
              则会判断是不是双击,去执行判断双击的情况*/
              if(STA_Machine.usKEY2_Press_Timer >= Set_Press_TIME)
              {
                STA_Machine.ucSTA_Machine_Status = STA4_KEY_UpShake;
                /*为什么要切换到状态4?
                
                解释:因为这里将KEY2.Press置TRUE后,后面执行一次按键动作,指示灯闪一下,随后KEY2.Press被清零;
                      下一个10ms再进入函数时,状态位还是3,还是进入这里执行,KEY2.Press又会被置成TRUE,所以一直
                      按住不放的话,指示灯就会一直闪,而不是闪一下的情况

                      而在这里第一次进来后将状态位切换到4,KEY2.Press还是会置TRUE,后面指示灯闪一下,KEY2.Press
                      被清零,下一个10ms进来后会直接跳到状态4,所以KEY2.Press不会再次被置TRUE,后面指示灯动作不会
                      再执行,所以只闪一下*/
                KEY2.KEY_Flag = TRUE;
                KEY2.Press    = TRUE;
                
                //因为已经判断为长按,所以不是双击操作,把单击缓存清0
                if(Click_Buf == TRUE)
                {
                  Click_Buf = FALSE;
                }
              }
            }
          }
          break;
        }
        //弹起抖动
        case STA4_KEY_UpShake:
        {
          if(KEY2_State == 1)
          {
            //切换到状态1,完成一次按键动作
            STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;
          }
          /*如果KEY2_State == 0,不跳回状态3,避免反复检测长按的情况*/
          break;
        }
        default:
            //默认情况都切换到状态1
            STA_Machine.ucSTA_Machine_Status = STA1_KEY_Up;             
          break;
      }

      /****执行按键动作,用于检测按键效果****/
      //单击动作 -> 指示灯翻转
      if(KEY2.KEY_Flag == TRUE)
      {
        if(KEY2.Click == TRUE)
        {
          Run_LED.Run_LED_Flip();
        }

        //长按动作 -> 指示灯闪一下
        if(KEY2.Press == TRUE)
        {
          Run_LED.Run_LED_Flip();
          Public.Delay_ms(100);
          Run_LED.Run_LED_Flip();
        }

        //双击动作 -> 指示灯闪三下
        if(KEY2.Double_Click == TRUE)
        {
          Run_LED.Run_LED_Flip();
          Public.Delay_ms(100);
          Run_LED.Run_LED_Flip();
          Public.Delay_ms(100);
          Run_LED.Run_LED_Flip();
          Public.Delay_ms(100);
          Run_LED.Run_LED_Flip();
          Public.Delay_ms(100);
          Run_LED.Run_LED_Flip();
          Public.Delay_ms(100);
          Run_LED.Run_LED_Flip();
        }
      }

      //按键状体位清零,为下一次按下准备
      KEY2.Click        = FALSE;
      KEY2.Press        = FALSE;
      KEY2.Double_Click = FALSE;

      //状态机扫描定时器清零,开始下一次扫描
      STA_Machine.ucSTA_Machine_Scan_Timer = 0;
    }
}
/********************************************************
  End Of File
********************************************************/
Timer0.c:

定时器0中断处理函数中让状态机里用到的3个定时器每隔5ms加1

/*
* @name   Timer0_isr
* @brief  定时器0中断处理函数(5ms进入一次)
* @param  None
* @retval None   
*/
void Timer0_isr() interrupt 1
{
	Timer0.msMCU_Timer0_Value++;
	if(Timer0.msMCU_Timer0_Value >= TIMER_500MS)	//计时到500ms
	{
		Timer0.msMCU_Timer0_Value = 0;
		//Run_LED.Run_LED_Flip();					//运行指示灯翻转
	}
	STA_Machine.ucSTA_Machine_Scan_Timer++;			//状态机扫描定时器
	STA_Machine.usKEY2_Double_Click_Timer++;		//双击定时器
	STA_Machine.usKEY2_Press_Timer++;				//长按定时器
}
main.c:

main函数先进行系统初始化,主要是引脚和定时器的初始化,然后在while循环里调用按键2的按键扫描函数不断扫描即可

/*
	* @name   main
	* @brief  主函数
	* @param  void	
	* @retval int      
*/
int main(void)
{	
	//系统初始化
	Hradware.Sys_Init();
	//系统主循环
	while(1)
	{
		//按键检测
		//KEY1.KEY_Detect();
		KEY2.KEY_Detect();
	}
}
  • 17
    点赞
  • 117
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值