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