• 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;
        	}
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    弊端:

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

    除非当前程序搭载了实时系统,一旦当前任务要进行延时操作,系统会自动进行任务调度,执行其他任务,当之前的任务延时完毕,系统会自动执行之前的任务。遗憾的是传统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
    ********************************************************/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    KEY2.c:
    1.先搭好状态机的框架:
    /* Includes ------------------------------------------------------------------*/
    #include 
    
    /* 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
    ********************************************************/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    2.完成按键单击、双击、长按的检测
    /* Includes ------------------------------------------------------------------*/
    #include 
    
    /* 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
    ********************************************************/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    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++;				//长按定时器
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    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();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
  • 相关阅读:
    Apollo 应用与源码分析:Monitor监控-硬件监控-CAN监控
    网页设计期末课程设计大作业 HTML、CSS 海绵宝宝动漫网页作业
    阿里云国际修改域名绑定的DDoS高防服务器
    更改搜索路径上的文件夹
    Android学习---zygote(上)
    Java过滤器Filter讲解
    k8s快速入门教程-----4 工作负载控制器之deployment
    Pytorch:使用data_prefetcher提升数据读取速度【自动将训练数据移动到GPU】
    常见音视频、流媒体开源编解码库及官网(四十一)
    设计模式之单例和原型
  • 原文地址:https://blog.csdn.net/weixin_46251230/article/details/126659673