前一篇写了正在开发中的M60键盘的功耗,这篇就来聊一聊键盘的延迟~
键盘按键的延迟,即按下按键到电脑响应按键之间的时间差,其影响因素包括:通信协议限制(USB和蓝牙)、矩阵矩阵扫描方式(周期扫描或者中断检测扫描)、防抖方式、键盘微处理器处理速度、电脑处理速度,甚至键程……
其中,关键因素是通信协议限制,现在广泛使用的是USB和蓝牙,其它方式很多也是通过USB转换,同样受到USB限制。
测键盘的延迟比较难,简化一点,我们测一测的键盘响应速度,从响应速度大致可以了解键盘的延迟。这里用跑Python的M60键盘在Surface Book上测试,先说结论:
M60键盘采用USB连接,可以稳定地每 1.1~1.2ms 处理一个按键事件(按下或释放);而采用低功耗蓝牙连接,则可以大概 3ms 处理一个按键事件,波动相对大。
键盘是USB中的标准 HID (Human Input Device) 设备,HID设备采用USB协议的中断传输方式 (Interrupt Transfer),虽然名字中有中断二字,但实际上是电脑以大致的周期轮询设备,其中,最小的轮询间隔 (即中断间隔,Interrupt Interval) 可设置为1ms,即最高频率为1000Hz。因此,很多游戏键盘以1000Hz矩阵扫描频率,高了也没多少用。 怎么测键盘的延迟呢?这里设计了一个小实验:
在Python键盘上,模拟字母a到z依次按下、释放、发送给电脑,在键盘端测量每次按键扫描、发送的处理时间,同时也在电脑端测量a到z按下、释放的时间间隔。
其中,键盘上的程序是这样的:
import time
import usb_hid
from matrix import Matrix
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
matrix = Matrix()
keyboard = Keyboard(usb_hid.devices)
def alphabet_test():
data = []
t = time.monotonic_ns()
for i in range(26):
t1 = time.monotonic_ns()
matrix.scan()
keyboard.press(Keycode.A + i)
t2 = time.monotonic_ns()
matrix.scan()
keyboard.release(Keycode.A + i)
t3 = time.monotonic_ns()
data.append((t2 - t1) // 100000 / 10.)
data.append((t3 - t2) // 100000 / 10.)
average = (time.monotonic_ns() - t) // (26 * 2 * 100000) / 10.
print('average: {}, max: {}, min: {}, data: {}'.format(average, max(data), min(data), data))
while True:
n = matrix.wait(10)
if not n:
continue
keys = [matrix.get() for _ in range(n)]
if keys[0] & 0x80:
continue
alphabet_test()
电脑端使用了keyboard库,代码如下:
import time
import keyboard
data = lambda: None
data.events = None
# {"event_type": "down", "scan_code": 29, "name": "ctrl", "time": 0, "is_keypad": false}
def print_pressed_keys(e):
if e.name == 'a' and e.event_type == 'down':
data.start = time.monotonic_ns()
data.events = [e]
elif data.events:
data.events.append(e)
if e.name == 'z' and e.event_type == 'up':
data.end = time.monotonic_ns()
t = []
for i in range(1, len(data.events)):
dt = data.events[i].time - data.events[i - 1].time
dt = int(dt * 100000) / 100.
t.append(dt)
average = (data.end - data.start) // (len(data.events) * 100000) / 10.
print('average: {}, max: {}, min: {}, data: {}'.format(average, max(t), min(t), t))
data.events = None
keyboard.hook(print_pressed_keys)
keyboard.wait()
这里附上次4次测量数据,需要注意的是,键盘端测量是扫描+发送的处理时间,电脑端是两个按键事件的时间差。
输出4串字母a到z,获得的扫描+发送的处理时间
average: 1.1, max: 1.1, min: 0.9, data: [0.9, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 0.9, 1.0, 0.9, 1.0, 1.0, 1.0, 1.1, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 1.0, 1.0, 1.1, 1.0, 0.9, 0.9, 1.0, 1.0, 0.9, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9]
average: 1.1, max: 1.1, min: 0.9, data: [1.0, 1.0, 1.0, 0.9, 1.0, 0.9, 1.1, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 1.0, 1.0, 0.9, 1.0, 0.9, 0.9, 1.0, 1.0, 0.9, 1.0, 0.9, 1.0, 1.0, 0.9, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 1.1, 1.0, 0.9, 1.0, 0.9, 0.9, 0.9, 1.0, 0.9, 1.0, 1.1, 0.9, 0.9, 1.0, 0.9, 1.0, 0.9]
average: 1.1, max: 1.2, min: 0.9, data: [1.0, 1.0, 1.0, 1.1, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 1.0, 1.1, 0.9, 0.9, 1.0, 1.0, 0.9, 1.0, 0.9, 1.0, 1.0, 1.1, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 1.0, 1.2, 0.9, 1.0, 1.0, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 1.1, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 1.0, 1.0, 0.9]
average: 1.1, max: 1.1, min: 0.9, data: [1.0, 1.0, 1.0, 1.0, 0.9, 1.0, 1.0, 1.0, 1.1, 0.9, 0.9, 1.0, 1.0, 0.9, 1.0, 0.9, 1.0, 1.0, 1.1, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 1.0, 1.0, 0.9, 1.1, 0.9, 0.9, 1.0, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 1.0]
输入4串字母a到z,获得的按键事件时间间隔
average: 1.2, max: 4.02, min: 0.0, data: [3.82, 2.68, 2.0, 4.02, 0.99, 0.99, 1.99, 2.94, 2.99, 0.99, 1.0, 0.99, 1.99, 0.0, 2.0, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.0, 0.99, 0.99, 0.99, 0.0, 0.99, 0.99, 0.0, 0.99, 0.99, 0.0, 0.99, 0.99, 0.0, 0.99, 0.0, 0.99, 0.0, 0.99, 0.99, 1.0, 0.99, 0.99, 1.01, 1.88, 1.0, 0.0, 1.1, 1.0, 0.99]
average: 1.2, max: 2.76, min: 0.0, data: [1.99, 1.99, 1.99, 1.95, 2.0, 0.99, 1.99, 2.76, 1.83, 1.0, 0.99, 0.0, 0.99, 0.99, 1.0, 0.99, 0.99, 0.99, 1.99, 0.99, 1.99, 0.99, 1.99, 1.99, 0.99, 0.99, 0.99, 0.0, 1.0, 0.99, 0.99, 0.0, 0.99, 0.0, 0.99, 0.99, 0.0, 0.99, 0.0, 0.99, 0.99, 1.0, 0.99, 1.0, 0.99, 1.99, 1.89, 1.02, 1.77, 1.45, 0.84]
average: 1.2, max: 3.14, min: 0.0, data: [0.95, 1.98, 3.14, 1.95, 2.02, 2.89, 2.6, 2.11, 1.0, 1.0, 0.99, 0.0, 0.99, 0.99, 0.99, 0.99, 0.0, 0.99, 0.0, 1.67, 0.85, 1.0, 0.0, 0.99, 0.99, 0.0, 0.99, 0.0, 0.99, 1.99, 1.0, 1.19, 1.35, 1.12, 1.0, 0.99, 0.99, 1.0, 0.99, 0.99, 1.0, 1.9, 0.96, 1.0, 0.99, 1.0, 0.98, 2.03, 2.9, 0.99, 1.02]
average: 1.1, max: 4.51, min: 0.0, data: [1.0, 0.99, 2.99, 1.99, 1.92, 1.35, 4.41, 4.51, 0.0, 1.99, 0.99, 0.99, 3.0, 1.98, 0.99, 1.0, 0.99, 1.36, 1.59, 1.0, 0.99, 0.99, 0.99, 1.01, 1.98, 1.98, 1.0, 0.0, 0.99, 0.99, 0.99, 0.0, 0.99, 0.99, 1.99, 0.99, 0.99, 1.99, 0.99, 1.99, 1.01, 2.02, 1.52, 2.63, 1.94, 0.99, 0.0, 0.99, 0.99, 1.11, 1.01]
从数据数据中可以看出,键盘端的数据要稳定一些,电脑端波动大一些,也许是因为Windows系统的非实时性导致获得事件时间不太准确。
接下来,分析一下蓝牙协议对延迟的影响,这里关注的是低功耗蓝牙(Bluetooth Low Energy,BLE)。
BLE在对键盘的支持上借用了USB HID协议,设计了一个叫 HID over GATT 的蓝牙 Profile。在BLE协议中,对延迟影响最大是两个设备连接之后的连接间隔(Connection Interval)。在不同的系统上,最小的连接间隔有所不同,其中,Android上最小为7.5ms;苹果系统上最小可以到11.25ms;Windows上,没用找说明,Surface Book上实测最小为11.25ms。
这里的连接间隔和前面USB HID的中断间隔是不同的,因为蓝牙的一个连接间隔里面可以发几包数据,而最大是几包,这又是因系统而异:Android上可以发6包数据;苹果系统上是4,Windows上结合下面的数据可能是6。
那么,键盘在Android上最快是每秒发 1000 6 / 7.5 = 800 次数据,平均间隔 1.25 ms;苹果系统上,最快是 1000 4 / 11.25 ~= 355 次/s,平均间隔 2.8125 ms;Windows上,可能是 1000 * 6 / 11.25 ~= 533 次/s,平均间隔 1.875 ms。
套用前面的小实验,把USB键盘改成蓝牙键盘测一下,电脑端代码不用变,键盘代码更改如下:
import time
import usb_hid
from matrix import Matrix
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
import adafruit_ble
from adafruit_ble.advertising import Advertisement
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.standard.hid import HIDService
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
ble_hid = HIDService()
advertisement = ProvideServicesAdvertisement(ble_hid)
advertisement.complete_name = 'PyKeyboard'
advertisement.appearance = 961
ble = adafruit_ble.BLERadio()
ble.name = 'PyKeyboard'
if ble.connected:
for c in ble.connections:
c.disconnect()
ble.start_advertising(advertisement)
keyboard = Keyboard(ble_hid.devices)
# keyboard = Keyboard(usb_hid.devices)
matrix = Matrix()
def alphabet_test():
data = []
t = time.monotonic_ns()
for i in range(26):
t1 = time.monotonic_ns()
matrix.scan()
keyboard.press(Keycode.A + i)
t2 = time.monotonic_ns()
matrix.scan()
keyboard.release(Keycode.A + i)
t3 = time.monotonic_ns()
data.append((t2 - t1) // 100000 / 10.)
data.append((t3 - t2) // 100000 / 10.)
average = (time.monotonic_ns() - t) // (26 * 2 * 100000) / 10.
print('average: {}, max: {}, min: {}, data: {}'.format(average, max(data), min(data), data))
while True:
n = matrix.wait(10)
if not n:
continue
keys = [matrix.get() for _ in range(n)]
if keys[0] & 0x80:
continue
if ble.connected:
for c in ble.connections:
if c.connection_interval > 11.25:
c.connection_interval = 11.25
print('ble connection interval {}'.format(c.connection_interval))
else:
continue
alphabet_test()
同样附上4次测量数据~
ble connection interval 11.25
average: 2.7, max: 31.6, min: 1.0, data: [1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 31.6, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.2, 1.5, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.1, 2.1, 2.0, 1.9, 1.0, 1.1, 1.0, 1.1, 1.8, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.2, 23.3, 1.8, 1.2, 1.1, 18.0, 2.1, 2.1, 1.1, 1.0]
ble connection interval 11.25
average: 2.0, max: 19.9, min: 1.0, data: [1.2, 1.1, 1.1, 1.0, 1.1, 1.1, 1.2, 1.5, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.2, 1.3, 11.4, 2.1, 2.1, 1.9, 1.8, 1.4, 1.9, 1.2, 1.1, 1.0, 1.1, 1.0, 1.2, 1.2, 1.1, 1.0, 19.9, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 2.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.2, 1.7, 2.1, 1.4]
ble connection interval 11.25
average: 3.5, max: 36.6, min: 1.0, data: [1.2, 1.1, 1.1, 1.1, 1.2, 1.2, 1.3, 1.0, 1.1, 1.0, 1.1, 27.8, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.2, 1.5, 1.4, 1.0, 1.1, 1.0, 1.1, 1.0, 1.2, 1.0, 1.7, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 36.6, 2.0, 1.1, 1.1, 1.0, 15.9, 2.0, 1.3, 1.0, 6.2, 1.8, 1.1, 30.4, 2.1, 2.2, 2.0, 1.3]
ble connection interval 11.25
average: 2.6, max: 34.3, min: 1.0, data: [1.1, 1.1, 1.1, 1.0, 1.1, 1.2, 1.1, 1.0, 1.1, 1.0, 1.1, 28.1, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.3, 1.5, 2.0, 1.0, 1.1, 1.0, 1.1, 1.0, 1.2, 1.6, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.2, 34.3, 2.0, 2.2, 2.0, 1.8, 1.4, 1.9, 2.0, 2.0, 2.0, 1.1, 1.6, 1.2, 1.0, 1.1, 1.0]
average: 3.9, max: 38.06, min: 0.0, data: [2.53, 8.0, 11.95, 0.99, 0.99, 0.99, 1.0, 0.99, 0.99, 5.16, 0.99, 0.66, 31.19, 1.02, 0.61, 20.85, 1.02, 0.99, 0.99, 1.0, 1.0, 0.99, 0.99, 38.06, 1.02, 0.0, 10.36, 0.0, 1.0, 0.99, 9.1, 1.0, 1.98, 1.0, 0.99, 0.98, 0.99, 0.99, 0.0, 1.0, 0.99, 0.0, 1.13, 1.01, 0.0, 0.98, 1.0, 1.34, 0.0, 29.67, 1.0]
average: 1.5, max: 24.81, min: 0.0, data: [1.95, 0.99, 1.98, 0.0, 0.99, 0.0, 0.99, 0.99, 0.0, 0.99, 0.99, 0.0, 0.99, 0.99, 0.0, 1.0, 0.77, 0.44, 1.7, 1.0, 0.0, 0.99, 0.74, 1.44, 0.0, 6.98, 0.99, 1.0, 0.0, 1.0, 1.98, 6.38, 0.99, 0.0, 0.99, 2.99, 1.0, 0.99, 0.99, 0.99, 0.0, 24.81, 1.02, 1.0, 1.99, 0.99, 1.0, 0.98, 1.79, 0.0, 2.0]
average: 3.3, max: 33.06, min: 0.0, data: [0.95, 1.99, 1.99, 0.98, 0.0, 0.99, 1.4, 1.0, 18.18, 1.02, 0.0, 0.99, 9.6, 0.0, 1.0, 33.06, 0.0, 0.99, 1.0, 0.0, 0.99, 1.0, 0.99, 0.99, 0.0, 0.99, 0.0, 1.0, 25.08, 1.0, 0.99, 0.0, 1.09, 30.51, 22.63, 1.0, 0.0, 0.99, 0.99, 1.99, 0.99, 0.0, 0.99, 1.01, 0.0, 0.0, 2.12, 0.86, 1.0, 0.99, 0.73]
average: 2.7, max: 62.49, min: 0.0, data: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 15.63, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 47.03, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 62.49, 0.0, 0.0, 0.0, 15.44]
从数据中,可以看出通过蓝牙连接,延迟波动比较大,最大延迟可达 62.49 ms,最小可到 1 ms(忽略数据中的0)。由于蓝牙键盘延迟不确定,最大延迟较大,看起来不太适合用来玩实时性要求很高的游戏。而日常打字敲代码没啥问题。 键盘端和电脑端的数据有差异,不过在同一个数量级,能大致反应出键盘的延迟。
如果你对键盘感兴趣,可以关注笔者正在设计的开源键盘项目 python-keyboard
从手焊一个跑Python的USB蓝牙双模键盘,到设计一个Python键盘
Yihui Xiong 2021-01-23 20:47
Yihui Xiong 2020-10-30 11:19
Yihui Xiong 2020-10-22 10:50
zh-cn 分支: 2021-01-23
原网址: 访问
创建于: 2021-09-13 11:54:18
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论