starkernel: item 4.3.5 -- amd64 I/O APIC + i8042 keyboard, interrupt-driven

Punch list §25 item 4.3.5 complete.

New ioapic.c/i8042.c drivers (MADT-derived I/O APIC base, no hardcoded
constants) plus a KBD-SCAN/KBD-DEBUG diagnostic word pair. Three real
bugs found and fixed en route, all blocking this item's own acceptance:
a fatal LAPIC spurious-vector crash (nothing had driven a real external
interrupt through the I/O APIC before), OVMF leaving the keyboard device
itself scanning-disabled (0xF4 fix), and isr.S's stub table only having
individually-numbered stubs through vector 32 -- everything above that,
including our IRQ1 vector 33, silently reported as vector 255 regardless
of which IDT slot actually fired. Verified live via QEMU sendkey against
KBD-SCAN: correct XT Set-1 make/break codes for two different keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-08 00:18:48 -04:00
co-authored by Claude Sonnet 5
parent 78ff335b97
commit 88eb73cfe8
14 changed files with 914 additions and 11 deletions
+64 -1
View File
@@ -3843,7 +3843,7 @@ document and committing that amendment as its own item.*
> **This is the checkpoint** — 4.3.x groundwork stops here for review per this item's own
> acceptance criterion, before scoping whatever comes next.
- [ ] **4.3.5 — amd64: I/O APIC bring-up + i8042 keyboard, interrupt-driven.** No I/O APIC
- [x] **4.3.5 — amd64: I/O APIC bring-up + i8042 keyboard, interrupt-driven.** No I/O APIC
driver exists in this tree today (checked: `apic.c` is Local-APIC-only, and the timer
needs no routing because it self-interrupts) and `pic_disable()` masks the legacy 8259
permanently — so no legacy IRQ, IRQ1 included, currently has any path to the CPU. This
@@ -3856,6 +3856,69 @@ document and committing that amendment as its own item.*
path with no polling loop in the code, three-arch QEMU boot unaffected, log committed.
*Refs:* §27.5.
> **Done, 2026-08-08.** New `src/starkernel/arch/amd64/ioapic.c`/`include/starkernel/
> ioapic.h`: parses the real ACPI MADT (RSDP → XSDT → APIC table, same walk `pci.c`
> already uses for MCFG, not shared code but the same technique) for the I/O APIC's MMIO
> base and any Interrupt Source Override entries — nothing hardcoded. New `i8042.c`/
> `i8042.h`: minimal controller init, a trivial ISR (read `0x60`, push to a small ring
> buffer, nothing else), and `i8042_pop_scancode()` for non-interrupt-context draining.
> New diagnostic word `KBD-SCAN ( -- c -1 | 0 )` in `src/word_source/keyboard_words.c`
> (raw hardware-boundary primitive, same relationship to future Console policy that
> `PLOT` has to `capsules/fabric.4th`) plus a standing `KBD-DEBUG ( -- isr_count
> spurious_count )` diagnostic. `apic_id()` added to `apic.h`/`apic.c` (LAPIC ID getter,
> needed for the redirection entry's destination field).
>
> **Three real bugs found and fixed, all blocking this item's own acceptance test, not
> scope creep:**
>
> 1. **LAPIC spurious-vector interrupts (`0xFF`) were previously fatal.** `pic_disable()`
> means the I/O APIC is the *first* real external-interrupt source this kernel has ever
> driven — the timer self-interrupts and never needed one. The very first live keypress
> test crashed with `Vector : 255 (0xff)` / "Fault: Unhandled vector". Per Intel SDM
> Vol.3 §10.9, a spurious-vector interrupt is a normal occasional hardware race, not a
> fault, and must be silently ignored with no EOI. Fixed with a dedicated case in
> `isr_common_handler()` (`interrupts.c`) plus a permanent `g_spurious_count` diagnostic
> counter — cheap enough to keep, and this item is exactly why it's worth having.
>
> 2. **OVMF's own PS/2 driver leaves the keyboard *device* (not just the controller) with
> scanning disabled.** After the crash was fixed, `KBD-SCAN` still came back empty on
> every keypress. QEMU's own PS/2 tracepoints (`-trace enable='ps2_*,pckbd_*'`) showed
> the device correctly generating and queuing the real make/break scancode sequence on
> `sendkey`, so the device model itself was never the problem. `i8042_init()` only
> programmed the *controller's* config byte (IRQ1-enable); it never told the *device*
> to resume scanning. Fixed by sending `0xF4` (enable scanning) to the device via the
> data port after the controller config write, with a bounded wait for the `0xFA` ACK —
> a one-time init handshake, not the forbidden steady-state polling. (Ancillary,
> reported not fixed: the controller's IRQ1-enable bit was already set before this
> kernel ever touched it on this QEMU/OVMF combination, per the trace — that write is a
> no-op here but stays correct for hardware that doesn't pre-enable it.)
>
> 3. **The real bug, found via `-d int` + a live LAPIC ISR/PPR register dump: `isr.S`'s
> stub table only ever had individually-numbered stubs for vectors 032.** Vectors
> 33255 all shared `isr_stub_default`, which unconditionally pushes `255` as "the
> vector" regardless of which IDT slot actually fired — nothing before this item had
> ever needed a real interrupt source above vector 32. The I/O APIC correctly delivered
> IRQ1 to vector 33 (0x21) — confirmed directly by reading the LAPIC's own ISR register
> at the moment of the spurious firing, which showed vector 33 genuinely in-service —
> but `isr_common_handler()` only ever saw vector 255, treated a real keyboard IRQ as
> spurious, and (correctly, per finding 1) skipped its EOI, permanently stranding
> vector 33's ISR bit and silently blocking that interrupt class forever after. A
> priority-class theory (moving the keyboard vector to `0x31`, a different class from
> the timer's `0x20`) was tested and ruled out first — the direct ISR-register readback
> is what actually found it. Fixed by adding a dedicated `isr_stub33` (same `ISR_NOERR`
> macro pattern as every other named stub) and pointing `isr_stub_table[33]` at it
> instead of the shared default.
>
> **Verified live on amd64** via QEMU's HMP `sendkey` monitor command against a real
> interactive serial session (same manual-injection technique used throughout 4.3.x):
> `sendkey a``KBD-SCAN .S` showed `<2> 30 -1` (30 = `0x1E`, the correct XT Set-1 make
> code for 'A'); `sendkey b` → two `KBD-SCAN` calls showed `48 -1` then `176 -1` (`0x30`
> make / `0x30|0x80` break for 'B', confirming the release-code bit-7 pattern). `KBD-DEBUG`
> read `2 0` (two real IRQs serviced, zero spurious) after each keypress. Three-architecture
> acceptance boot clean on all three (`logs/20260808-001508` amd64, `logs/20260808-001546`
> aarch64, `logs/20260808-001637` riscv64) — the feature is amd64-only this item, so
> aarch64/riscv64 are a regression check, not a keyboard test.
- [ ] **4.3.5a — riscv64: PLIC bring-up (external interrupt controller).** Does not exist
anywhere in this tree — Phase 0 (0.2/0.3) only ever enabled the S-mode *timer* interrupt
(`sie.STIE`); external interrupts (`sie.SEIE`, bit 9) were never touched, and