基于状态变化机制的按键监控代码,可实现长按,N击

前言

更新时间:2025.2.3

编程语言:micropython

硬件:esp32s3wroom1u

平台:windows10,mpremote(一个第三方python包,用于操作远程python解释器),vscode

因为近期研发的项目需要用到按键,本代码由本人独立书写,未参考过其他按键处理方法,如有雷同,纯属巧合。需要用到本文章内容的,请标明出处。

本代码使用的是esp32s3wroom1u的GPIO36作为按键检测,设置GPIO45内部上拉。

原理图如下:

思想

按键按下时会在K1端产生如下波形:

观察到按键按下时会经历5个状态:

  1. 闲置状态:此时按键未被按下,读取到高电平
  2. 按下抖动状态:此时按键的抖动有可能来自于外界振动或按键按下时引起的抖动
  3. 按下状态:此时按键被按下,读取到低电平
  4. 按键松开抖动状态:此时按键的抖动来自于按键松开时的抖动
  5. 按键松开状态:此时按键被松开,读取到高电平,接着按键进入闲置状态,开始新的轮回

 我按照计算机的排序给上面5个状态依次从0编号到4,分别代表一个状态。

每个状态转化都是有过渡的,分为转化前的状态和转化后的状态,或者说,上一时刻状态和当前状态,用两个变量分别描述:last_statenow_state

我使用1个定时器,每20ms读取一次按键的状态(至于为什么是20ms,随便取的,有兴趣的可以试试其他值)。

20ms采一次样,可能落在波形上的任意一点,根据状态分情况,就有5个分支。

if last_state==0:
    ...
elif last_state==1:
    ...
elif last_state==2:
    ...
elif last_state==3:
    ...
elif last_state==4:
    ...

先看第一种情况上次采样时按键状态编号为0,对应闲置状态。

然后读取现在时刻按键的电平, 有可能按键还是闲置状态 key1_value==1,或者处于按下抖动状态 key1_value==0。据此,我们可以判断当前的按键状态并更新到变量now_state

last_state=now_state
key1_value=key1.value()
if last_state==0:
    # 按键未被按下
    if key1_value==1:
        now_state=0
    # 按键处于按下抖动状态
    else:
        now_state=1

看第二种情况: 上次采样时按键状态编号为1,对应按键按下抖动状态。

然后比较现在时刻按键的电平与上一采样时刻按键电平, 

key1_value==1 此时距离上次采样已经间隔了20ms(如果忽略程序执行时间),这个时间足以把按键抖动的时间包含在内,抖动时间一般极短,所以可以判断按键没有被按下,上次采样的key1_value==0只是因为外界震动引起的

 key1_value==0 同理,此时距离上次采样已经间隔了20ms,所以判断按键被按下,然后我采用变量press_time自增的方式对按键按下的时间进行计时,在这一步可以开始计时,对应代码press_time=0

据此,我们可以判断当前的按键状态并更新到变量now_state

elif last_state==1:
    # 按键被按下
    if key1_value==0:
        now_state=2

        # 初始化按键按下时间
        press_time=0

    # 只是抖动而已
    else:
        now_state=0

看第三种情况: 上次采样时按键状态编号为2,对应按键按下状态。

然后比较现在时刻按键的电平与上一采样时刻按键电平, 

key1_value==1 由于上一采样时按键的状态为2,所以判断现在时刻按键处于 松开抖动状态

key1_value==0 由于上一采样时按键的状态为2,所以判断现在时刻按键依然处于按下的状态,由于本次采样距离上次采样间隔20ms(程序运行时间忽略不计),意味着按键按下的时间多了20ms,对变量press_time自增,对应代码press_time+=20

elif last_state==2:
    # 按键仍然处于按下状态
    if key1_value==0:
        press_time+=20
    # 按键位于松开抖动状态
    else:
        now_state=3

第四种情况 :上次采样时按键状态编号为3,对应按键松开抖动状态。

然后比较现在时刻按键的电平与上一采样时刻按键电平, 

