- Home
- Kernel Internals
- Input
- Keyboard
Keyboard¶
IRQ1 Handler (abi/idt.rs)¶
On every keypress or key-release, the 8042 latches a scancode in port 0x60 and asserts IRQ1. keyboard_handler fires at IDT vector 0x21:
- Read scancode from port
0x60. - Call
keyboard::dispatch_scancode(sc)— writes intoSCANCODE_BUFand callss.push_irq(sc)for every activeRECEPTORSsubscriber. - Acknowledge master PIC: port
0x20←0x20.
The handler runs in interrupt context with interrupts disabled and no scheduler involvement.
USB Keyboards (xHCI)¶
EFI boots initialize the first PCI xHCI controller automatically. Add usb=xhci
to the GRUB multiboot2 line to enable it explicitly, including on BIOS boots;
usb=firmware disables automatic initialization. The driver takes controller
ownership, disables firmware SMIs, resets the host, reserves DMA memory, and
enumerates devices connected before boot. Intel Panther Point (8086:1e31)
switchable USB2 ports are routed from EHCI to xHCI.
Supported keyboards advertise a HID boot interface (class 3, subclass 1,
protocol 1) with an interrupt-IN endpoint. Composite devices are supported when
they contain this interface. The driver selects boot protocol and translates
eight-byte reports into Set 1 make/break codes, including modifiers and E0
navigation keys. It repeats held keys after 500 ms, at approximately 30 Hz,
and releases held keys when a device disconnects or its endpoint fails.
usb::xhci::poll() runs from the PIT interrupt and sends bytes through
keyboard::dispatch_scancode, so the shell and applications use their existing
input API. Polling performs no allocation or blocking waits. DMA memory is
reserved outside process frames, the boot archive, and the RAM disk. Controller
BARs above 4 GiB receive supervisor-only, uncached mappings.
This initial driver supports direct root-port connections and USB2 hubs, up to 16 device slots and five hub levels. Connect the keyboard before starting R2. Runtime attachment/reconnection, SuperSpeed hubs, EHCI/OHCI/UHCI input, USB mice/trackpads, keyboard LEDs, and keyboards requiring a custom report protocol are not supported. A Mac's built-in keyboard may remain on an EHCI-only port; an external boot-protocol keyboard on a switchable xHCI port is the first hardware test to try.
Host tests cover HID reports and descriptors; the VM test boots an EFI image with PS/2 disabled and checks USB key presses, modifiers, arrows, repeat, ring wrap, and disconnect releases:
rustc tests/unit/main.rs --test --edition 2021 -o /tmp/r2-unit-tests
/tmp/r2-unit-tests
python3 tests/usb/qemu_keyboard.py
python3 tests/usb/qemu_keyboard.py --hub
python3 tests/usb/qemu_keyboard.py --ps2
The VM test needs QEMU, OVMF, nm, and a freshly built r2-efi.iso with its
matching staged iso/boot/kernel_graphics.elf.
Register and report layouts follow the Intel xHCI specification and USB-IF HID specification.
Scancode Buffer (input/keyboard.rs)¶
SCANCODE_BUF: [u8; 2048] is a kernel-internal circular buffer used exclusively by the shell loop.
SCANCODE_BUF: [u8; 2048]
SCANCODE_BUF_HEAD: usize ← write index
SCANCODE_BUF_LOCKED: bool ← true when no new scancode is available
Write path (push_scancode, called through dispatch_scancode by IRQ1 or USB polling):
1. Write scancode to SCANCODE_BUF[HEAD].
2. Increment and wrap HEAD modulo 2048.
3. Set SCANCODE_BUF_LOCKED = false.
Read path (load_scancode, called from shell):
1. Spin with pause until SCANCODE_BUF_LOCKED == false.
2. Set SCANCODE_BUF_LOCKED = true.
3. Return SCANCODE_BUF[(HEAD - 1) % 2048].
This is a single-consumer, single-producer arrangement — only the shell reads from the buffer. There is no blocking sleep; the shell busy-waits using pause to yield to the CPU's pipeline without spinning on memory.
Shell Loop (keyboard_loop)¶
keyboard_loop() is the kernel shell. It runs as a dedicated task (slot 0 of KSTACK_POOL) and never returns.
loop:
key ← load_scancode()
if key & 0x80 != 0: // key-release event
scancode_to_ascii(key) // update modifier state (Shift, CapsLock)
continue
match key:
0x0E (Backspace) → erase last char, decrement input_len
0x0F (Tab) → tab completion (see below)
0x1C (Enter) → submit line to cmd::handle(), clear buffer, print prompt
0x1D (Ctrl) → set ctrl_down = true
0x26 + ctrl_down → Ctrl+L: clear screen, reset buffer, print prompt
_ → scancode_to_ascii(key) → append to input_buffer if printable
Input buffer: [u8; 128]. Once full (128 bytes), further printable characters are silently dropped.
Scancode Set 1 Translation (scancode_to_ascii)¶
Translates PS/2 Set 1 make-codes to ASCII bytes. Returns None for non-printable keys or modifier-only events.
Modifier state is tracked in two static mut booleans:
| Variable | Updated on scancode |
|---|---|
SHIFT_PRESSED |
0x2A/0x36 (make), 0xAA/0xB6 (break) |
CAPS_LOCK_ON |
0x3A (make, toggled) |
Letter key rule: upper is emitted when CAPS_LOCK_ON XOR SHIFT_PRESSED is true.
Key Map (selected)¶
| Scancode | Normal | Shifted |
|---|---|---|
0x02–0x0B |
1–9, 0 |
!, @, #, $, %, ^, &, *, (, ) |
0x0C |
- |
_ |
0x0D |
= |
+ |
0x0E |
Backspace (8) | — |
0x0F |
Tab | — |
0x10–0x19 |
q w e r t y u i o p |
uppercase |
0x1A |
[ |
{ |
0x1B |
] |
} |
0x1C |
Enter (\n) |
— |
0x1D |
Ctrl | — |
0x1E–0x26 |
a s d f g h j k l |
uppercase |
0x27 |
; |
: |
0x28 |
' |
" |
0x29 |
` |
~ |
0x2A |
Shift (L) | — |
0x2B |
\ |
\| |
0x2C–0x32 |
z x c v b n m |
uppercase |
0x33 |
, |
< |
0x34 |
. |
> |
0x35 |
/ |
? |
0x36 |
Shift (R) | — |
0x39 |
Space | — |
0x3A |
Caps Lock (toggle) | — |
Break-codes (bit 7 set) for the above are scancode | 0x80. All unrecognized scancodes return None.
Tab Completion¶
When Tab (scancode 0x0F) is pressed:
- Split the current input buffer at the first space:
(cmd, prefix). - If
prefixis empty, runcmd::handle(b"help")and return. - Scan FAT12 directory entries in the current working directory (
FatDev::cwd_mounted()). When it is on no FAT12 volume (/,/mnt, the ISO or the archive) there is nothing to scan, and Tab does nothing. - Pad
prefixto 11 characters (FAT 8.3 name format:pad_prefix()). - On a match:
- Backspace over
prefix.len()characters. - Print the full matched name in magenta.
- Replace the prefix in
input_bufferwith the completed name. - For
cd: skip file entries (only directories match).
Tab completion operates on FAT12 only (no ISO9660 or VFS dispatch).
Keyboard Pipe — Userland Subscriber Fan-out (input/irq.rs)¶
When the shell is not the consumer, userland programs can receive raw scancodes directly via a subscriber mechanism.
Subscriber Table¶
pub static mut RECEPTORS: [Subscriber; 5]
Each Subscriber has:
| Field | Type | Description |
|---|---|---|
buf_ptr |
u64 |
Virtual address of the userland mailbox byte (or 0 if unused) |
pid |
usize |
PID of the subscribed process (0 = free slot) |
kbuf |
[u8; 256] |
Kernel-side ring buffer |
head/tail |
AtomicUsize |
Ring buffer write/read indices (SeqCst) |
Two Read Paths¶
Mailbox path (polling-friendly): push_irq writes to *buf_ptr only when the current value is 0. Userland polls this byte; after reading it must clear it to 0 to receive the next scancode.
Ring buffer path (drain-friendly): push_irq always enqueues into kbuf, regardless of the mailbox state. Userland calls syscall 0x03/0x03 which invokes copy_to_user() to drain up to 16 bytes from the ring.
Both paths are written on every IRQ1 event for every active subscriber.
Syscall Interface¶
| Syscall | Sub-opcode | Action |
|---|---|---|
0x03 |
0x01 |
pipe_subscribe(addr) — register calling process, set mailbox address |
0x03 |
0x02 |
pipe_unsubscribe(_addr) — deregister calling process |
0x03 |
0x03 |
copy_to_user(arg2, 16) — drain up to 16 bytes from ring buffer |
pipe_subscribe finds the first slot where pid == 0, sets pid and buf_ptr, and clears the ring buffer. pipe_unsubscribe clears the slot by PID. Maximum 5 simultaneous subscribers.
Related documentation: Input Overview, Kernel Internals, Kernel Shell, IPC.