0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

基于状态机的按键驱动设计

CHANBAEK 来源:嵌入式小书虫 作者:FledgingSu 支离苏 2023-07-04 11:43 次阅读

【说在前面的话】

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

图片

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

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

图片

机械按键被按下或松开时,会有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;
}

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

【状态机简介】

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

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

图片

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

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

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

图片

我们以按键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到重点了,这个只是简单实现了按键的单击,当然,我们也可以实现按键的双击和长按。

哈哈,在编写驱动之前,我们先细化一下需求,首先区分单击和长按,这个很简单,规定一个时间就可以,我们定为1秒钟。即按下时间小于1秒为单击,大于1秒为长按。

那双击怎么办呢?

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

图片

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

图片

哈哈哈,还可以吧,没那么复杂。相信大家应该能看懂。这里有必要说一下状态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为长按
  • 这里提醒大家按键在按下和松开时记得延时消抖

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

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

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

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

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

图片

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

#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要等待双击事件)

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

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
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 单片机
    +关注

    关注

    6001

    文章

    43973

    浏览量

    620854
  • 驱动程序
    +关注

    关注

    19

    文章

    770

    浏览量

    47236
  • 状态机
    +关注

    关注

    2

    文章

    486

    浏览量

    27182
  • 驱动设计
    +关注

    关注

    1

    文章

    108

    浏览量

    15187
  • 按键驱动
    +关注

    关注

    0

    文章

    11

    浏览量

    7100
收藏 人收藏

    评论

    相关推荐

    STM32按键消抖——入门状态机思维

    本篇介绍了嵌入式软件开发中常用的状态机编程实现,并通过按键消抖实例,以常用的switch-case形式,实现了对应的状态机编程代码实现,并通过测试,串口打印对应状态,分析
    的头像 发表于 09-02 21:54 4285次阅读
    STM32<b class='flag-5'>按键</b>消抖——入门<b class='flag-5'>状态机</b>思维

    STM32按键状态机2——状态简化与增加长按功能

    本篇继续介绍状态机的使用,在上篇的基础上,通过简化按键去抖逻辑,并增加按键长按功能,进一步介绍状态图的修改与状态机代码的实现,并通过实际测试
    的头像 发表于 09-03 21:26 3383次阅读
    STM32<b class='flag-5'>按键</b><b class='flag-5'>状态机</b>2——<b class='flag-5'>状态</b>简化与增加长按功能

    状态机编程

    状态机编程基于状态机按键输入软件接口设计一般的教课书中给出的按键输入软件接口程序通常非常简单,在程序中一旦检测到按键输入口为低电平时(图9
    发表于 07-10 18:00

    有限状态机按键接口程序怎么编写?

    的系统,根据状态机的原理对其动作的操作和确认的过程进行分析,并用状态图表示出来,然后根据状态图编写出按键接口程序。
    发表于 10-21 08:33

    什么是状态机状态机是如何编程的?

    什么是状态机状态机是如何编程的?
    发表于 10-20 07:43

    单片状态机按键长按和短按实现

    本文只介绍主要代码段,完整代码可参考我的“蓝桥杯单片状态机按键按下和松开实现不同功能”蓝桥杯单片状态机
    发表于 01-06 08:26

    如何利用STM32去实现一种按键有限状态机

    STM32实现按键有限状态机(超详细,易移植)一、状态机简而言之,状态机是使不同状态之间的改变以及状态
    发表于 02-16 06:58

    独立按键状态机读取函数的过程分享

    蓝桥杯单片状态机按键按下和松开实现不同功能独立按键状态机读取函数key_flag 键值读取标志位key 主函数中得到键值key_press
    发表于 02-23 06:20

    怎样去编写一种按键状态机函数呢

    如何利用定时器间隔扫描按键驱动函数呢?怎样去编写一种按键状态机函数呢?
    发表于 03-01 07:15

    利用状态机按键消抖程序

    利用状态机按键消抖程序讲解,很好的资料下载吧。
    发表于 01-11 09:32 30次下载

    基于状态机的简单按键驱动设计

    一般的按键驱动程序通常非常简单。在程序中一旦检测到按键输入口为低电平时,就采用软件延时10 ms后再次检测按键输入口。如果仍然是低电平则表示有按键
    发表于 10-31 16:42 1次下载
    基于<b class='flag-5'>状态机</b>的简单<b class='flag-5'>按键</b><b class='flag-5'>驱动</b>设计

    基于状态机的单片机按键短按长按功能的实现

    本文主要介绍了基于状态机的单片机按键短按长按功能的实现,按键的击键过程也是一种状态的切换,也可以看着是一个状态机,一个
    发表于 12-28 08:43 1.8w次阅读
    基于<b class='flag-5'>状态机</b>的单片机<b class='flag-5'>按键</b>短按长按功能的实现

    STM32实现按键有限状态机(超详细,易移植)

    STM32实现按键有限状态机(超详细,易移植)一、状态机简而言之,状态机是使不同状态之间的改变以及状态
    发表于 12-17 18:37 25次下载
    STM32实现<b class='flag-5'>按键</b>有限<b class='flag-5'>状态机</b>(超详细,易移植)

    按键状态机代码

    自己写的按键状态机,需要的时候根据情况修改一下
    发表于 03-27 10:42 6次下载

    一个基于事件驱动的有限状态机

    EFSM(event finite state machine,事件驱动型有限状态机),是一个基于事件驱动的有限状态机,主要应用于嵌入式设备的软件系统中。 EFSM的设计原则是
    的头像 发表于 08-30 09:28 491次阅读
    一个基于事件<b class='flag-5'>驱动</b>的有限<b class='flag-5'>状态机</b>