USB協議和藍牙協議對鍵盤延遲的影響

前一篇寫了正在開發中的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

 

參考

相關文章
相關標籤/搜索