前言
更新时间:2025.2.3
编程语言:micropython
硬件:esp32s3wroom1u
平台:windows10,mpremote(一个第三方python包,用于操作远程python解释器),vscode
因为近期研发的项目需要用到按键,本代码由本人独立书写,未参考过其他按键处理方法,如有雷同,纯属巧合。需要用到本文章内容的,请标明出处。
本代码使用的是esp32s3wroom1u的GPIO36作为按键检测,设置GPIO45内部上拉。
原理图如下:
思想
按键按下时会在K1端产生如下波形:
观察到按键按下时会经历5个状态:
- 闲置状态:此时按键未被按下,读取到高电平
- 按下抖动状态:此时按键的抖动有可能来自于外界振动或按键按下时引起的抖动
- 按下状态:此时按键被按下,读取到低电平
- 按键松开抖动状态:此时按键的抖动来自于按键松开时的抖动
- 按键松开状态:此时按键被松开,读取到高电平,接着按键进入闲置状态,开始新的轮回
我按照计算机的排序给上面5个状态依次从0编号到4,分别代表一个状态。
每个状态转化都是有过渡的,分为转化前的状态和转化后的状态,或者说,上一时刻状态和当前状态,用两个变量分别描述:last_state,now_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())
总结
根据按键的状态分类讨论,这是核心思想,剩下的就是调参数调手感