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

    关注

    6037

    文章

    44558

    浏览量

    635187
  • 驱动程序
    +关注

    关注

    19

    文章

    836

    浏览量

    48035
  • 状态机
    +关注

    关注

    2

    文章

    492

    浏览量

    27539
  • 驱动设计
    +关注

    关注

    1

    文章

    111

    浏览量

    15285
  • 按键驱动
    +关注

    关注

    0

    文章

    11

    浏览量

    7133
收藏 人收藏

    评论

    相关推荐

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

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

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

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

    状态机举例

    状态机举例 你可以指定状态寄存器和状态机状态。以下是一个有四种状态的普通状态机。 // Th
    发表于 03-28 15:18 986次阅读

    利用状态机按键消抖程序

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

    状态机原理及用法

    状态机原理及用法状态机原理及用法状态机原理及用法
    发表于 03-15 15:25 0次下载

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

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

    状态机概述 如何理解状态机

    本篇文章包括状态机的基本概述以及通过简单的实例理解状态机
    的头像 发表于 01-02 18:03 1w次阅读
    <b class='flag-5'>状态机</b>概述  如何理解<b class='flag-5'>状态机</b>

    FPGA:状态机简述

    本文目录 前言 状态机简介 状态机分类 Mealy 型状态机 Moore 型状态机 状态机描述 一段式
    的头像 发表于 11-05 17:58 7399次阅读
    FPGA:<b class='flag-5'>状态机</b>简述

    什么是状态机状态机5要素

    玩单片还可以,各个外设也都会驱动,但是如果让你完整的写一套代码时,却无逻辑与框架可言。这说明编程还处于比较低的水平,你需要学会一种好的编程框架或者一种编程思想!比如模块化编程、状态机编程、分层思想
    的头像 发表于 07-27 11:23 2w次阅读
    什么是<b class='flag-5'>状态机</b>?<b class='flag-5'>状态机</b>5要素

    基于事件驱动的有限状态机介绍

      一、介绍 EFSM(event finite state machine,事件驱动型有限状态机),是一个基于事件驱动的有限状态机,主要应用于嵌入式设备的软件系统中。 EFSM的设计
    的头像 发表于 11-16 15:29 2354次阅读

    基于STM32按键的防抖和松开处理:状态机

    用延时和while();去处理按键很浪费资源,这里我们用定时器来做一个按键的处理-状态机;typedef enum {KEY_RELEASED,KEY_PRESSED,KEY_PROCESSED
    发表于 12-09 09:21 7次下载
    基于STM32<b class='flag-5'>按键</b>的防抖和松开处理:<b class='flag-5'>状态机</b>

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

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

    基于事件驱动的有限状态机介绍

    EFSM(event finite state machine,事件驱动型有限状态机),是一个基于事件驱动的有限状态机,主要应用于嵌入式设备的软件系统中。
    的头像 发表于 02-11 10:17 1055次阅读

    按键状态机代码

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

    什么是状态机状态机的种类与实现

    状态机,又称有限状态机(Finite State Machine,FSM)或米利状态机(Mealy Machine),是一种描述系统状态变化的模型。在芯片设计中,
    的头像 发表于 10-19 10:27 9545次阅读