key1_value==0 两次采样间隔20ms(忽略程序运行时间),说明抖动是人手长时间按下按键造成的

key1_value==1 判断按键由松开抖动状态转为松开状态,同时停止按键按下的计时

elif last_state==3:
    # 按键由松开抖动状态转到松开状态
    if key1_value==1:
        now_state=4
    # 只是抖动而已
    else:
        now_state=2

 第五种情况:上次采样时按键状态编号为4,对应按键松开状态。

 然后比较现在时刻按键的电平与上一采样时刻按键电平,

key1_value==1 判断按键由松开状态转为闲置状态

elif last_state==4:
    # 按键由松开状态转到闲置状态
    if key1_value==1:
        now_state=0

以上就是根据按键状态 划分的大体框架,接着就需要处理具体的按键事件如单击、双击、长按、超长按、三击、四击。。。

按照一般情况,以单击为基准

双击的第一击按下时间会比单击的按下时间短,具体波形如下

 然后长按时,按键按下的时间比单击按下的时间更长,超长按按下的时间更更更长

所以我们可以得到 t2<t1<t3<t4,我们就可以人为规定3个时间节点区分这4种按键情况。以下时间点是我觉得比较适合我的程序的,你可以根据你的应用场景调节下列值,或者进一步划分为3s长按,5s长按,10s长按等。interval_time变量后面会提到,先不讲。

if press_time<120:
    press_count+=1
    # 初始化间隔计时
    interval_time=0

elif 120<=press_time<=300:
    # print('single click')
    return 1

elif 300<press_time<=1500:
    # print('long click')
    return -1

elif 1500<press_time:
    # print('long long click')
    return -2

 以上代码就可以放在press_time停止计时的地方,也就是按键状态由3转4(松开抖动状态转为松开状态)

到这里我就已经实现了单击、长按、超长按的判断,接下来就是多击了。

我们通过多击的第一次按下时间初步将多击与单击区分,多击另一个很重要的点是:第一次按下后松开后又会以极短的时间按下,你细品。所以我从3转4的瞬间(松开抖动状态转为松开状态)又定义了一个间隔时间变量interval_time,以这个变量的自增作为计时(我用的是20ms定时器,所以自增代码为interval_time+=20)。

在按键处于松开状态时到下一次按下,必然会经历闲置状态,人手按下的速度一般认为不可能快于20ms。所以我们在按键状态4转0(由松开状态转闲置状态)的部分对变量进行自增interval_time+=20,于此同时,在松开按键后,按键还可能一直处于闲置状态,我们在这一部分继续加上变量自增的代码。

根据测试,我们把连击的间隔设置为150ms,这个比较适合我的手速,可以根据需要调慢点会舒服一些。

当按键状态由3转4(松开抖动状态到松开状态),便增加一次按键按下次数press_count+=1

 我们在150ms后对按键按下次数进行检测,如果大于等于2,就输出连击的次数,否则说明没有连击,就对press_count进行清0

if last_state==0:
    # 按键未被按下
    if key1_value==1:
        now_state=0
        if interval_time>150:
            if press_count>=2:
                print(f'{press_count} click')
                # return press_count
            press_count=0
        else:
            interval_time+=20
    # 按键按下且处于抖动期
    else:
        now_state=1

代码

所有代码如下:

