• 由一个按键程序引发的思考(下)


      书接上文,上文由一个按键程序引发的思考(中)中讨论了如何实现按键单次按下后能立即执行按键动作,同时又能滤除掉按下时间非常短的无效按键。但是在实际项目中,一个按键往往会具有多种功能,可以通过单击、双击、长按等方式切换不同的功能。
      那对于单击、双击、长按这些功能要如何有效区分呢?下面就从按键的波形和判断逻辑来讨论这三种情况如何区分?

    在这里插入图片描述
      上篇文章中说过,按键的状态是在定时器中10ms扫描一次的,这10ms就相当于已经做过软件防抖了。所以在这里分析按键状态的时候,所有的状态都认为是有效的,不考虑软件防抖的情况。

      在单击按键判断的时候,采用的方法是:当按键出现了下降沿,同时按键的低电平持续一定时间之后,就认为按键有效,就可以去处理按键的动作了。等到按键弹起后在接着扫描下一次按键,这样就不用一直等到按键弹起后再去处理按键动作,提高了按键的响应速度。那么依然用这种方式来区分单击、双击、长按还行吗?

      通过观察上面三种波形可以发现,每种按键都包含了上升沿、下降沿、低电平。如果只用这三种状态去判断的话,这三种按键很难区分开,那么要彻底的将这三种状态区分开,那么就得找出每种按键和其他按键最大的不同。区别最明显的就是双击按键,因为它有两个上升沿,两个下降沿。接下来是长按按键,因为它中间有一段很长的低电平时间。看来要区分这三种状态的按键,就得把按键中的所有信息都收集到,包括上升沿、下降沿、低电平时间、高电平时间、上升沿次数、下降沿次数。

      由于现在一个按键中需要监控的状态很多,直接定义变量的话,需要定义很多个变量。为了方便管理,现在用一个结构体,将按键的所有信息都存储在结构体中。

    typedef struct
    {
        _Bool  last_state;                   //上一状态
        _Bool  now_state;                    //现在状态
        u16    low_level_time;               //低电平时间
        u16    high_level_time;              //高电平时间
        u8     rising_cnt;                   //上升沿次数
        u8     falling_cnt;                  //下降沿次数
        _Bool  is_rising_edge;               //是否上升沿
        _Bool  is_falling_edge;              //是否下降沿
    } Key_TypeDef;
    
    Key_TypeDef keyState;     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

      接下来在定时器10ms中断中,读取按键的状态,将按键所有的状态存储起来。
    在这里插入图片描述

    void key_check( void )
    {
        keyState.now_state = KEY;                           //存储按键当前电平信号,防止在代码处理过程中信号突变。
    
        if( keyState.now_state != keyState.last_state )     //出现了电平跳变
        {
            keyState.last_state = keyState.now_state;
            if( keyState.now_state == 0 )                   //当前为低电平,说明下降沿
            {
                keyState.is_falling_edge = 1;               //标记下降沿
                keyState.falling_cnt += 1;                  //下降沿次数加1
            }
            if( keyState.now_state == 1 )                   //当前为高电平,说明上升沿
            {
                keyState.is_rising_edge = 1;                  //标记上升沿
                keyState.rising_cnt += 1;                     //上升沿次数加1
            }
        }
    
        if( ( keyState.now_state == 0 ) && ( keyState.is_falling_edge == 1 ) ) //当前为低电平 同时出现过下降沿
        {
            keyState.low_level_time++;                      //累计低电平时间
        }
    
        if( ( keyState.now_state == 1 ) && ( keyState.is_rising_edge == 1 ) ) //当前为高电平 同时出现过上升沿
        {
            keyState.high_level_time++;                     //累计高电平时间
        }
    
    }
    
    • 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

      为了使代码的结构看起来更简单,同时为了方便以后移植,在定时器扫描中只对按键的状态做记录,并不判断按键是哪种按键,对按键种类的判断,单独在另一个函数中去判断。
      当按键的电平出现跳变时,说明按键此刻弹起或者按下了,将上升沿或者下降沿标记出来,同时记录上升沿和下降沿出现的次数。当出现下降沿时同时为低电平时,就累计低电平的时长,当出现上升沿并为高电平的时候,就累计高电平的时长。

      当按键双击时,出现了两次低电平,为了程序写起来简单一点,这两次低电平的时间没有分开统计,两个低电平时间统计到了一起。

      当按键所有的信息都统计到了之后,那么此时就可以根据每个按键的信息来区分按键种类了。

    在这里插入图片描述
      下面分别讨论这三种按键为什么要这么区分?

      首先是单击按键,需要判断上升沿,下降沿,还有低电平的时间为 0.3–0.8s
    之间。
      假如只判断 下降沿和低电平时最小长时。
    在这里插入图片描述
      这三种按键在扫描的时候,都有这三种状态出现,就有可能将其他两种按键提前判定为单次按键。
      那假如只判断 下降沿、上升沿 和低电平。
    在这里插入图片描述
      在双击按键按下的过程中也会出现下降沿、低电平、上升沿这三个条件。在双击按键按下的过程中,有可能就提前判断成了单击按键。那么如何才能将单击按键和双击按键第一个低电平有效区分呢?此时只有通过低电平的时间来区分。

    在这里插入图片描述
      当单击按键低电平持续的时间大于 双击按键第一次低电平和第一次高电平时间之和时,此时单击按键有下降沿,低电平,上升沿三种状态。而双击按键此时有两个下降沿,低电平、上升沿四种状态。这样在双击按键按下的过程中双击按键弹起的瞬间,此时低电平持续的时间比较短,不会误判为单击按键。当双击按键第二次按下后,第二次低电平时间会和第一次低电平时间累加,低电平时间有可能和单击按键低电平时间一样,但是此时双击按键已经有两个下降沿了,也不会误认为是单击按键。

      同时单击按键也需要设置低电平的最长时间,否则低电平时间持续过长就会和长按有冲突。

      所以为了单击按键能和双击按键,长按按键有效区分时,按键时间必须比双击按键按下的时间长一点,但同时按下时间也不能太长。

      单击按键有效判断条件为:一次上升沿、一次下降沿、低电平时间在0.3s到0.8s之间。当然这个低电平时间条件也可以根据项目的实际情况修改。

      接下来判断双击按键,双击按键和其他按键最好区分,因为双击按键里面会出现两个下降沿。
    在这里插入图片描述
      既然用下降沿可以区分,那么为什么还要加上高电平时间长度的限制呢?可以考虑一下下面这种情况。

    在这里插入图片描述
      当单击按键连续按了两次,中间的高电平时间为2s,如果此时判断为是双击按键,肯定是不合适的。既然是双击,那么两次按键中间的停顿必须很短,如果间隔几秒钟那肯定就是单击按键了。所以此时就要加上高电平时间的限制,中间停顿时间很短时才是双击按键,否则就当做单击按键处理。

      双击按键有效判断条件为:两次下降沿、高电平时间小于0.5s。由于上升沿和低电平时间对于双击按键来说,不是最主要的因素,所以就可以不用考虑低电平和上升沿。如果想要双击按键按下的速度要快一点,可以加上低电平时长的限制。

      最后来判断长按按键,长按按键的特征是最鲜明的。
    在这里插入图片描述
      它的低电平时间远远大于其他两种按键的总时间,所以只要出现下降沿,同时低电平的时间足够长就可以判定为时长按。

      这里长按不推荐去检测上升沿,因为长按按键,按下的时间人很难准确的把握,如果要等到按键抬起后去判断,不是按键时间太短,就是按键时间太长,用户操作起来感觉不是很友好,当按下的低电平时间够了之后,直接去执行按键动作,此时用户就知道长按按键生效了,自然就会抬起按键。

      同时还要限制长按按键的低电平时间最长时间,这里是为了防止按键失效或者被外部其他东西压住后一直处于按下状态。如果不限制低电平最长时间,程序就会一直处于长按按键检测中,影响其他功能的正常运行。

      根据上面这些条件,单独写一个按键模式判断的函数。

    在这里插入图片描述

    u8 key_read( void )
    {
        u8 val = 0;
    
        //无效按键
        //当高电平超过500ms,说明按键已经释放了,此时复位按键状态。准备下一次检测。也就是两次按键中间间隔至少0.5s
        //当低电平时间超过 10s 说明按键一直未释放,有可能按键损坏,此按键无效。
        if( ( keyState.high_level_time > HIGH_MAX_TIME ) || ( keyState.low_level_time > LONG_MAX_TIME ) )
        {
            val = 0;
            keyState_rst();
            return val;
        }
    
        //连续按键 判断
        //下降沿次数为2 ,同时高电平时间小于500ms。 将下降沿次数改为3  就可以判断 3次按键
        if( keyState.falling_cnt == 2  )
        {
            if( ( keyState.high_level_time < HIGH_MAX_TIME ) && ( keyState.high_level_time > HIGH_MIN_TIME ) )
            {
                val = 2;                //2次连续按键
                return val;
            }
        }
    
        //长按按键 判断
        //下降沿次数为1 低电平时间大于2s, 小于10s。
        if( keyState.falling_cnt == 1 )
        {
            if( ( keyState.low_level_time < LONG_MAX_TIME ) && ( keyState.low_level_time > LONG_MIN_TIME ) )
            {
                val = 3;                //长按
                return val;
            }
        }
    
        //单次按键 判断
        //下降沿次数为1 上升沿次数为1  低电平时间大于300ms, 小于800ms。
        //由于长按的状态包含了单次按键的状态,所以在长按的时候会先检测到 单次按键 等低电平超时后 才认为是长按
        //如果要将单次按键和长按区分开,单次按键检测时就必须识别上升沿,也就是等按键弹起
        if(  keyState.falling_cnt == 1   ) && (  keyState.rising_cnt == 1   )
        {
            if( ( keyState.low_level_time > ONCE_MIN_TIME ) && ( keyState.low_level_time < LONG_MIN_TIME ) )
            {
                val = 1;                //单次按键
                return val;
            }
        }
    
        return val;
    }
    
    
    • 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

      当按键的高电平时间超过0.5s时,就认为按键已经弹起来了,此时复位按键的所有状态,准备下一次按键的检测。

    void keyState_rst( void )
    {
        keyState.high_level_time = 0;
        keyState.low_level_time = 0;
        keyState.is_falling_edge = 0;
        keyState.is_rising_edge = 0;
        keyState.rising_cnt = 0;
        keyState.falling_cnt = 0;
        keyState.last_state = KEY_INVALID_LEVEL;            //按键默认为无效电平
        keyState.now_state = KEY_INVALID_LEVEL;             //
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      有的按键按下为低电平,有的按键按下为高电平。这里使用的按键是默认高电平,按下为低电平。如果按键状态默认为低电平的话,只需要修改按键模式的判断条件就行,不需要修改定时器中对按键状态记录的函数。同样如果想要判断其他类型的按键话,也只需要修改按键模式判断函数中的内容就行。

  • 相关阅读:
    3D建模游戏场景创建大致流程
    ssh免密登陆远程Linux服务器
    Vue2转Vue3快速上手第一篇(共两篇)
    C++ 基础一
    新浪股票行情数据接口有什么作用?
    聊天软件项目开发2
    【第三篇】- 深入学习Git 工作流程
    树的引进以及二叉树的基础讲解——【数据结构】
    2022-mac系统,系统各个文件夹的的含义,防止有些同学误删
    【VSCode 插件商城无法搜索到插件的解决方法】
  • 原文地址:https://blog.csdn.net/qq_20222919/article/details/127699656