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:

  1. The controller sets each column to low level in sequence
  2. Reads the level state of all rows
  3. When a key is pressed, the corresponding row’s GPIO reads low due to the short circuit

Bash
        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 pressed

Using 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:

C
// 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

Bash
Host → Keyboard (Host-to-device)
         ┌────┐
  Clock ─┤    ├──── Clock
  Data  ─┤    ├──── Data
         └────┘

Keyboard → Host (Device-to-host)
  Clock ────────┬────┐
                └────┘
  Data  ────────[0x1C]──

2.2 Scan Codes

Bash
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 characters

3. USB HID: Modern Keyboard Protocol

Bash
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

Bash
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
Last modified: 2024年4月16日

Author

Comments

Write a Reply or Comment

Your email address will not be published.