from machine import Timer,Pin
key1=Pin(36,Pin.IN,Pin.PULL_UP)
now_state=0
last_state=0
press_count=0
interval_time=0
press_time=0
def key_state(t):
    global last_state,now_state,press_time,press_count,interval_time
    last_state=now_state
    key1_value=key1.value()
    if last_state==0:
        # 按键未被按下
        if key1_value==1:
            now_state=0
            if interval_time>150:
                if press_count>=2:
                    print(f'{press_count} click')
                    # return press_count
                press_count=0
            else:
                interval_time+=20
        # 按键按下且处于抖动期
        else:
            now_state=1
    elif last_state==1:
        # 按键被按下
        if key1_value==0:
            now_state=2
            # 初始化按键按下时间
            press_time=0
        # 只是抖动而已
        else:
            now_state=0
    elif last_state==2:
        # 按键仍然处于按下状态
        if key1_value==0:
            press_time+=20
        # 按键位于松开抖动状态
        else:
            now_state=3
    elif last_state==3:
        # 按键由松开抖动状态转到松开状态
        if key1_value==1:
            now_state=4
            if press_time<120:
                press_count+=1
                # 初始化间隔计时
                interval_time=0
            elif 120<=press_time<=300:
                # return 1
                print('single click')
            elif 300<press_time<=1500:
                print('long click')
                # return -1
            elif 1500<press_time:
                print('long long click')
                # return -2
                
        # 只是抖动而已
        else:
            now_state=2
    elif last_state==4:
        # 按键由松开状态转到闲置状态
        if key1_value==1:
            now_state=0
            interval_time+=20
        # 只是抖动而已
        else:
            pass
    # return
t=Timer(0,period=20,callback=key_state)
while True:
    ...
'''
mpremote mount . run 1.py
'''

再思考

相信通过上面的浏览,你已经完整的了解了用状态去解析按键的过程,但是,这似乎太复杂了,有没有更简单的想法呢?当然有的,且听我慢慢道来。

简单来看,按键就2个状态,空闲 或 按下,当调用1个20ms的定时器,在本轮读取到按键的值存储起来,20ms后再读取按键的值,这20ms的空闲时间,按键的抖动早就消失了,所以,可以利用这个时间差,直接忽略按键的抖动,这样,按键便只有2个状态。

所以,我们只需对这2个状态分类讨论即可。

如何编写代码呢?

首先,导入一些库,定义好一些全局变量,初始化变量

from machine import Pin,Timer
from micropython import const
import asyncio

# 按键的5个状态
idle_state=const(0) # 空闲状态
# press_shaking_state=const(1) # 按下抖动状态
press_state=const(2) # 按下状态
# release_shaking_state=const(3) # 松开抖动状态
# release_state=const(4) # 松开状态

key0=Pin(0,Pin.IN)

timer_call_interval=25 # 定时器的间隔时间
last_key_state=key0.value() # 上一时刻按键的状态
now_key_state=key0.value() # 现在时刻按键的状态
press_time=0 # 按键按下的时间
press_count=0 # 按键连按的次数
interval_time=0 # 按键松开再按下的间隔时间

在函数内定义全局变量,此时处于新周期的开始,now_key_state在上一时刻更新的,所以把now_key_state赋值给last_key_state

然后读取本周期内按键的电平,接着分类讨论

def read_key():
    global press_count,now_key_state,press_time,press_count,timer_call_interval,interval_time
    last_key_state=now_key_state

按键的电平只有2种情况,要么1要么0,上一时刻按键的状态只有2种,要么松开要么按下,于是细分为4种情况。

def read_key():
    global press_count,now_key_state,press_time,press_count,timer_call_interval,interval_time
    last_key_state=now_key_state
    key0_value=key0.value()
    if key0_value==1:

        if last_key_state==idle_state:

        elif last_key_state==press_state:

    elif key0_value==0:

        if last_key_state==idle_state:

        elif last_key_state==press_state:

当读取到按键的电平时,就可以判断按键此时的状态了,于是我们进行更新now_key_state

def read_key():
    global press_count,now_key_state,press_time,press_count,timer_call_interval,interval_time
    last_key_state=now_key_state
    key0_value=key0.value()
    if key0_value==1:
        now_key_state=idle_state
        if last_key_state==idle_state:

        elif last_key_state==press_state:

    elif key0_value==0:
        now_key_state=press_state
        if last_key_state==idle_state:

        elif last_key_state==press_state:

看第1种情况:

上一时刻和本时刻按键均处于空闲状态:进一步分析按键的使用场景,要么是按键一直没被按下,要么是连击之间的间隔,如果是连击,这个间隔时间是很短的;如果是一直没被按下,或者连击结束,这个间隔时间会长一点,所以可以在这里进行判断区分。而此时距离上次的定时器调用差了timer_call_interval(20ms),所以要加上,模拟时间的流逝。还要记得,如果连击结束,需要把连击的次数归0. 这个区分时间我取150ms,可以根据按键型号和按压手感自行微调。

