QMK und Kinesis Advantage: Selbst die Tastatur-Latenz messen - und reduzieren

Dieser Text ist eine Übersetzung. Das Original ist im Blog von Michael Stapelberg(öffnet im neuen Fenster) zu finden.
Ich habe in den letzten Jahren an einigen Projekten zum Thema Eingabelatenz bei Tastaturen gearbeitet. Konkret geht es mir dabei um Austauschcontroller für die Advantage-Produktreihe der Firma Kinesis. 2018 habe ich den Kinx-Tastaturcontroller mit 0,2 ms Eingabelatenz(öffnet im neuen Fenster) vorgestellt und 2020 dann den Kint-Tastaturcontroller(öffnet im neuen Fenster) . Letzterer ist mit verschiedenen Teensy-Mikrocontrollern und sowohl mit der alten KB500- als auch mit der neueren KB600-Revision der Kinesis Advantage kompatibel, die intern andere Steckverbindungen nutzt, um die Tastenblöcke anzuschließen.
Während der Kinx-Controller von 2018 über eine integrierte Latenzmessung verfügte, begann ich mit dem Kint-Design bei null, da ich dieses Mal lieber die QMK-Tastaturfirmware anstelle einer selbst entwickelten Firmware verwenden wollte. Das hat mich zum Nachdenken gebracht: Gibt es eine Möglichkeit, die Latenzzeiten extern zu messen, idealerweise ganz ohne Änderungen an der Firmware?
In diesem Artikel erkläre ich, wie man eine Messumgebung einrichtet, mit der man die Eingabelatenz eines Tastaturcontrollers messen kann und die sowohl für originale als auch für selbst gebaute Controller geeignet ist. Ich verwende als Beispiel meine Kinesis-Advantage-Tastatur, doch dieser Ansatz sollte sich auf alle Tastaturen übertragen lassen.
Außerdem erkläre ich einige häufige Ursachen für lange Latenzzeiten bei der Tastatureingabe und zeige, wie diese in der QMK-Tastatur-Firmware behoben werden können.
So ist die Messung aufgebaut
Ich simuliere mit einem Teensy 4.0(öffnet im neuen Fenster) das Drücken der Feststelltaste und messe die Dauer, bis der Tastendruck zu einem Wechsel der Feststelltasten-LED führt. Ich habe mich dabei für die Feststelltaste entschieden, weil sie eine der wenigen Tasten ist, die einen LED-Wechsel bewirkt.





