Writing an OS Kernel from Scratch | 346: Keyboard — How Keystrokes Become Characters
Abstract: You press a key, and a character appears on screen — what happens in between? This article traces the complete data path of keyboard input, from hardware matrix scanning, PS/2 protocol, USB HID descriptors, to the Linux input subsystem. wandos currently has no USB HID implementation; the USB HID section of this article is only effective in a Linux environment.
1. Hardware Layer: Matrix Scanning
1.1 How the Matrix Is Organized
Keyboard keys are arranged in a row×column matrix. Each key is the intersection of a row line and a column line. Pressing a key shorts the corresponding row and column lines. The scanning process:
- The controller sets each column to low level in sequence
- Reads the level state of all rows
- When a key is pressed, the corresponding row’s GPIO reads low due to the short circuit
Column0 Column1 Column2 Column3
Row0 ──┬────────┬────────┬────────┐
│ K11 │ K12 │ K13 │ K14
Row1 ──┼────────┼────────┼────────┼─────
│ K21 │ K22 │ K23 │ K24
Row2 ──┼────────┼────────┼────────┼─────
│ K31 │ K32 │ K33 │ K34
Row3 ──┴────────┴────────┴────────┴─────
Scanning Column0:
- Row0=0, Row1=0, Row2=0, Row3=1 → K11, K21, K31 pressed
- Row0=1, Row1=0, Row2=0, Row3=0 → K22, K32 pressedUsing a 4×8 matrix as an example: 8 rows × 4 columns = 32 keys only need 12 GPIO pins (far fewer than 32 individual wires). This is the core principle of matrix scanning saving I/O.
1.2 Key Debounce
Mechanical keys have several milliseconds of bounce when pressed and released, requiring debounce handling:
// Simple software debounce
#define DEBOUNCE_MS 10
uint32_t last_key_state = 0;
uint32_t key_state = 0;
void scan_keyboard(void) {
uint32_t new_state = read_matrix();
if (new_state != last_key_state) {
sleep_ms(DEBOUNCE_MS); // Wait for bounce to settle
new_state = read_matrix();
if (new_state != last_key_state) {
// State has actually changed
uint32_t changed = new_state ^ last_key_state;
for (int i = 0; i < 32; i++) {
if (changed & (1 << i)) {
bool pressed = (new_state >> i) & 1;
key_event(i, pressed);
}
}
last_key_state = new_state;
}
}
}2. PS/2 Protocol: Old-Style Keyboard Communication
2.1 Physical Layer: Clock + Data
The PS/2 interface has 6 pins (Mini-DIN), but only uses 2 signal lines:
- Clock: Host-controlled clock line, frequency 10-16.7 kHz
- Data: Data line, LSB first
Host → Keyboard (Host-to-device)
┌────┐
Clock ─┤ ├──── Clock
Data ─┤ ├──── Data
└────┘
Keyboard → Host (Device-to-host)
Clock ────────┬────┐
└────┘
Data ────────[0x1C]──2.2 Scan Codes
PS/2 keyboard scan codes:
Key Make (Press) Break (Release)
A 0x1C 0xF0 0x1C
B 0x32 0xF0 0x32
Enter 0x5A 0xF0 0x5A
LShift 0x12 0xF0 0x12
0xE0 prefix indicates extended keys (arrow keys, etc.)
0xE1 prefix indicates pause key
The keyboard driver translates scan codes → key codes → ASCII characters3. USB HID: Modern Keyboard Protocol
USB HID keyboard report descriptor (simplified):
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x05, 0x07, // Usage Page (Keyboard)
0x19, 0xE0, // Usage Minimum (Keyboard LeftControl)
0x29, 0xE7, // Usage Maximum (Keyboard Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data,Var,Abs) ← Modifier keys
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x75, 0x08, // Report Size (8)
0x95, 0x06, // Report Count (6) ← 6 keyboard keys
0x81, 0x00, // Input (Data,Array,Abs)
0xC0 // End Collection
Input report (8 bytes):
Byte 0: Modifier keys (Ctrl/Shift/Alt/GUI)
Byte 1: Reserved
Bytes 2-7: Key codes (up to 6 simultaneous keys)4. Linux Input Subsystem
Device initialization:
→ USB device detected
→ HID driver parses report descriptor
→ input_register_device() registers event device
Event generation:
→ USB interrupt transfer sends report (every 8ms)
→ HID driver calls input_report_key()
→ input_sync() sends the event
Event path:
/dev/input/event0 → evdev interface
/dev/input/by-path/ → Persistent device paths
/dev/input/by-id/ → Device-specific paths
Testing commands:
$ evtest /dev/input/event0 # View input events
$ showkey -s # Show scan codes
$ showkey -k # Show keycodes
$ xev # X11 event viewer
$ cat /proc/bus/input/devices # All input devices
Comments