USB协议和蓝牙协议对键盘延迟的影响 - Y1HUI的个人空间 - OSCHINA - 中文开源技术交流社区

前一篇写了正在开发中的M60键盘的功耗,这篇就来聊一聊键盘的延迟~

键盘按键的延迟,即按下按键到电脑响应按键之间的时间差,其影响因素包括:通信协议限制(USB和蓝牙)、矩阵矩阵扫描方式(周期扫描或者中断检测扫描)、防抖方式、键盘微处理器处理速度、电脑处理速度,甚至键程……

其中,关键因素是通信协议限制,现在广泛使用的是USB和蓝牙,其它方式很多也是通过USB转换,同样受到USB限制。

测键盘的延迟比较难,简化一点,我们测一测的键盘响应速度,从响应速度大致可以了解键盘的延迟。这里用跑Python的M60键盘在Surface Book上测试,先说结论:

M60键盘采用USB连接,可以稳定地每 1.1~1.2ms 处理一个按键事件(按下或释放);而采用低功耗蓝牙连接,则可以大概 3ms 处理一个按键事件,波动相对大。

USB对延迟的影响

键盘是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对延迟的影响

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

makerdiary/PYKB

Star 89 | Fork 11

从手焊一个跑Python的USB蓝牙双模键盘,到设计一个Python键盘

issues:

最近提交:

zh-cn 分支: 2021-01-23

源码下载

GITEE.COM

参考


原网址: 访问
创建于: 2021-09-13 11:54:18
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论