前一篇写了正在开发中的M60键盘的功耗,这篇就来聊一聊键盘的延迟~html
键盘按键的延迟,即按下按键到电脑响应按键之间的时间差,其影响因素包括:通讯协议限制(USB和蓝牙)、矩阵矩阵扫描方式(周期扫描或者中断检测扫描)、防抖方式、键盘微处理器处理速度、电脑处理速度,甚至键程……python
其中,关键因素是通讯协议限制,如今普遍使用的是USB和蓝牙,其它方式不少也是经过USB转换,一样受到USB限制。android
测键盘的延迟比较难,简化一点,咱们测一测的键盘响应速度,从响应速度大体能够了解键盘的延迟。这里用跑Python的M60键盘在Surface Book上测试,先说结论:ios
M60键盘采用USB链接,能够稳定地每 1.1~1.2ms 处理一个按键事件(按下或释放);而采用低功耗蓝牙链接,则能够大概 3ms 处理一个按键事件,波动相对大。git
USB对延迟的影响
键盘是USB中的标准 HID (Human Input Device) 设备,HID设备采用USB协议的中断传输方式 (Interrupt Transfer),虽然名字中有中断二字,但其实是电脑以大体的周期轮询设备,其中,最小的轮询间隔 (即中断间隔,Interrupt Interval) 可设置为1ms,即最高频率为1000Hz。所以,不少游戏键盘以1000Hz矩阵扫描频率,高了也没多少用。 怎么测键盘的延迟呢?这里设计了一个小实验:github
在Python键盘上,模拟字母a到z依次按下、释放、发送给电脑,在键盘端测量每次按键扫描、发送的处理时间,同时也在电脑端测量a到z按下、释放的时间间隔。app
其中,键盘上的程序是这样的:async
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库,代码以下:ide
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对延迟的影响
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