if last_key_state==idle_state:
    interval_time+=timer_call_interval
    if interval_time>=150:
        temp_value=press_count
        press_count=0
        return temp_value

第2种情况:

上一时刻按键处于按下状态,本时刻按键处于松开状态:可能是短按的结束,也可能是长按的结束,长按与短按就区别于按下的时间,根据按下的时间分类判断,我给出了一些参考,可自行修改。另外,短按又分为单击和多击,区别与次数(press_count),所以按键从按下到松开把次数加1;并且,开始间隔时间的计数(intercval_time=0)。

        elif last_key_state==press_state:
            interval_time=0
            if press_time<=250:
                press_count+=1
            else:
                if press_count==0:
                    if 250<press_time<=500:
                        return -1
                    elif 500<press_time<=900:
                        return -2
                    elif 900<press_time<=1600:
                        return -3
                    elif 1600<press_time<=2500:
                        return -4

第3种情况:

上一时刻按键处于空闲状态,本时刻按键处于按下状态:开始记录按键按下的时间

if last_key_state==idle_state:
    press_time=0

第4种情况:

上一时刻和本时刻按键均处于按下状态:记录按下的时间(press_time+=timer_call_interval),由于定时器的周期调用,所以得加上周期时间

elif last_key_state==press_state:
    press_time+=timer_call_interval

代码2

这里给出了所有的代码,可根据自己的需要修改

'''
mpremote run helloworld/key.py
'''
from machine import Pin,Timer
from micropython import const
import asyncio

# 按键的5个状态
idle_state=const(0) # 空闲状态
# press_shaking_state=const(1) # 按下抖动状态
press_state=const(2) # 按下状态
# release_shaking_state=const(3) # 松开抖动状态
# release_state=const(4) # 松开状态

key0=Pin(0,Pin.IN)

timer_call_interval=25 # 定时器的间隔时间
last_key_state=key0.value() # 上一时刻按键的状态
now_key_state=key0.value() # 现在时刻按键的状态
press_time=0 # 按键按下的时间
press_count=0 # 按键连按的次数
interval_time=0 # 按键松开再按下的时间

def read_key():
    global press_count,now_key_state,press_time,press_count,timer_call_interval,interval_time
    last_key_state=now_key_state
    key0_value=key0.value()
    if key0_value==1:
        now_key_state=idle_state
        if last_key_state==idle_state:
            interval_time+=timer_call_interval
            if interval_time>=150:
                temp_value=press_count
                press_count=0
                return temp_value
        elif last_key_state==press_state:
            interval_time=0
            if press_time<=250:
                press_count+=1
            else:
                if press_count==0:
                    if 250<press_time<=500:
                        return -1
                    elif 500<press_time<=900:
                        return -2
                    elif 900<press_time<=1600:
                        return -3
                    elif 1600<press_time<=2500:
                        return -4
    elif key0_value==0:
        now_key_state=press_state
        if last_key_state==idle_state:
            press_time=0
        elif last_key_state==press_state:
            press_time+=timer_call_interval

def f(x):
    key_state=read_key()
    if key_state==-4:
        print('2s long press')
    elif key_state==-3:
        print('1s long press')
    elif key_state==-2:
        print('long long press')
    elif key_state==-1:
        print('long press')
    elif key_state==1:
        print('single press')
    elif key_state==2:
        print('double press')
    elif key_state==3:
        print('triple press')
    elif key_state==4:
        print('four press')
    elif key_state==5:
        print('five press')
async def main():
    t=Timer(0,period=timer_call_interval,callback=f)
    while True:
        await asyncio.sleep(0)

if __name__=='__main__':
    asyncio.run(main())

总结

根据按键的状态分类讨论,这是核心思想,剩下的就是调参数调手感

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值