vesperW · 2023年03月22日

【重学51单片机】二、基于状态机的按键驱动

image.png
【说在前面的话】

按键作为单片机的输入设备,可以向单片机输入数据、传输命令等,是设置参数和控制设备的常用接口。所以,学会按键驱动也是初学者必不可少的能力。说到按键驱动程序,大家应该也不陌生,而一般的按键驱动流程图如下

image.png

这里,可能有人会问,为什么要延时10ms啊?

那是因为按键被按下时,不会像理想的情况非0即1,而是会有抖动,如下图

image.png

当机械按键被按下或松开时,会有10ms的抖动时间,所以要延时10ms来消去波形抖动(* ̄︶ ̄)

知道了这个,一般初学者编写的按键驱动程序如下:

//延时1ms
void Delay1ms() {   //@12.000MHz
  unsigned char i, j;
  i = 12;
  j = 169;
  do{  
    while (--j);
  } while (--i);
}
//ms延时 
void delay_ms(int ms){
  char i = 0;
  for(i = 0; i < ms; i++){
    Delay1ms();  
  }
}
char get_key(){
  //检测按键是否被按下
  if(KEY1 == 0){
    //延时10ms
    delay_ms(10);
    //再次检测按键是否被按下
    if(KEY1 == 0){
      //等待按键松开
      while(KEY1 == 0){ }
      delay_ms(10);
      return 1;  
    }
  }
  return 0;
}

像这种按键驱动程序也很简单,作为基础学习和一些简单的系统中还可以,但是在很多的产品设计中,这种按键程序还是有很大的不足和缺陷。因为他不仅采用了软件延时使单片机效率降低而且还在那里死等按键松开,系统的实时性也变得很差。为此,有人提出了一种基于状态机的按键驱动程序,很好地解决了上述程序的缺陷。下面我们就简单讲一下什么是状态机。

【状态机简介】

对于学电子的同学,首先接触到的状态机应该是在数字逻辑电路(简称数电)中,状态机的分析方法被应用于时序逻辑电路的设计中,其实状态机的思想对我们的软件设计也很有用,首先简单介绍一下状态机,它是由有限的状态相互之间的迁移构成的。在任何时候,只能处于状态机的某一个状态,当接收到一个转移事件时,状态机进行状态的转移。

下面,就以按键驱动为例,画出他的状态转移图,如下

image.png

有了状态转移图,那我们就用程序实现一下这个按键驱动程序。从图中我们知道按键驱动程序由3个状态,刚好可以用C语言的switch     case语句来实现这3个状态,而状态间的迁移就可以用if条件判断语句来实现。知道了这个,那我们就动手实现一下。

基于状态机的按键驱动程序

首先,打开原理图,看一下按键接到了单片机的哪个管脚,如下

image.png

我们以按键1为例,接到了单片机的P33脚,当按键按下时为低电平松开为高电平,基于此我们的按键程序如下:


sbit KEY1 = P3^3;//key1
char get_key(){
  //保存按键状态
  static char key_flag = 0;
  //软件延时计时器
  static unsigned int s_Counter = 0;
  switch(key_flag){
    //状态0为无按键按下
    case 0:
      if(KEY1 == 0){
        //如果有按键按下,转为状态1
        key_flag = 1;
      }
      break;
     //状态1为延时消抖  
    case 1:
      s_Counter++;
      if(s_Counter > 1000){
        //延时10ms,计时器清零
        s_Counter = 0;
        if(KEY1 == 0){
          //如果按键被按下,转为状态2
          key_flag = 2;
          return 1;
        }else{
          //如果按键未按下,转为状态0
          key_flag = 0;
        }
      }
      break;
     //状态2为等待按键释放  
    case 2:
      if(KEY1 == 1){
        //如果按键松开,转为状态0
        key_flag = 0;
      }
      break;  
  }
  return 0;
}
  • 注意,每个case结束后都有一个break
  • 第18行,s_Counter \> 1000相当于延时10ms,当然这个1000是随便给的值,大家要根据具体情况设置此值,如果测试小于10ms就可以加大此值,我们只是为了说明用s_Counter 可以延时。
  • 第24行,在延时去抖完成后就返回了1(相当于按键按下),这样做的好处就是可以提高按键响应速度。当然也可以在状态2按键松开后返回1。

基于状态机的按键驱动程序我们就简单写完了,相信大家也get到重点了,这个只是简单实现了按键的单击,当然,我们也可以实现按键的双击和长按。
image.png
哈哈,在编写驱动之前,我们先细化一下需求,首先区分单击和长按,这个很简单,规定一个时间就可以,我们定为1秒钟。即按下时间小于1秒为单击,大于1秒为长按。

那双击怎么办呢?

我们规定,当第一次按下持续时间小于500ms内松开按键,在之后500ms内又按下按键,此时为双击事件。这里有两点需要注意,1、第一次按下的时间不能超过500ms,否则就被判断为单击或长按。2、在第一次按下松开后开始计时,如果500ms内没有按键再次按下则为单击。按键双击的原理如下图所示

image.png

按键双击和长按的需求我们讲完了,接下来画出他的状态转移图,如下

image.png

image.png

哈哈哈,还可以吧,没那么复杂。相信大家应该能看懂。这里有必要说一下状态5,判断双击其实就是第二次按下延时10ms消抖,如果确实按下则为双击否则为单击。好了,看看程序怎么实现吧,如下