QMK-Debug-Konsole aktivieren
Bereiten wir nun die QMK-Arbeitskopie vor. Ich arbeite für jedes Projekt gerne mit einer separaten QMK-Arbeitskopie:
% docker run -it -v $PWD:/usr/src archlinux # pacman -Sy && pacman -S qmk make which diffutils python-hidapi python-pyusb # cd /usr/src # qmk clone -b develop qmk/qmk_firmware $PWD/qmk-input-latency # cd qmk-input-latency
Die Firmware für meine Tastatur kompiliere ich folgendermaßen:
# make kinesis/kint36:stapelberg
Um die Debug-Konsole zu aktivieren, muss ich meine QMK-Keymap Stapelberg bearbeiten, indem ich die Datei keyboards/kinesis/keymaps/stapelberg/rules.mk wie folgt aktualisiere:
CONSOLE_ENABLE = yes
Nachdem die Firmware kompiliert und geflasht ist, erkennt das Tool hid_listen das Gerät und wartet auf QMK-Debug-Meldungen:
% sudo hid_listen Waiting for device:... Listening:
Die Pins identifizieren
Jetzt müssen wir die Zeile und Spalte der Feststelltaste in unserer Tastaturmatrix ausfindig machen.
Wir können QMK dazu bringen, nach jedem Scan anzuzeigen, welche Tasten erkannt wurden, indem wir in keyboards/kinesis/keymaps/stapelberg/keymap.c den folgenden Code einfügen:
void keyboard_post_init_user() { debug_config.enable = true; debug_config.matrix = true; }Jetzt sehen wir in der Ausgabe von hid_listen , welche Taste beim Drücken der Feststelltaste aktiv ist:
r/c 01234567 00: 00100000 01: 00000000 [...]
Bei unserem Kint-Controller liegt die Feststelltaste in der QMK-Matrix auf Zeile 0, Spalte 2. Im Kint-Schaltplan(öffnet im neuen Fenster) lauten die entsprechenden Signale ROW_EQL und COL_2 .
Um den Teensy-4.0-Latenzmessungstreiber anzuschließen, stelle ich die folgenden GPIO-Verbindungen zu den Tastaturcontrollern Kint36, Kint41 oder Kint2pp (mit Spannungswandler!) her:
| Treiber 4.0 | Signal | kint36, kint41 | kint2pp (5V!) |
| GND | GND | GND | GND |
| Pin 10 | ROW_EQL | Pin 8 | D7 |
| Pin 11 | COL_2 | Pin 15 | F7 |
| Pin 12 | LED_CAPS_LOCK | Pin 12 | C1 |
Hinweis: Leider sind die Signale auf dem Teensy 4.x Dev-Kit NXP i.MX RT1060 Evaluierungskit(öffnet im neuen Fenster) (MIMXRT1060-EVK) nicht verfügbar. Hier wird Pin 8 (B1_00) stattdessen von der LVDI-Schnittstelle belegt.
Reaktion der Caps-Lock-LED beschleunigen
Wenn der Host der Tastatur signalisiert, dass die Feststelltaste aktiv ist, setzt die QMK-Firmware zunächst im USB Interrupt Handler ein Interrupt Flag. Der LED-Pin wird jedoch erst nach Abschluss des nächsten Matrixscans aktualisiert.
Für den normalen Gebrauch ist das absolut in Ordnung. Die Messwerte werden jedoch präziser, wenn der LED-Pin sofort aktualisiert wird. Das lässt sich mit set_led_transfer_cb in tmk_core/protocol/chibios/usb_main.c machen, welches vom USB Interrupt Handler aufgerufen wird:
#include "gpio.h"
static void set_led_transfer_cb(USBDriver *usbp) {
if (usbp->setup[6] == 2) { /* LSB(wLength) */
uint8_t report_id = set_report_buf[0];
if ((report_id == REPORT_ID_KEYBOARD) || (report_id == REPORT_ID_NKRO)) {
keyboard_led_state = set_report_buf[1];
}
} else {
keyboard_led_state = set_report_buf[0];
}
if ((keyboard_led_state & 2) != 0) {
writePinLow(C7); // turn on CAPS_LOCK LED
} else {
writePinHigh(C7); // turn off CAPS_LOCK LED
}
}
Host-Einstellungen (Linux)
Auf dem USB-Host, das heißt dem Linux-Rechner, wechsle ich zu einem Virtuellen Terminal(öffnet im neuen Fenster) (VT), indem ich meinen Log-in-Manager stoppe (und damit meine aktuelle grafische Sitzung beende!):
% sudo systemctl stop gdm
Ist das Virtuelle Terminal aktiv, wird das Drücken der Feststelltaste vollständig im Kernel-Treibercode verarbeitet, ohne einen Umweg über den Userspace zu nehmen. Das können wir überprüfen, indem wir mit bpftrace(8)(öffnet im neuen Fenster) Stacktraces sammeln, wenn der Kernel die Funktion kbd_event in drivers/tty/vt(öffnet im neuen Fenster) ausführt:
% sudo bpftrace -e 'kprobe:kbd_event { @[kstack] = count(); }'
Nachdem wir die Feststelltaste gedrückt und den bpftrace-Prozess abgebrochen haben, sollte ein Stacktrace zu sehen sein.
Ich habe dann die grundlegende Ende-zu-Ende-Latenz gemessen - mithilfe meiner measure-fw-Firmware(öffnet im neuen Fenster) auf dem FRDM-K66F-Eval-Kit, einem billigen und leicht erhältlichen USB-2.0-High-Speed-Gerät. Die Firmware misst die Latenzzeit zwischen einem Tastendruck und der USB-HID-Meldung für die LED der Feststelltaste, allerdings ohne zusätzliche Verzögerung durch den Matrixscan oder Ähnliches:
% cat /dev/ttyACM0 sof=74 μs report=393 μs sof=42 μs report=512 μs sof=19 μs report=512 μs sof=39 μs report=488 μs sof=20 μs report=518 μs sof=90 μs report=181 μs sof=42 μs report=389 μs sof=7 μs report=319 μs
Eine schnellere Reaktion können wir aus diesem Computer nicht herausholen. Alles, was dazukommt (z. B. X11, Anwendungen), macht das Ganze langsamer. Dieser Wert stellt also die untere Grenze dar.
Code für die Simulation von Tastendrücken und Durchführung von Messungen
Ich verwende den Arduino-Sketch latencydriver(öffnet im neuen Fenster) ; die Arduino IDE muss so konfiguriert sein:
Teensy 4.0 (USB Type: Serial, CPU Speed: 600 MHz, Optimize: Faster)
Die Pins werden im Messtreiber Teensy 4.0 wie folgt eingerichtet:
void setup() {
Serial.begin(115200);
// Connected to kinT pin 15, COL_2
pinMode(11, OUTPUT);
digitalWrite(11, HIGH);
// Connected to kinT pin 8, ROW_EQL.
// Pin 11 will be high/low in accordance with pin 10
// to simulate a key-press, and always high (unpressed)
// otherwise.
pinMode(10, INPUT_PULLDOWN);
attachInterrupt(digitalPinToInterrupt(10), onScan, CHANGE);
// Connected to the kinT LED_CAPS_LOCK output:
pinMode(12, INPUT_PULLDOWN);
attachInterrupt(digitalPinToInterrupt(12), onCapsLockLED, CHANGE);
}
Damit eine Taste als gedrückt erkannt wird, müssen wir in der Tastaturmatrix ( hier mehr zur Tastaturmatrix(öffnet im neuen Fenster) ) die Spalte mit der Zeile verbinden - aber nur, wenn die Spalte gescannt wird. Das machen wir über den Interrupt Handler wie folgt:
bool simulate_press = false;
void onScan() {
if (simulate_press) {
// connect row scan signal with column read
digitalWrite(11, digitalRead(10));
} else {
// always read not pressed otherwise
digitalWrite(11, HIGH);
}
}
Jetzt können wir über unser Textinterface eine Messung starten:
caps_lock_on_to_off = capsLockOn();
Serial.printf("# Caps Lock key pressed (transition: %s)\r\n",
caps_lock_on_to_off ? "on to off" : "off to on");
simulate_press = true;
t0 = ARM_DWT_CYCCNT;
emt0 = 0;
eut0 = 0;
Beim nächsten Scan der Tastaturmatrix wird die gedrückte Taste erkannt und ein HID-Report an das Betriebssystem gesendet. Sobald das Betriebssystem mit seinem HID-Report antwortet, der den Status der Caps-Lock-LED enthält, beendet unser Caps-Lock-LED-Interrupt-Handler die Messung:
void onCapsLockLED() {
const uint32_t t1 = ARM_DWT_CYCCNT;
const uint32_t elapsed_millis = emt0;
const uint32_t elapsed_micros = eut0;
uint32_t elapsed_nanos = (t1 - t0) / cycles_per_ns;
Serial.printf("# Caps Lock LED (pin 12) is now %s\r\n", capsLockOn() ? "on" : "off");
Serial.printf("# %u ms == %u us\r\n", elapsed_millis, elapsed_micros);
Serial.printf("BenchmarkKeypressToLEDReport 1 %u ns/op\r\n", elapsed_nanos);
Serial.printf("\r\n");
}
Messungen durchführen
Schließen wir den Teensy 4.0 an den Computer an und öffnen die serielle USB-Konsole:
% screen /dev/ttyACM0 115200
Es sollte die folgende Willkommensnachricht erscheinen:
# kinT latency measurement driver # t - trigger measurement
Die Tastenkombination C-a H veranlasst screen , die Ausgaben in die Datei screenlog.0 zu schreiben. So können wir die Messergebnisse abspeichern. Jetzt drücken wir ein paar Mal t , um mehrere Messungen auszulösen, und schließen screen mit C-a k . Die Ergebnisse der Messungen können wir dann mit Benchstat(öffnet im neuen Fenster) zusammenfassen:
% benchstat screenlog.0 name time/op KeypressToLEDReport 1.82ms ±20%
Scan-to-Scan-Verzögerung
Die Messwertausgabe auf der seriellen USB-Konsole enthält auch die Scan-to-Scan-Verzögerung der Matrix:
# scan-to-scan delay: 422475 ns
Bei jedem Scan der Tastaturmatrix wird jede Zeile einzeln geschaltet und dann alle Spalten gelesen. Das bedeutet, dass ROW_EQL bei jedem Scan der Matrix einmal auf high und dann wieder auf low gesetzt wird. Der Teensy 4.0 misst die Scan-to-Scan-Verzögerung, indem er das Intervall zwischen den Aktivierungen von ROW_EQL misst.
Diesen Ansatz können wir überprüfen, indem wir QMK dazu bringen, seine Scanrate selbst zu melden. Um das zu tun, müssen wir die Debug-Option für die Matrixscan-Rate unter keyboards/kinesis/keymaps/stapelberg/config.h wie folgt aktivieren:
#pragma once
#define DEBUG_MATRIX_SCAN_RATE
Mit hid_listen erhalten wir nun die folgenden QMK-Debug-Meldungen:
% sudo hid_listen Waiting for new device:.. Listening: matrix scan frequency: 2300 matrix scan frequency: 2367 matrix scan frequency: 2367
Eine Matrixscan-Rate/-Frequenz von 2367 Scans pro Sekunde entspricht 422 μs pro Scan:
1000000 μs / 2367 scans/second = 422μs
Eine weitere Möglichkeit, den Ansatz zu überprüfen, besteht darin, eine End-to-End-Messung mit einer einzeiligen Änderung im QMK-Tastaturcode kurzzuschließen:
bool process_action_kb(keyrecord_t *record) {
#define LED_CAPS_LOCK LINE_PIN12
#define ledTurnOn writePinLow
ledTurnOn(LED_CAPS_LOCK);
return true;
}
Wiederholt man die Messungen nun, erhält man:
% benchstat screenlog.0 name time/op KeypressToLEDReport 693µs ±26%
Dieser Wert liegt zwischen 0 und dem doppelten Matrixscan-Intervall (2 x 422μs), da es passieren kann, dass eine Taste gedrückt wird, nachdem sie bereits vom laufenden Matrixscan abgetastet wurde. In diesem Fall muss sie warten, bis der nächste Scan abgeschlossen ist (!), bevor sie als gedrückt registriert werden kann.
Tastaturcontroller verbinden
Nachdem wir nun unsere allgemeine Messumgebung eingerichtet haben, ist es an der Zeit, unseren Teensy 4.0 an verschiedene Tastaturcontroller anzuschließen!
Kint36, Kint41: GPIO
Nicht auf einer Platine verlötete Mikrocontroller lassen sich ganz simpel für die Messung einrichten: Wir müssen einfach alle GPIO-Pins direkt an den Teensy-4.0-Latenztesttreiber anschließen! Das habe ich beim Kint36 und Kint41 so gemacht:





Kint2pp: 5V
Da der Teensy++ einen 5-V-Logikpegel verwendet, müssen wir die Pegel von/auf 3,3 V konvertieren. Das geht zum Beispiel mit dem Sparkfun Logic Level Converter(öffnet im neuen Fenster) (bidirektional) auf einem Breadboard ganz einfach:





Kinx: FPC
Was, wenn der Mikrocontroller nicht eigenständig ist, sondern auf einer Tastaturcontroller-Platine verlötet ist, wie zum Beispiel bei meinem früheren Kinx-Controller?
Dann kann man einen FPC-Stecker ( Molex 39-53-2135(öffnet im neuen Fenster) ) verwenden und eine Drahtbrücke an die Pins für COL_2 und ROW_EQL löten. Für Caps Lock und Ground habe ich Schaltdrähte auf die Platine gelötet:





Hinweis: Der KB500-Controller-auf-KB600-Adapter(öffnet im neuen Fenster) kann für diesen Zweck leider nicht verwendet werden, da die benötigten Pins auf Masse liegen.
Originaler Kinesis-Controller
Was tun, wenn man Schaltdrähte lieber nicht direkt auf die Platine lötet?
Die am wenigsten invasive Methode besteht darin, den FPC-Steckverbinder anzuschließen und während der Messung Tastköpfe an die Kontakte zu halten:





