Tastatureingaben einlesen (Keyboard-Interrupts)
Nachdem wir uns in den letzten Tutorials mit der Interrupt Descriptor Table, dem Programmable Interrupt Controller und einer Textausgabe für unseren Kernel beschäftigt haben, wollen wir nun weitere Hardware-Interrupts verarbeiten - nämlich unsere Tastatureingaben.
Die Tastatur sendet bei jedem Tastendruck und bei jedem Loslassen einen Interrupt. Diesen soll anschließend ein Tastaturtreiber verarbeiten und die richtigen Tastenanschläge ermitteln. Vom Keyboard-Controller erhalten wir nämlich je nach Tastatur-Aktion mehrere Bytes als Code und jedes Byte wird als eigener Interrupt gesendet.
Scancodes Einlesen mit Zustandsautomat
Eine Lösung hierfür ist ist ein Zustandsautomat, in dem wir die bisherigen Bytes über den aktuellen Zustand darstellen. Da Scancode-Set 2 am weitesten unterstützt wird, will ich den gesamten Ablauf anhand dieses erklären.
Im Scancode-Set 2 wird der Druck einer normalen Taste durch ein Byte repräsentiert. Multimedia-Tasten bestehen aus einem führenden 0xe0
sowie einem weiteren Byte, also insgesamt zwei Bytes. Nach dem Lesen eines 0x0e
-Bytes wechseln wir also in einen Zustand multimedia
.
Ähnlich verhält es sich beim Loslassen einer Taste. Hier erhalten wir dasselbe Byte wie nach dem Drücken, jedoch geführt von einem 0xf0
. Wird beim Drücken von Q
bspw. 0x15
gesendet, so erhalten wir beim Loslassen 0xf0 0x15
.
Beim Loslassen einer Multimedia-Taste schiebt sich das 0xf0
-Byte in die Mitte der beiden Bytes vom Drücken der Taste. Wenn also 0xe0 0x11
für das Drücken von alt gr steht, dann steht 0xe0 0xf0 0x10
für das Loslassen selbiger Taste. Häufig liegen auch Sondertasten wie “Play” oder “Browser” im Multimedia-Bereich, allerdings sind meines Wissens nur wenige dieser Sondertasten standardisiert und können je nach Tastatur abweichen.
Wir können also festhalten, dass wir nach einem 0xf0
in einen Release-Zustand springen müssen. Um uns zu merken, ob wir vorher schon 0xe0
gesehen haben, jeweils in einen anderen.
Nun gibt es noch ein paar Spezialfälle an Tasten, die aus bis zu acht einzelnen Bytes bestehen, z.B. “Pause gedrückt”: 0xe1, 0x14, 0x77, 0xe1, 0xf0, 0x14, 0xf0, 0x77
. Diese Taste ist auch insofern speziell, als es keinen Release-Befehl für sie gibt. Dies müssen wir beim Design unserer Architektur beachten.
Mit diesem Wissen können wir uns einen Endlichen Zustandsautomaten entwerfen. Dies ist mein Entwurf mit dem Finite State Machine Designer von Evan Wallace. Exemplarisch für die Scancodes mit sehr vielen Bytes habe ich lediglich die Taste print screen released eingezeichnet. pause pressed und die Übrigen gehen analog.
byte valid bedeutet, dass das Byte tatsächlich einem Scancode zugeordnet ist. So ist bspw. 0x02
kein gültiger Scancode. In diesen Fällen landen wir in einem Fehlerzustand, den ich zur besseren Übersicht nicht eingezeichnet habe.
Umsetzung in C
Diesen Zustandsautomaten müssen wir nun in C überführen. Vorher sollten wir uns jedoch überlegen, was überhaupt passieren soll, wenn ein gültiger Scancode gelesen werden sollte. Um unseren Kernel flexibel zu halten, macht es natürlich wenig Sinn, direkt an dieser Stelle das Zeichen auf den Bildschirm zu schreiben. Immerhin wollen wir nicht nur Zeichen auf dem Bildschirm anzeigen, sondern - um in der Konsole zu bleiben - in Anwendungen wie vim auch die Steuerung des Cursors übernehmen. Oder später in Videospielen eine Figur steuern.
Deshalb darf der Tastaturtreiber das eingelesene Zeichen den einzelnen Anwendungen lediglich irgendwie bekanntmachen. Hierfür gibt es zwei Varianten:
- Der Tastaturtreiber hat eine Liste von Callback-Funktionen und ruft diese auf, wenn er eine neue Tastaturaktion bemerkt (Push- / Event-System)
- Der Tastaturtreiber verwaltet eine Liste von aktuellen Tastenzuständen, die die Anwendungen abfragen können (Poll-System)
Ich habe mich für das Push-System entschieden, bei dem Events an Callbacks gesendet werden. Man kann aber auch beide Ansätze kombinieren. Das Poll-System alleine scheint mir relativ unpraktisch, weil dann jede Anwendung in möglichst häufigen Abständen nachschauen muss, ob es Änderungen an der Tastatur gibt. In einer Vorlesung habe ich mal gehört, dass bereits eine relativ kleine Verzögerung von wenigen hundert Millisekunden dem Anwender langsam vorkommt, d.h. wir müssten mehrmals pro Sekunde die Tastenliste prüfen.
Außerdem macht es wenig Sinn, die Scancodes selbst wieder rauszuschicken. Wir hatten ja große Probleme, weil diese aus einer ganz unterschiedlichen Anzahl von Bytes bestehen können. Deshalb legen sich die meisten Betriebssystemprogrammierer eine Liste von sogenannten Keycodes an. Dies ist auch wieder nur eine Zuordnung von Tasten auf Zahlen/Bytes, aber mit einheitlicher Bytelänge. Theoretisch kann man aber natürlich auch ganz abgefahrene Dinge tun und einen eigenen Keycode senden, sobald zwei Tasten gleichzeitig gedrückt werden (und dies als Spezialereignis handhaben).
Derzeit habe ich die Keycodes noch in ein enum geschrieben, will das aber ändern, damit ich die Bytegröße reduzieren kann:
Nun ist also die Taste 1 der Zahl 0 zugeordnet, die Taste 2 der Zahl 1 und so weiter.
Sobald diese Keycodes definiert sind, können wir uns auch eine Zuordnung von den Standard-Scancodes (also nicht multimedia) auf die Keycodes anlegen:
Und wenn wir diese Tabelle haben, ist es auch möglich einen Teil des obigen Zustandsautomaten zu realisieren. Für Zustandsautomaten gibt es auch wieder mehrere Umsetzungen in C. Ich habe mich für die Methode entschieden, bei der ein Funktionspointer auf den aktuellen Zustand zeigt. Jeder Zustand hat daher seine eigene Funktion und bei eintreffen eines neuen Bytes, wird diese Funktion mit dem Byte ausgeführt.
Für den obigen Code fehlt uns jetzt noch die Funktion keyboard_fire_event()
, welche das neue Tastaturereignis an alle vorhandenen Callbacks sendet. Diese Funktion soll ein Event an alle Callbacks senden mit notwendigen und hilfreichen Informationen wie dem betroffenen Keycode, ob es ein Press- oder Release-Event war und dem ASCII-Symbol sowie der Aussage, ob der Keycode überhaupt darstellbar ist.
Wir benötigen also diese Struktur, eine Definition der Callbacks, eine Funktion, mit der man Callbacks registrieren kann, und die bereits bekannte Funktion keyboard_fire_event()
. In einer Header-Datei führt das zu folgenden Deklarationen:
In einer C-Datei implementieren wir natürlich diese ganzen Funktionen. Aber weil die Struktur auch den ASCII-Code des Keycodes und das Flag printable
enthält, brauchen wir wieder einige Zuordnungstabellen, die uns diese Informationen für alle Keycodes liefern.
Insgesamt könnte die C-Datei dann so aussehen:
Die Tastatur richtig initialisieren
Was wir jetzt bisher noch versäumt haben, ist es, der Tastatur überhaupt mitzuteilen, dass wir Untranslated Scancode Set 2 wollen.
Hierzu brauchen wir einige weitere Zeilen Code und unsere bereits erstellten IO-Funktionen. Zunächst mal müssen wir dem Keyboard-Controller das gewünschte Scancode Set mitteilen. Dazu schreiben wir 0xf0
auf Port 0x60
und anschließend die Nummer des Scancodes. Danach müssen wir noch die Translation deaktivieren. Das wird gemacht, indem man das 6. Bit des Comannd Byte löscht.
Um zudem in Zukunft auch auf andere Scancode Sets umsteigen zu können, habe ich gleich noch eine Liste von Startzuständen für jedes Scancode Set angelegt. Verwendet wird bisher aber nur Scancode Set 2.
Die Anbindung an Interrupts
Jetzt muss das ganze Scancode-Handling nur noch an die Interrupts gekoppelt werden, damit bei einem Interrupt auch etwas passieren kann. Dazu schreibt man sich einen Interrupt Handler, der im Falle eines Interrupts ausgeführt wird. Für unsere Tastatur sieht er so aus:
Weil der C-Compiler aber immer noch spezielle Funktionseintrittslogik hinzukompiliert, schreibt man die Eintrittspunkte für Interrupts meistens direkt in Assembler (mit speziellen Compiltermakros geht es auch in C). Wir brauchen also noch einen Assemblerabschnitt, der diese Funktion aufruft. Und dieser Assemblerabschnitt wird dann als Interrupt-Handler in der IDT registriert.
Zunächst also der Assembler-Code:
extern keyboard_irq_handler
global int_handler_33
int_handler_33:
mov ax, 0x10
mov gs, ax
mov dword [gs:0xB8000],'3 3 '
call keyboard_irq_handler
mov al, 0x20
out 0x20, al
iret
Und im IDT-Code brauchen wir noch diese Zeilen:
Den ganzen Spaß testen
Um das alles ausprobieren zu können, brauchen wir eigentlich nur noch einen Callback, der die Tastaturereignisse entgegennimmt und ausgibt und passen unsere main()
-Funktion ein wenig an:
Und schon können wir tippen!
I do not maintain a comments section. If you have any questions or comments regarding my posts, please do not hesitate to send me an e-mail to blog@stefan-koch.name.