#define DELAY_10ms   500
#define DELAY_500ms   10000
char get_key3(){
  static char key_flag = 0;
  static unsigned int s_Counter = 0;
  switch(key_flag){
    case 0://无按键按下
      if(KEY1 == 0){
        key_flag = 1;
      }
      break;
    case 1://延时10ms消抖
      s_Counter++;
      if(s_Counter > DELAY_10ms){
        s_Counter = 0;
        if(KEY1 == 0){
          key_flag = 2;
        }else{
          key_flag = 0;
        }
      }
      break;
    case 2://计时500ms,等待按键松开
      s_Counter++;
      if(s_Counter > DELAY_500ms){//500ms内按键未松开
        s_Counter = 0;  
        key_flag = 6;
      }
      if(KEY1 == 1){//500ms内按键松开
        s_Counter = 0;  
        key_flag = 3;
      }
      break;
    case 3://按键松开,延时10ms消抖
      s_Counter++;
      if(s_Counter > DELAY_10ms){
        s_Counter = 0;  
        key_flag = 4;
      }
    break;
    case 4://等待双击
      s_Counter++;
      if(s_Counter > DELAY_500ms){
        s_Counter = 0;
        key_flag = 7;//500ms内按键未按下
      }
      if(KEY1 == 0){//500ms内按键被按下
        s_Counter = 0;
        key_flag = 5;
      }
    break;
    case 5://延时10ms消抖
      s_Counter++;
      if(s_Counter > DELAY_10ms){
        s_Counter = 0;        
        if(KEY1 == 0){
          key_flag = 8;//等待按键松开      
          return 2;//双击
        }else{          
          key_flag = 7;//单击
        }
      }
    break;    
    case 6:
      s_Counter++;
      if(s_Counter > DELAY_500ms){
        s_Counter = 0;  
        key_flag = 8;//等待按键松开      
        return 3;//长按
      }
      if(KEY1 == 1){
        s_Counter = 0;  
        key_flag = 7;
      }
    break;
    case 7://单击
      key_flag = 8;
      return 1;//单击
      break;
    case 8://等待按键松开      
      if(KEY1 == 1){
        s_Counter = 0;  
        key_flag = 0;        
      }
    break;  
  }
  return 0;
}
  • 在开头定义了延时10ms和500ms的宏
  • 用返回值代表不同的按键事件,返回1为单击,返回2为双击,返回3为长按
  • 这里提醒大家按键在按下和松开时记得延时消抖

怎么样,双击和长按是不是很简单,接下来的彩蛋也很精彩哦。

image.png

今天的彩蛋环节依然是对上面的代码进行化简,使其变得更简洁和优雅。在简化之前,我们要在讲一个知识点,那就是子状态机。顾名思义,就是我们可以把上面的复杂状态机(包含8个状态的状态机)拆成多个简单的状态机,而拆开的每一个状态机就是一个子状态机

这个概念是懂了,那怎么把上面的状态机拆开呢?

哈哈,这个就需要在“双击”事件中做文章,大家可以这样想,双击其实就是两次单击,只不过两次单击的间隔时间小于500ms而已。基于此,我们就可以先用一个子状态机来区分单击和长按,然后再用一个状态机来区分双击,这样我们就把上面的复杂状态机拆成了两个状态机了。

image.png

可能这样说,大家还是不太明白,那我们就直接画出状态转移图,如下,是一个区分短按和长按的子状态机

image.png

有了状态转移图,程序也很简单,如下

#define DELAY_1000ms   20000
unsigned int  get_key_short_or_long(){
  static char key_flag = 0;
  static unsigned int s_Counter = 0;
  switch(key_flag){
    case 0://无按键按下
      if(KEY1 == 0){
        s_Counter = 0;
        key_flag = 1;
      }
      break;
    case 1://延时10ms消抖
      s_Counter++;
      if(s_Counter > DELAY_10ms){
        s_Counter = 0;
        if(KEY1 == 0){
          key_flag = 2;
        }else{
          key_flag = 0;
        }
      }
      break;  
    case 2://计时1000ms,
      s_Counter++;
      if(s_Counter > DELAY_1000ms){//大于1000ms为长按
        key_flag = 3;//等待按键松开      
        return s_Counter;//长按
      }
      if(KEY1 == 1){//小于1000ms为短按
        key_flag = 0;
        return s_Counter;//短按
      }
      break;
    case 3://等待按键松开      
      if(KEY1 == 1){        
        key_flag = 0;        
      }
    break;      
  }
  return 0;
}
  • 在状态2中,我们判断是长按还是短按,大于1000ms为长按,否则为短按
  • 注意,我们这次返回的是计数器s_Counter的值,这个是为了方便之后判断是不是双击(第一次单击持续时间小于500ms要等待双击事件)

image.png

好,接下来我们就看看程序怎么判断是双击的,如下

char get_key4(){
  static char key_flag = 0;
  static unsigned int s_Counter = 0;
  unsigned int key_time = 0;
  key_time = get_key_short_or_long();
  switch(key_flag){
    case 0:
      if(key_time >= DELAY_1000ms){
        return 3;
      }else if(key_time >= DELAY_500ms){
        return 1;
      }else if(key_time > 0){
        s_Counter = 0;
        key_flag = 1;
      }
      break;
    case 1://等待双击
      s_Counter++;
      if(s_Counter > DELAY_500ms){        
        key_flag = 0;
        return 1;
      }
      if(key_time > 0){
        key_flag = 0;
        return 2;
      }
      break;
  }    
}
  • 由于只有2个状态,而且都很简单,所以没有画它的状态转移图
  • 在状态0中,我们根据key_time 来判断长按还是短按,如果大于1秒为长按,返回3;如果大于500ms小于1s为单击返回1;如果按下的时间小于500ms,就转为状态1,等待双击。
  • 在状态1中,我们等待500ms,如果时间到了还没有按键按下则返回1,如果有按键按下(key_time 大于0小于500ms),则为双击返回2
作者:FledgingSu 支离苏
文章来源:嵌入式小书虫

推荐阅读

欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
2831
内容数
162
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息