QMK-Eingabelatenz
Nun, da die Messhardware eingerichtet ist, widmen wir uns dem Code. In den folgenden Abschnitten bespreche ich verschiedene Faktoren, die die Eingabelatenz beeinflussen können.
Eager-Entprellung
Wenn eine Taste gedrückt wird, erzeugt sie kein sauberes Signal, sondern schaltet sich wellenartig mehrfach ein und aus. Das Beseitigen dieser ungewollten Signale nennt sich Entprellung(öffnet im neuen Fenster) oder auch Debouncing. Jede Tastaturfirmware macht das.
In der Dokumentation von QMK zur Entprellungs-API(öffnet im neuen Fenster) werden die Unterschiede zwischen den verschiedenen Entprellungsansätzen gut verständlich dargestellt.
Der Standard-Entprellungsalgorithmus sym_defer_g von QMK ist vorsichtig gewählt. Ich kenne die Kriterien nicht genau, die festlegen, ob Tasten unter Rauschen leiden und daher den sym_defer_g -Algorithmus benötigen. Aber ich weiß, dass Schalter mit Dioden, wie sie in der Kinesis Advantage verwendet werden, kein Rauschen haben und daher auch die anderen Entprellungsalgorithmen geeignet sind.
Der Standard-Entprellungsalgorithmus sym_defer_g ist zwar robust, verursacht aber auch eine zusätzliche Eingabelatenz von 5 ms:
% benchstat screenlog-kint36.0 name time/op KeypressToLEDReport 7.61ms ± 8%
Um die Eingabelatenz zu verringern, benötigen wir einen eifrigen eager-Algorithmus. Ich habe mich für den Entprellungsalgorithmus sym_eager_pk entschieden und ihn zu meiner Datei keyboards/kinesis/kint36/rules.mk hinzugefügt:
DEBOUNCE_TYPE = sym_eager_pk
Jetzt sind die zusätzlichen 5 ms weg:
% benchstat screenlog-kint36-eager.0 name time/op KeypressToLEDReport 2.12ms ±16%
Ein Beispiel-Change findet sich hier(öffnet im neuen Fenster) .
Schnelleres USB-Abtastintervall
Der USB-Host (Computer) unterteilt Zeit in Segmente fester Länge, die Frames genannt werden:
- USB Full Speed (USB 1.0) verwendet Frames mit einer Dauer von jeweils 1 ms.
- USB High Speed (USB 2.0) führt Microframes ein, die 125 μs betragen.
Jedes USB-Gerät gibt in seinem Gerätedeskriptor an, wie häufig das Gerät abgefragt werden soll (in Frames). Die schnellste Abtastrate(öffnet im neuen Fenster) für USB 1.0 beträgt 1 Frame, das heißt, das Gerät kann Daten nach höchstens 1 ms senden. Ähnlich verhält es sich bei USB 2.0: Hier beträgt die Abfragerate 1 Microframe, das heißt Daten werden alle 125 μs gesendet.
Eine schnellere Abtastrate bedeutet natürlich auch, dass Ressourcen auf dem USB-Bus belegt werden, die dann für andere Geräte nicht mehr zur Verfügung stehen. Bei größeren USB-Hubs kann das bedeuten, dass weniger Geräte gleichzeitig verwendet werden können. Die Einzelheiten dieser Einschränkung hängen aber auch von einer Reihe anderer Faktoren ab. Zusätzlich zur Abtastrate spielen die maximale Paketgröße und die Anzahl der Endpunkte eine Rolle.
Ich spreche hier allerdings nur von der gleichzeitigen Gerätenutzung, nicht von der Beanspruchung der Bandbreite: Die Geschwindigkeit der Bulk Transfers von USB-Massenspeichergeräten wird in meinen Tests nicht beeinträchtigt. Ich erreiche mit und ohne Kint41-USB-2.0-High-Speed-Controller mit bInterval=1 etwa 37 MiB/s.
Selbst wenn ich zwei Kint41-Controller gleichzeitig anschließe, bleiben immer noch genug Ressourcen übrig, um eine Logitech-C920-Webkamera mit höchster Auflösung und größtem Format (das heißt mit maximaler Beanspruchung der Bandbreite) einzusetzen. Zum Vergleich: Wenn ich statt meiner zwei kint41-Controller nur einen LPC-Link2-Debug-Tester von NXP anschließe, reduziert die Logitech C920 ihre Auflösung.
Noch offen: Würde die Angabe mehrerer alternativer Einstellungen in unserem USB-Gerätedeskriptor den Ressourcenverbrauch dynamisch verringern? Unsere Tastatur könnte etwa eine alternative Einstellung mit bInterval=1 und eine mit bInterval=10 anbieten. Um das konfigurierte Intervall anzuzeigen, stellt der Linux-Kernel eine Debug-Pseudodatei zur Verfügung:
% sudo cat /sys/kernel/debug/usb/devices [...] T: Bus=01 Lev=02 Prnt=09 Port=02 Cnt=02 Dev#= 53 Spd=480 MxCh= 0 D: Ver= 2.00 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=64 #Cfgs= 1 P: Vendor=1209 ProdID=345c Rev= 0.01 S: Manufacturer="https://github.com/stapelberg" S: Product="kinT (kint41)" C:* #Ifs= 3 Cfg#= 1 Atr=a0 MxPwr=500mA I:* If#= 0 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=01 Driver=usbhid E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=125us I:* If#= 1 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=00 Prot=00 Driver=usbhid E: Ad=82(I) Atr=03(Int.) MxPS= 32 Ivl=125us I:* If#= 2 Alt= 0 #EPs= 2 Cls=03(HID ) Sub=00 Prot=00 Driver=usbhid E: Ad=83(I) Atr=03(Int.) MxPS= 32 Ivl=125us E: Ad=04(O) Atr=03(Int.) MxPS= 32 Ivl=125us [...]
Alternativ kann man den USB-Gerätedeskriptor zum Beispiel mit sudo lsusb -v -d 1209:345c anzeigen lassen und die bInterval -Einstellung selbst interpretieren. Die obige Ausgabe zeigt den besten Fall: ein USB-2.0-High-Speed-Gerät (Spd=480) mit bInterval=1 im Gerätedeskriptor (Iv=125us).
Der originale Kinesis-Advantage-2-Tastaturcontroller (KB600) verwendet zwar USB 2.0, jedoch im Full-Speed-Modus (Spd=12), und ist daher nicht schneller als USB 1.1. Außerdem ist hier bInterval=10 angegeben, was einem Abfrageintervall von 10 ms entspricht (Ivl=10ms):
T: Bus=01 Lev=02 Prnt=09 Port=02 Cnt=02 Dev#= 52 Spd=12 MxCh= 0 D: Ver= 2.00 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=64 #Cfgs= 1 P: Vendor=29ea ProdID=0102 Rev= 1.00 S: Manufacturer=Kinesis S: Product=Advantage2 Keyboard C:* #Ifs= 3 Cfg#= 1 Atr=a0 MxPwr=100mA I:* If#= 0 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=02 Driver=usbhid E: Ad=83(I) Atr=03(Int.) MxPS= 8 Ivl=10ms I:* If#= 1 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=01 Driver=usbhid E: Ad=84(I) Atr=03(Int.) MxPS= 8 Ivl=2ms I:* If#= 2 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=00 Prot=00 Driver=usbhid E: Ad=85(I) Atr=03(Int.) MxPS= 8 Ivl=2ms
Meine Empfehlung:
- Bei USB 1.1 Full Speed sollte unbedingt bInterval=1 angegeben werden. Mir sind keine Nachteile bekannt.
- Auch bei USB 2.0 High Speed ist bInterval=1 meiner Meinung nach eine gute Wahl, allerdings bin ich mir hier nicht ganz so sicher. Wenn Probleme auftreten, dann reduziert auf bInterval=3 und schickt mir eine Nachricht.
Einzelheiten zur Messung siehe Anhang B: USB-Abtastintervall (Geräteseite(öffnet im neuen Fenster) ). Ein Beispiel-Change ist hier(öffnet im neuen Fenster) .
Schnellerer Matrixscan
Der Zweck eines Tastaturcontrollers ist es, nach dem Scannen der Tastenmatrix gedrückte Tasten zu melden. Je mehr Scans ein Tastaturcontroller pro Sekunde durchführen kann, desto schneller kann er auf einen Tastendruck reagieren.
Wie viele Scans der Controller durchführt, hängt von mehreren Faktoren ab:
- Taktrate des Mikrocontrollers. Es lohnt sich zu prüfen, ob das Mikrocontrollermodell höhere Taktraten unterstützt, oder die Tastatur gleich mal auf ein schnelleres Modell aufzurüsten. Die Grenze, ab der sich ein Wechsel nicht mehr lohnt, liegt vermute ich bei ≈100 MHz. Vergleicht man zum Beispiel den Kint36 mit 120 MHz mit dem Kint36 mit 180 MHz, so beträgt der Unterschied von Scan zu Scan 5 μs.
- Wie viel anderen Code die Firmware neben dem Matrixscan ausführt. Ich empfehle, Zusatzfunktionen in QMK oder sogar selbst geschriebenen Code für die Messung zu deaktivieren.
- Ob die Scans hintereinander oder zum Beispiel synchronisiert mit USB-Start-of-Frame-Interrupts durchgeführt werden. QMK führt Scans hintereinander durch, daher ist dieser Punkt nur bei anderen Firmwares relevant.
- Wie lange Entprellzeit eingestellt ist. Eine Verkürzung der Pause erhöht die Scans pro Sekunde, ist die Sleep-Funktion allerdings zu kurz eingestellt, erkennt das Gerät Geistertastendrücke. Mehr dazu im nächsten Abschnitt über Kürzere Pausen.
Einzelheiten zur Messung können im Abschnitt zur Scan-to-Scan-Verzögerung(öffnet im neuen Fenster) weiter oben nachgelesen werden.
Ich habe auch versucht, die GPIOs schneller zu konfigurieren, um herauszufinden, ob dies die erforderliche Verzögerung nach Loslassen der Taste reduzieren würde, doch leider gab es keinen Unterschied zwischen der Standardeinstellung und der schnellsten Einstellung: Drive Strength 6 (DSE=6), schnelle Slew-Rate (SRE=1), 200 MHz (Speed=3).
Kürzere Pausen
QMK nutzt im Matrixscan-Code die Funktion chThdSleepMicroseconds von ChibiOS(öffnet im neuen Fenster) . Leider ist die kürzeste Sleep-Dauer dieser Funktion mit 1 ChibiOS-Tick immer noch relativ lang: Die minimale Sleep-Dauer beträgt 100µs, selbst wenn wir eine geringere Dauer einstellen. Das ist bei Controllern wie dem Kint41 ein Problem - hier sollte die Pause nur 10 μs betragen.
Die Länge eines ChibiOS-Ticks hängt davon ab, wie der ARM-SysTick-Timer auf dem von uns verwendeten Mikrocontroller eingestellt ist. Man könnte den SysTick-Timer zwar so konfigurieren, dass er häufiger auslöst, allerdings ist das nicht ratsam: chSysTimerHandlerI() muss in weniger als einem Tick ausführbar sein(öffnet im neuen Fenster) .
Stattdessen finde ich es einfacher, durch aktives Warten (busy waiting) kurze Verzögerungen zu implementieren, bis das ARM Cycle Counter Register (CYCCNT) anzeigt, dass genügend Zeit vergangen ist. Hier ist ein Beispiel aus keyboards/kinesis/kint41/kint41.c :
// delay_inline sleeps for |cycles| (e.g. sleeping for F_CPU will sleep 1s).
//
// delay_inline assumes the cycle counter has already been initialized and
// should not be modified, i.e. is safe to call during keyboard matrix scan.
//
// ChibiOS enables the cycle counter in chcore_v7m.c.
static void delay_inline(const uint32_t cycles) {
const uint32_t start = DWT->CYCCNT;
while ((DWT->CYCCNT - start) < cycles) {
// busy-loop until time has passed
}
}
void matrix_output_unselect_delay(void) {
// 600 cycles at 0.6 cycles/ns == 1μs
const uint32_t cycles_per_us = 600;
delay_inline(10 * cycles_per_us);
}
Natürlich hängt der Wert cycles/ns von der Frequenz ab, mit der unser Mikrocontroller läuft. Dieser Code muss daher für jede Plattform angepasst werden.
Ergebnisse
Wie schneiden die verschiedenen Kinesis-Tastaturcontroller im Vergleich ab, wenn in der QMK-Tastatur-Firmware die niedrigste Eingabelatenz konfiguriert ist? Ich habe die folgenden Werte gemessen:
| Modell | CPU-Geschwindigkeit | USB-Abfrageintervall | Scan-to-Scan | Scanrate | Caps-to-Report |
|---|---|---|---|---|---|
| kint41 | 600 MHz | 125 μs | 181 μs | 5456 Scans/s | 930 µs ±17% |
| kinX | 120 MHz | 125 μs | 213 μs | 4694 Scans/s | 953 µs ±15% |
| kint36 | 180 MHz | 1000 μs | 444 μs | 2252 Scans/s | 1,97 ms ±15% |
| kint2pp | 16 MHz | 1000 μs | 926 μs | 1078 Scans/s | 3,27 ms ±32% |
| original | 60 MHz | 10000 μs | 1936 μs | 516 Scans/s | 13,6 ms ±21% |
Die erforderlichen Änderungen, um diese Ergebnisse zu erhalten, sind seit QMK 0.12.38 (2021-04-20) enthalten.
Die Kint41-Unterstützung(öffnet im neuen Fenster) mit allen erforderlichen Änderungen ist in Arbeit, wird aber kommen.
In den folgenden Abschnitten werden die Ergebnisse im Detail erläutert.
Kint41
Es freut mich, dass der neueste Teensy-4.1-Mikrocontroller die Führung übernimmt! Der Kinx-Controller erreicht zwar ähnliche Werte, doch er war relativ schwierig zu bauen, so dass ihn nur wenige Leute benutzten.
Die wichtigste Verbesserung gegenüber dem Teensy 3.6 ist, dass jetzt USB 2.0 High Speed verfügbar ist; die hohe Taktrate von 600 MHz ermöglicht einen noch schnelleren Matrixscan.
Kinx
In meinem früheren Artikel über den Kinx-Controller(öffnet im neuen Fenster) habe ich die Scan-Verzögerung von Kinx mit ≈100 μs gemessen. Während meiner Arbeit an diesem Artikel habe ich festgestellt, dass dieser Wert irreführend war: Der Code, der die Messungen durchführte, schaltete Interrupts aus, um nur die Scan-Funktion zu messen. Das ist zwar technisch korrekt, in der Praxis aber nicht sinnvoll, da Interrupts nicht deaktiviert werden sollen(öffnet im neuen Fenster) . Die Scanfunktion wird so häufig unterbrochen, dass wir einen Wert von ≈208 μs erreichen.
Ich habe außerdem das USB-Abfrageintervall in der Kinx-Firmware korrigiert, da es nicht auf bInterval=1(öffnet im neuen Fenster) eingestellt war.
Originaler Kinesis
Der Original-Tastaturcontroller, mit dem die Kinesis-Advantage-2-Tastatur (KB600) ausgestattet ist, verwendet einen AT32UC3B0256-Mikrocontroller(öffnet im neuen Fenster) , der mit 60 MHz getaktet ist. Hier liegt die gemessene Eingabelatenz viel höher als selbst beim langsamsten Kint-Controller (Kint2pp mit 16 MHz). Was ist da los?
Ohne Zugang zur Firmware können wir auf Folgendes schließen:
1. Kinesis scheint einen eager-Entprellungsalgorithmus(öffnet im neuen Fenster) einzusetzen (das ist gut!), andernfalls würden wir eine noch höhere Latenz beobachten.
2. Das USB-Abfrageintervall (öffnet im neuen Fenster) (bInterval=10) ist übermäßig hoch eingestellt, vor allem weil USB Full Speed mit längeren USB-Frames verwendet wird. Ich würde dem Hersteller empfehlen, es auf bInterval=1 zu reduzieren. Sie könnten damit bis zu 10 ms geringere Eingabelatenzen erreichen!
3. Die Matrixscan-Rate ist nur halb so schnell wie bei meinem Kint2pp. Ich bin mir nicht sicher, warum das so ist. Vielleicht macht die Firmware zwischen den Matrixscans noch eine Menge anderer Dinge.
Bitte beachtet jedoch, dass wir die unter Reaktion der Caps-Lock-LED beschleunigen beschriebene Firmware-Änderung nicht auf den originalen Controller anwenden konnten, weshalb die Messabweichung ±21 % beträgt. Diese Abweichung beinhaltet ± 1,9 ms für den Abschluss eines Matrixscans, bevor der LED-Status aktualisiert wird.
Fazit
Nach Analyse der verschiedenen Controller in meiner Messumgebung glaube ich, dass die folgenden Faktoren, geordnet nach Relevanz, die größte Rolle bei der Latenz der Tastatureingabe spielen:
- Verwendet die Firmware einen Eager-Entprellungsalgorithmus?(öffnet im neuen Fenster)
- Hat das Gerät eine schnelle USB-Abtastrate (Einstellung von bInterval(öffnet im neuen Fenster) )?
- Liegt die Frequenz der Matrixscans im erwarteten Bereich oder wird sie von etwas verlangsamt?
Ich hoffe, dass euch dieser Artikel alle Werkzeuge an die Hand gibt, die ihr benötigt, um die Eingabelatenz eures eigenen Tastaturcontrollers messen und reduzieren zu können!
Anhang A: Is It Snappy
Die iPhone-App Is It Snappy?(öffnet im neuen Fenster) nimmt Videos mit der 240-fps-iPhone-Kamera auf und ermöglicht es, das Bild zu markieren, mit dem die Messung beginnt bzw. endet. Die App macht den sonst sehr mühsamen Prozess, die Einzelbilder eines Videos durchzusehen, viel angenehmer.
Für die Messung der Latenzzeit bei Tastatureingaben halte ich diesen Ansatz jedoch für sinnlos:
- Die Auflösung ist zu ungenau. Bei 240 Bildern pro Sekunde bedeutet das, dass jedes Einzelbild 4,6 ms entspricht, was bereits mehr ist als die Eingabelatenz unseres langsamsten Mikrocontrollers.
- Rein visuell zu entscheiden, ob eine Taste gedrückt ist oder nicht, und zwar mit frame-perfekter Präzision, scheint mir unmöglich.
Wenn die Latenz, die man messen will, sehr hoch ist, ist die App sicherlich hilfreich. Bei den in diesem Artikel behandelten Geräten konnte die App jedoch nicht einmal eine Eingabelatenz von 10 ms messen.
Anhang B: USB-Abtastintervall (Geräteseite)
Auch auf der Geräteseite können wir das USB-Abtastintervall überprüfen. Im SOF (Start Of Frame) Interrupt unter tmk_core/protocol/chibios/usb_main.c können wir für jede Sekunde das Zyklus-Delta zum vorherigen SOF-Callback ausgeben:
#include "timer.h"
static uint32_t last_sof = 0;
static uint32_t sof_timer = 0;
void kbd_sof_cb(USBDriver *usbp) {
(void)usbp;
uint32_t now = DWT->CYCCNT;
uint32_t delta = now - last_sof;
last_sof = now;
uint32_t timer_now = timer_read32();
if (TIMER_DIFF_32(timer_now, sof_timer) > 1000) {
sof_timer = timer_now;
dprintf("sof delta: %u cycles", delta);
}
}
hid_listen sollte dann ein Delta von ≈75.000 Zyklen messen, was der 125μs Microframe-Latenz von USB 2.0 High Speed mit bInterval=1 im USB-Gerätedeskriptor entspricht: 125μs * 1.000 * 0,6 Zyklen/ns = 75.000 Zyklen.



