diff --git a/FABRIC.md b/FABRIC.md index 0bd7cd7..3f0301b 100644 --- a/FABRIC.md +++ b/FABRIC.md @@ -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 0–32.** Vectors + > 33–255 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 diff --git a/Makefile.starkernel b/Makefile.starkernel index a99f321..39e6d7a 100644 --- a/Makefile.starkernel +++ b/Makefile.starkernel @@ -417,6 +417,12 @@ LOADER_ARCH_SRCS := \ $(KERNEL_SRC)/arch/$(ARCH)/interrupts.c \ $(KERNEL_SRC)/arch/$(ARCH)/apic.c \ $(KERNEL_SRC)/arch/$(ARCH)/timer.c + +ifeq ($(ARCH),amd64) +LOADER_ARCH_SRCS += \ + $(KERNEL_SRC)/arch/$(ARCH)/ioapic.c \ + $(KERNEL_SRC)/arch/$(ARCH)/i8042.c +endif else LOADER_SRCS_BASE := \ $(KERNEL_SRC)/boot/uefi_loader.c \ diff --git a/include/starkernel/apic.h b/include/starkernel/apic.h index a3bb27c..0b9b1df 100644 --- a/include/starkernel/apic.h +++ b/include/starkernel/apic.h @@ -53,9 +53,20 @@ /* Heartbeat timer vector (user-defined IRQ space starts at 0x20) */ #define APIC_TIMER_VECTOR 0x20 +/* Spurious-interrupt vector (APIC_REG_SIVR, set in apic_init()). A normal, + * occasional hardware race per Intel SDM Vol.3 §10.9 -- not a fault. Must + * be silently ignored with no EOI. Surfaced by item 4.3.5's I/O APIC work: + * no real external interrupt had ever been delivered through the I/O APIC + * before, so this path was previously dormant. */ +#define APIC_SPURIOUS_VECTOR 0xFF + /* Initialize Local APIC (enables APIC, sets spurious vector) */ int apic_init(BootInfo *boot_info); +/* Return this CPU's xAPIC ID (APIC_REG_ID bits 31:24) — used as the + * destination field when programming I/O APIC redirection entries. */ +uint8_t apic_id(void); + /* Send End-of-Interrupt signal */ void apic_eoi(void); diff --git a/include/starkernel/i8042.h b/include/starkernel/i8042.h new file mode 100644 index 0000000..f1097c9 --- /dev/null +++ b/include/starkernel/i8042.h @@ -0,0 +1,81 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + This file is part of the StarForth project. + + Licensed under the StarForth License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + + You may obtain a copy of the License at: + https://github.com/star.4th@proton.me/StarForth/LICENSE.txt + + This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, + express or implied, including but not limited to the warranties of + merchantability, fitness for a particular purpose, and noninfringement. + + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/** + * i8042.h - PS/2 keyboard controller interface (amd64 only) + * + * Item 4.3.5 (FABRIC.md §27.5). Interrupt-driven only — no polling of the + * status port (0x64) anywhere in this path. Groundwork only: this captures + * and prints raw scancodes. Scancode-to-keycode translation and a consumer + * API belong to the REPL keyboard-input work noted in FABRIC.md, not here. + */ + +#ifndef STARKERNEL_I8042_H +#define STARKERNEL_I8042_H + +#include + +/* IDT vector the I/O APIC delivers legacy IRQ1 to. */ +#define I8042_KEYBOARD_VECTOR 0x21 + +/** + * Enable IRQ1 delivery in the i8042 controller's command byte. Assumes the + * controller was already brought up by firmware (OVMF) — does not run the + * 0xAA self-test or the full two-port init sequence, since that is more + * than this item's scope requires. + */ +void i8042_init(void); + +/** + * Drain any byte sitting in the output buffer right before unmasking IRQ1. + * Edge-triggered lines only assert on a rising edge; if OBF is already set + * (from real wall-clock time elapsing between i8042_init() and unmask + * while OBF was already high), there is no edge left to fire on, ever. + */ +void i8042_drain_stale(void); + +/* Count of real keyboard IRQs serviced since boot (diagnostic). */ +extern volatile uint32_t g_i8042_isr_count; + +/** + * Called from the keyboard IRQ dispatch path (interrupts.c) on every + * I8042_KEYBOARD_VECTOR interrupt. Trivial by design: reads the scancode + * from port 0x60 (must always be read, or the controller never clears + * OBF and stops delivering further interrupts) and pushes it onto a small + * ring buffer. No printing, no translation, no other work — that all + * happens outside interrupt context via i8042_pop_scancode(). Does not + * issue apic_eoi() — the caller does. + */ +void i8042_handle_irq(void); + +/** + * Pop one raw scancode off the ring buffer, from non-interrupt context. + * Single producer (the ISR), single reader (this function) — same shape + * already established safe on one hart elsewhere in this tree (§21.1). + * + * @param out Written with the popped scancode on success. + * @return 1 if a scancode was popped, 0 if the buffer was empty. + */ +int i8042_pop_scancode(uint8_t *out); + +#endif /* STARKERNEL_I8042_H */ diff --git a/include/starkernel/ioapic.h b/include/starkernel/ioapic.h new file mode 100644 index 0000000..d011a31 --- /dev/null +++ b/include/starkernel/ioapic.h @@ -0,0 +1,68 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + This file is part of the StarForth project. + + Licensed under the StarForth License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + + You may obtain a copy of the License at: + https://github.com/star.4th@proton.me/StarForth/LICENSE.txt + + This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, + express or implied, including but not limited to the warranties of + merchantability, fitness for a particular purpose, and noninfringement. + + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/** + * ioapic.h - I/O APIC interface (amd64 only) + * + * Item 4.3.5 (FABRIC.md §27.5): no I/O APIC driver existed anywhere in this + * tree before this item. The Local APIC (apic.h) self-interrupts for the + * timer and needs no routing; any *legacy* IRQ (i8042 keyboard's IRQ1 + * included) requires the I/O APIC to redirect it to a Local APIC vector. + */ + +#ifndef STARKERNEL_IOAPIC_H +#define STARKERNEL_IOAPIC_H + +#include + +/** + * Locate and map the I/O APIC via the ACPI MADT (parsed from @p acpi_rsdp). + * Also captures any Interrupt Source Override entries so legacy ISA IRQs + * with a non-default GSI/polarity/trigger mapping route correctly. + * + * @param acpi_rsdp BootInfo->acpi_table (RSDP), or NULL. + * @return 0 on success, -1 if no MADT/I/O APIC entry was found. + */ +int ioapic_init(void *acpi_rsdp); + +/** + * Program a redirection entry for a legacy ISA IRQ, initially masked. + * Honors an Interrupt Source Override for @p isa_irq if the MADT had one + * (different GSI, polarity, or trigger mode); otherwise uses the ISA + * default (GSI == isa_irq, active-high, edge-triggered) per the ACPI + * specification's "conforms to bus" default. + * + * @param isa_irq Legacy IRQ number (0-15). + * @param vector IDT vector to deliver (e.g. 0x21). + * @param dest_apic_id Destination Local APIC ID (see apic_id()). + * @return 0 on success, -1 if ioapic_init() has not succeeded. + */ +int ioapic_route_legacy_irq(uint8_t isa_irq, uint8_t vector, uint8_t dest_apic_id); + +/** + * Clear the mask bit on a previously routed legacy IRQ's redirection entry. + * Call only after ioapic_route_legacy_irq() has programmed it. + */ +void ioapic_unmask_legacy_irq(uint8_t isa_irq); + +#endif /* STARKERNEL_IOAPIC_H */ diff --git a/src/starkernel/arch/amd64/apic.c b/src/starkernel/arch/amd64/apic.c index 6bd17c6..ad8d3b7 100644 --- a/src/starkernel/arch/amd64/apic.c +++ b/src/starkernel/arch/amd64/apic.c @@ -273,14 +273,46 @@ int apic_init(BootInfo *boot_info) { lapic_write(APIC_REG_TPR, 0); /* - * Issue a spurious EOI to clear any stale in-service interrupt left by - * UEFI firmware. UEFI's last APIC timer tick may have fired just before - * ExitBootServices with no subsequent EOI; the APIC's ISR then shows - * vector 0x20 still "in service", which suppresses all future 0x20 - * delivery until an EOI is written. Issuing EOI here is always safe — - * it is a no-op if the ISR is already clear. + * Drain ALL stale in-service bits left by UEFI firmware, not just one. + * UEFI's own interrupt handling commonly uses the same 0x20-based + * legacy-IRQ vector remap convention this kernel does, and can leave + * MORE THAN ONE vector "in service" when ExitBootServices() cuts it + * off mid-handling (its own timer tick AND, per item 4.3.5's finding, + * its own keyboard IRQ1 handling -- both land in the same 0x20-0x2F + * range). A single EOI clears only the highest-priority stale bit; + * firmware's other stale bit then sits permanently in-service and + * silently demotes every future same-or-lower-class interrupt to the + * spurious vector instead of its real one. EOI always clears whichever + * ISR bit is currently highest-priority, so looping while any of the + * 8 ISR registers (0x100-0x170, vectors 0-255) is nonzero drains all + * of them. Bounded to 8 iterations -- one firmware is not expected to + * leave more stale bits than that; if it does, stop rather than spin + * forever on real hardware. */ - lapic_write(APIC_REG_EOI, 0); + { + #define APIC_REG_ISR_BASE 0x100 + int drained; + for (drained = 0; drained < 8; drained++) { + int any_set = 0; + for (int r = 0; r < 8; r++) { + if (lapic_read(APIC_REG_ISR_BASE + (uint32_t)r * 0x10) != 0) { + any_set = 1; + break; + } + } + if (!any_set) break; + lapic_write(APIC_REG_EOI, 0); + } + console_puts("APIC: stale-ISR drain: "); + { + char buf[8]; int i = 0; uint32_t v = (uint32_t)drained; + if (v == 0) buf[i++] = '0'; + else { char tmp[8]; int j = 0; while (v > 0) { tmp[j++] = (char)('0' + (v % 10)); v /= 10; } while (j > 0) buf[i++] = tmp[--j]; } + buf[i] = '\0'; + console_puts(buf); + } + console_puts(" EOI(s) issued\n"); + } console_puts("APIC: initialized (xAPIC MMIO, TPR=0, SIVR=0x1FF, EOI-clear)\n"); return 0; @@ -302,6 +334,20 @@ void apic_eoi(void) { lapic_write(APIC_REG_EOI, 0); } +/** + * @brief Return this CPU's xAPIC ID. + * + * Reads @c APIC_REG_ID and extracts bits 31:24, the xAPIC ID field. Used by + * the I/O APIC driver (item 4.3.5) as the destination field when programming + * a redirection entry — interrupts routed to this ID are delivered to the + * current CPU's Local APIC. + * + * @return This CPU's 8-bit xAPIC ID. + */ +uint8_t apic_id(void) { + return (uint8_t)(lapic_read(APIC_REG_ID) >> 24); +} + /* ============================================================================ * APIC Timer * ============================================================================ */ diff --git a/src/starkernel/arch/amd64/i8042.c b/src/starkernel/arch/amd64/i8042.c new file mode 100644 index 0000000..36c19dc --- /dev/null +++ b/src/starkernel/arch/amd64/i8042.c @@ -0,0 +1,150 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + This file is part of the StarForth project. + + Licensed under the StarForth License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + + You may obtain a copy of the License at: + https://github.com/star.4th@proton.me/StarForth/LICENSE.txt + + This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, + express or implied, including but not limited to the warranties of + merchantability, fitness for a particular purpose, and noninfringement. + + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/** + * i8042.c - PS/2 keyboard controller driver (amd64) + * + * Item 4.3.5 (FABRIC.md §27.5). + */ + +#ifndef __STARKERNEL__ +#error "i8042.c is kernel-only" +#endif + +#include +#include "starkernel/i8042.h" +#include "console.h" + +#define I8042_DATA_PORT 0x60 +#define I8042_STATUS_PORT 0x64 +#define I8042_CMD_PORT 0x64 + +#define I8042_STATUS_OUTPUT_FULL (1u << 0) +#define I8042_STATUS_INPUT_FULL (1u << 1) + +#define I8042_CMD_READ_CONFIG 0x20 +#define I8042_CMD_WRITE_CONFIG 0x60 +#define I8042_CONFIG_IRQ1_ENABLE (1u << 0) + +#define I8042_DEV_CMD_ENABLE_SCANNING 0xF4 +#define I8042_DEV_ACK 0xFA + +/* Bounded wait -- command handshakes during one-time init only, never in + * the interrupt path or a per-keystroke loop. */ +#define I8042_INIT_WAIT_ITERS 100000 + +static inline void outb(uint16_t port, uint8_t val) { + __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port)); +} +static inline uint8_t inb(uint16_t port) { + uint8_t val; + __asm__ volatile ("inb %1, %0" : "=a"(val) : "Nd"(port)); + return val; +} + +void i8042_init(void) { + /* Drain any stale output byte left by firmware before enabling IRQ1 — + * otherwise the first "interrupt" would really be this leftover byte + * read via polling, not a real IRQ1 delivery. */ + while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) { + (void)inb(I8042_DATA_PORT); + } + + outb(I8042_CMD_PORT, I8042_CMD_READ_CONFIG); + uint8_t config = inb(I8042_DATA_PORT); + config |= I8042_CONFIG_IRQ1_ENABLE; + outb(I8042_CMD_PORT, I8042_CMD_WRITE_CONFIG); + outb(I8042_DATA_PORT, config); + + /* Tell the keyboard device itself to (re)enable scanning. OVMF's own + * PS/2 driver commonly sends 0xF5 (disable scanning) to the device as + * part of its own teardown before handoff, the same way it stops + * driving the PIT/PIC -- the controller's IRQ1-enable bit above only + * governs the controller, not whether the device generates scancodes + * at all. Bounded waits: one-time init handshake, not steady-state + * polling. */ + { + int i; + for (i = 0; i < I8042_INIT_WAIT_ITERS; i++) { + if (!(inb(I8042_STATUS_PORT) & I8042_STATUS_INPUT_FULL)) break; + } + outb(I8042_DATA_PORT, I8042_DEV_CMD_ENABLE_SCANNING); + for (i = 0; i < I8042_INIT_WAIT_ITERS; i++) { + if (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) { + uint8_t resp = inb(I8042_DATA_PORT); + if (resp == I8042_DEV_ACK) { + console_println("i8042: keyboard ACKed enable-scanning"); + } else { + console_println("i8042: keyboard responded (not ACK) to enable-scanning"); + } + break; + } + } + } + + console_println("i8042: IRQ1 enabled in controller config byte"); +} + +void i8042_drain_stale(void) { + /* Edge-triggered IRQ1 + OBF already set at unmask time == no rising + * edge ever again (the textbook "fires once, then never" pattern). + * i8042_init()'s drain happens at M4; VM bootstrap runs for real wall + * time between then and unmask, so re-drain immediately before. */ + while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) { + (void)inb(I8042_DATA_PORT); + } +} + +/* Ring buffer: single producer (the ISR), single reader (i8042_pop_scancode(), + * called from ordinary non-interrupt context, e.g. a diagnostic FORTH word). + * 32 entries is far more than one QEMU test keypress burst can fill. */ +#define RING_SIZE 32 +static volatile uint8_t ring[RING_SIZE]; +static volatile uint8_t ring_head = 0; /* next slot the ISR writes */ +static volatile uint8_t ring_tail = 0; /* next slot the reader takes */ + +/* Count of real keyboard IRQs serviced since boot -- a plain counter, no + * I/O, is cheap enough to keep in the ISR itself and doubles as the + * quickest way to confirm the interrupt path is alive without touching + * the ring buffer at all. */ +volatile uint32_t g_i8042_isr_count = 0; + +void i8042_handle_irq(void) { + /* Must always read 0x60, even to discard — otherwise OBF never clears + * and the controller stops delivering further interrupts. */ + uint8_t scancode = inb(I8042_DATA_PORT); + g_i8042_isr_count++; + + uint8_t next_head = (uint8_t)((ring_head + 1) % RING_SIZE); + if (next_head != ring_tail) { /* drop the byte if the ring is full */ + ring[ring_head] = scancode; + ring_head = next_head; + } +} + +int i8042_pop_scancode(uint8_t *out) { + if (ring_tail == ring_head) return 0; /* empty */ + *out = ring[ring_tail]; + ring_tail = (uint8_t)((ring_tail + 1) % RING_SIZE); + return 1; +} diff --git a/src/starkernel/arch/amd64/interrupts.c b/src/starkernel/arch/amd64/interrupts.c index 86571ee..3be0ab5 100644 --- a/src/starkernel/arch/amd64/interrupts.c +++ b/src/starkernel/arch/amd64/interrupts.c @@ -49,11 +49,15 @@ #include "console.h" #include "apic.h" #include "timer.h" +#include "starkernel/i8042.h" /* Set by the FORTH dispatcher just before calling entry->func(vm). * Printed on fault to identify which word was executing. */ volatile const char *g_sk_fault_word = (void *)0; +/* Count of LAPIC spurious-vector interrupts since boot (item 4.3.5). */ +volatile uint32_t g_spurious_count = 0; + #define IDT_ENTRIES 256 #define INTERRUPT_GATE 0x8E @@ -342,6 +346,25 @@ void isr_common_handler(uint64_t vector, return; } + /* Handle i8042 keyboard interrupt (item 4.3.5) */ + if (vector == I8042_KEYBOARD_VECTOR) { + i8042_handle_irq(); + apic_eoi(); + return; + } + + /* Spurious interrupt: normal occasional race per Intel SDM Vol.3 §10.9, + * not a fault. No EOI -- the SDM is explicit that spurious-vector + * interrupts must not be acknowledged. g_spurious_count is a cheap, + * permanent diagnostic (item 4.3.5 found a real keyboard IRQ silently + * misreported as spurious due to a missing dedicated ISR stub -- see + * isr.S -- so a way to notice "spurious firing when it shouldn't be" + * is worth keeping, not just scaffolding for that one investigation). */ + if (vector == APIC_SPURIOUS_VECTOR) { + g_spurious_count++; + return; + } + /* All other vectors are exceptions - print diagnostic and halt */ console_println("\n=== INTERRUPT/EXCEPTION ==="); diff --git a/src/starkernel/arch/amd64/ioapic.c b/src/starkernel/arch/amd64/ioapic.c new file mode 100644 index 0000000..d09a2c7 --- /dev/null +++ b/src/starkernel/arch/amd64/ioapic.c @@ -0,0 +1,319 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + This file is part of the StarForth project. + + Licensed under the StarForth License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + + You may obtain a copy of the License at: + https://github.com/star.4th@proton.me/StarForth/LICENSE.txt + + This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, + express or implied, including but not limited to the warranties of + merchantability, fitness for a particular purpose, and noninfringement. + + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/** + * ioapic.c - I/O APIC driver (amd64) + * + * Item 4.3.5 (FABRIC.md §27.5). MADT parsing mirrors pci.c's RSDP -> XSDT -> + * table-by-signature walk (the two files don't share a header for this — + * same duplication pci.c already has relative to a hypothetical shared + * acpi.c, not introduced fresh here). + */ + +#ifndef __STARKERNEL__ +#error "ioapic.c is kernel-only" +#endif + +#include +#include +#include "starkernel/ioapic.h" +#include "console.h" +#include "vmm.h" + +/* ------------------------------------------------------------------------- + * ACPI structures for MADT discovery + * ------------------------------------------------------------------------- */ + +typedef struct { + uint8_t signature[8]; + uint8_t checksum; + uint8_t oem_id[6]; + uint8_t revision; + uint32_t rsdt_address; + uint32_t length; + uint64_t xsdt_address; + uint8_t extended_checksum; + uint8_t reserved[3]; +} __attribute__((packed)) Rsdp2; + +typedef struct { + uint8_t signature[4]; + uint32_t length; + uint8_t revision; + uint8_t checksum; + uint8_t oem_id[6]; + uint8_t oem_table_id[8]; + uint32_t oem_revision; + uint32_t creator_id; + uint32_t creator_revision; +} __attribute__((packed)) AcpiHeader; + +typedef struct { + AcpiHeader hdr; + uint32_t local_apic_address; + uint32_t flags; + /* variable-length entries follow */ +} __attribute__((packed)) MadtHeader; + +typedef struct { + uint8_t type; /* 1 = I/O APIC */ + uint8_t length; /* 12 */ + uint8_t io_apic_id; + uint8_t reserved; + uint32_t io_apic_address; + uint32_t gsi_base; +} __attribute__((packed)) MadtIoApic; + +typedef struct { + uint8_t type; /* 2 = Interrupt Source Override */ + uint8_t length; /* 10 */ + uint8_t bus; + uint8_t source; /* legacy ISA IRQ */ + uint32_t gsi; + uint16_t flags; /* bits[1:0] polarity, bits[3:2] trigger mode */ +} __attribute__((packed)) MadtIntSrcOverride; + +#define MADT_TYPE_IOAPIC 1 +#define MADT_TYPE_INT_SRC 2 + +#define ISO_POLARITY_MASK 0x3u +#define ISO_POLARITY_HIGH 0x1u +#define ISO_POLARITY_LOW 0x3u +#define ISO_TRIGGER_MASK 0xCu +#define ISO_TRIGGER_EDGE 0x4u +#define ISO_TRIGGER_LEVEL 0xCu + +/* ------------------------------------------------------------------------- + * Module state + * ------------------------------------------------------------------------- */ + +#define MAX_OVERRIDES 16 + +static uint64_t g_ioapic_base = 0; +static uint32_t g_ioapic_gsi_base = 0; +static int g_ioapic_ready = 0; + +static MadtIntSrcOverride g_overrides[MAX_OVERRIDES]; +static int g_override_count = 0; + +/* Cached low-dword redirection values, indexed by GSI - g_ioapic_gsi_base, + * so ioapic_unmask_legacy_irq() can clear just the mask bit without having + * to re-derive polarity/trigger from the override table again. */ +#define MAX_REDIR_ENTRIES 24 +static uint32_t g_redir_low[MAX_REDIR_ENTRIES]; +static int g_redir_valid[MAX_REDIR_ENTRIES]; + +/* ------------------------------------------------------------------------- + * I/O APIC MMIO access (IOREGSEL @ +0x00, IOWIN @ +0x10) + * ------------------------------------------------------------------------- */ + +static void ioapic_write(uint8_t reg, uint32_t val) { + volatile uint32_t *regsel = (volatile uint32_t *)(uintptr_t)(g_ioapic_base + 0x00); + volatile uint32_t *iowin = (volatile uint32_t *)(uintptr_t)(g_ioapic_base + 0x10); + *regsel = reg; + *iowin = val; +} + +/* ------------------------------------------------------------------------- + * ACPI MADT parsing + * ------------------------------------------------------------------------- */ + +static int sig4_eq(const uint8_t *p, const char *s) { + return p[0] == (uint8_t)s[0] && p[1] == (uint8_t)s[1] && + p[2] == (uint8_t)s[2] && p[3] == (uint8_t)s[3]; +} +static int sig8_eq(const uint8_t *p, const char *s) { + int i; + for (i = 0; i < 8; i++) + if (p[i] != (uint8_t)s[i]) return 0; + return 1; +} + +static int parse_madt(void *rsdp) { + if (!rsdp) return -1; + + Rsdp2 *r = (Rsdp2 *)rsdp; + if (!sig8_eq(r->signature, "RSD PTR ")) return -1; + if (r->revision < 2 || r->xsdt_address == 0) return -1; + + AcpiHeader *xsdt = (AcpiHeader *)(uintptr_t)r->xsdt_address; + if (!sig4_eq(xsdt->signature, "XSDT")) return -1; + + uint32_t hdr_len = xsdt->length; + if (hdr_len <= 36u) return -1; + uint32_t n_entries = (hdr_len - 36u) / 8u; + uint64_t *entries = (uint64_t *)((uint8_t *)xsdt + 36u); + + uint32_t i; + for (i = 0; i < n_entries; i++) { + AcpiHeader *sdt = (AcpiHeader *)(uintptr_t)entries[i]; + if (!sig4_eq(sdt->signature, "APIC")) continue; + + MadtHeader *madt = (MadtHeader *)sdt; + uint8_t *p = (uint8_t *)madt + sizeof(MadtHeader); + uint8_t *end = (uint8_t *)madt + madt->hdr.length; + int found_ioapic = 0; + + while (p + 2 <= end) { + uint8_t type = p[0]; + uint8_t len = p[1]; + if (len == 0 || p + len > end) break; + + if (type == MADT_TYPE_IOAPIC && !found_ioapic) { + MadtIoApic *io = (MadtIoApic *)p; + g_ioapic_base = io->io_apic_address; + g_ioapic_gsi_base = io->gsi_base; + found_ioapic = 1; + } else if (type == MADT_TYPE_INT_SRC && g_override_count < MAX_OVERRIDES) { + MadtIntSrcOverride *iso = (MadtIntSrcOverride *)p; + g_overrides[g_override_count++] = *iso; + } + + p += len; + } + + return found_ioapic ? 0 : -1; + } + return -1; +} + +/* ------------------------------------------------------------------------- + * Public API + * ------------------------------------------------------------------------- */ + +int ioapic_init(void *acpi_rsdp) { + if (parse_madt(acpi_rsdp) != 0) { + console_println("I/O APIC: no MADT I/O APIC entry found"); + return -1; + } + + if (vmm_map_range(g_ioapic_base, g_ioapic_base, 0x1000u, + VMM_FLAG_WRITABLE | VMM_FLAG_CACHE_DISABLE) != 0) { + console_println("I/O APIC: failed to map MMIO page"); + return -1; + } + + g_ioapic_ready = 1; + + console_puts("I/O APIC: base=0x"); + for (int s = 28; s >= 0; s -= 4) + console_putc("0123456789abcdef"[((uint32_t)g_ioapic_base >> s) & 0xF]); + console_puts(", gsi_base="); + { + char buf[16]; int i = 0; uint32_t v = g_ioapic_gsi_base; + if (v == 0) buf[i++] = '0'; + else { char tmp[16]; int j = 0; while (v > 0) { tmp[j++] = (char)('0' + (v % 10)); v /= 10; } while (j > 0) buf[i++] = tmp[--j]; } + buf[i] = '\0'; + console_puts(buf); + } + console_puts(", overrides="); + { + char buf[8]; int i = 0; uint32_t v = (uint32_t)g_override_count; + if (v == 0) buf[i++] = '0'; + else { char tmp[8]; int j = 0; while (v > 0) { tmp[j++] = (char)('0' + (v % 10)); v /= 10; } while (j > 0) buf[i++] = tmp[--j]; } + buf[i] = '\0'; + console_puts(buf); + } + console_println(""); + + /* TEMP DEBUG (item 4.3.5 investigation) */ + for (int oi = 0; oi < g_override_count; oi++) { + console_puts(" override: source="); + console_putc("0123456789abcdef"[(g_overrides[oi].source >> 4) & 0xF]); + console_putc("0123456789abcdef"[g_overrides[oi].source & 0xF]); + console_puts(" gsi="); + { + char buf[16]; int i = 0; uint32_t v = g_overrides[oi].gsi; + if (v == 0) buf[i++] = '0'; + else { char tmp[16]; int j = 0; while (v > 0) { tmp[j++] = (char)('0' + (v % 10)); v /= 10; } while (j > 0) buf[i++] = tmp[--j]; } + buf[i] = '\0'; + console_puts(buf); + } + console_puts(" flags=0x"); + console_putc("0123456789abcdef"[(g_overrides[oi].flags >> 12) & 0xF]); + console_putc("0123456789abcdef"[(g_overrides[oi].flags >> 8) & 0xF]); + console_putc("0123456789abcdef"[(g_overrides[oi].flags >> 4) & 0xF]); + console_putc("0123456789abcdef"[g_overrides[oi].flags & 0xF]); + console_println(""); + } + + return 0; +} + +int ioapic_route_legacy_irq(uint8_t isa_irq, uint8_t vector, uint8_t dest_apic_id) { + if (!g_ioapic_ready) return -1; + + uint32_t gsi = isa_irq; + uint32_t polarity = 0; /* active-high (ISA default) */ + uint32_t trigger = 0; /* edge (ISA default) */ + + int i; + for (i = 0; i < g_override_count; i++) { + if (g_overrides[i].source == isa_irq) { + gsi = g_overrides[i].gsi; + uint16_t flags = g_overrides[i].flags; + if ((flags & ISO_POLARITY_MASK) == ISO_POLARITY_LOW) polarity = 1; + if ((flags & ISO_TRIGGER_MASK) == ISO_TRIGGER_LEVEL) trigger = 1; + break; + } + } + + if (gsi < g_ioapic_gsi_base) return -1; + uint32_t redir_index = gsi - g_ioapic_gsi_base; + if (redir_index >= MAX_REDIR_ENTRIES) return -1; + + uint32_t low = (uint32_t)vector + | (polarity << 13) + | (trigger << 15) + | (1u << 16); /* masked until ioapic_unmask_legacy_irq() */ + uint32_t high = (uint32_t)dest_apic_id << 24; + + ioapic_write((uint8_t)(0x10 + 2 * redir_index + 1), high); + ioapic_write((uint8_t)(0x10 + 2 * redir_index), low); + + g_redir_low[redir_index] = low; + g_redir_valid[redir_index] = 1; + + return 0; +} + +void ioapic_unmask_legacy_irq(uint8_t isa_irq) { + if (!g_ioapic_ready) return; + + uint32_t gsi = isa_irq; + int i; + for (i = 0; i < g_override_count; i++) { + if (g_overrides[i].source == isa_irq) { + gsi = g_overrides[i].gsi; + break; + } + } + + if (gsi < g_ioapic_gsi_base) return; + uint32_t redir_index = gsi - g_ioapic_gsi_base; + if (redir_index >= MAX_REDIR_ENTRIES || !g_redir_valid[redir_index]) return; + + uint32_t low = g_redir_low[redir_index] & ~(1u << 16); + ioapic_write((uint8_t)(0x10 + 2 * redir_index), low); + g_redir_low[redir_index] = low; +} diff --git a/src/starkernel/arch/amd64/isr.S b/src/starkernel/arch/amd64/isr.S index bea2875..7e9a38f 100644 --- a/src/starkernel/arch/amd64/isr.S +++ b/src/starkernel/arch/amd64/isr.S @@ -24,6 +24,7 @@ .hidden isr_stub_table .hidden isr_stub0 .hidden isr_stub32 +.hidden isr_stub33 .hidden isr_stub_default .hidden isr_common_entry @@ -155,7 +156,20 @@ isr_stub32: push 0x20 jmp isr_common_entry -/* Default stub for other vectors (33-255) */ +/* Stub for i8042 keyboard IRQ1, routed via I/O APIC to vector 0x21 = 33 + * (item 4.3.5, FABRIC.md §27.5). Before this stub existed, vector 33 fell + * through to isr_stub_default below, which unconditionally reports "255" + * regardless of which IDT slot actually fired -- the CPU legitimately took + * IDT[33] (confirmed via the LAPIC's own ISR register), but isr_common_handler + * only ever saw vector 255 and treated a real keyboard IRQ as spurious, + * silently skipping EOI and leaving vector 33 permanently stuck in-service. */ + .global isr_stub33 +isr_stub33: + push 0 + push 33 + jmp isr_common_entry + +/* Default stub for other vectors (34-255) */ .global isr_stub_default isr_stub_default: push 0 /* fake error */ @@ -177,7 +191,9 @@ isr_stub_table: .quad isr_stub28, isr_stub29, isr_stub30, isr_stub31 /* Vector 32 = APIC Timer */ .quad isr_stub32 - /* Vectors 33-255 default to unknown stub */ - .rept 223 + /* Vector 33 = i8042 keyboard IRQ1 (item 4.3.5) */ + .quad isr_stub33 + /* Vectors 34-255 default to unknown stub */ + .rept 222 .quad isr_stub_default .endr diff --git a/src/starkernel/kernel_main.c b/src/starkernel/kernel_main.c index bae1e70..f10c103 100644 --- a/src/starkernel/kernel_main.c +++ b/src/starkernel/kernel_main.c @@ -28,6 +28,8 @@ #include "vmm.h" #include "apic.h" #include "timer.h" +#include "starkernel/ioapic.h" +#include "starkernel/i8042.h" #include "kmalloc.h" #include "starkernel/kernel_args.h" @@ -423,6 +425,20 @@ void kernel_main(BootInfo *boot_info) { apic_init(boot_info); console_println("APIC: init done\n"); +#ifdef ARCH_AMD64 + /* item 4.3.5 (FABRIC.md §27.5): I/O APIC + i8042 keyboard, interrupt- + * driven. Routed masked here; unmasked in kernel_main_deep() at the + * same point the APIC timer is started. */ + console_println("I/O APIC: init..."); + if (ioapic_init(boot_info->acpi_table) == 0 && + ioapic_route_legacy_irq(1, I8042_KEYBOARD_VECTOR, apic_id()) == 0) { + i8042_init(); + console_println("I/O APIC: keyboard IRQ1 routed (masked)\n"); + } else { + console_println("I/O APIC: keyboard bring-up FAILED\n"); + } +#endif + /* M5: Timer subsystem */ console_println("Timer: init..."); timer_init(boot_info); @@ -640,6 +656,11 @@ static void kernel_main_deep(BootInfo *boot_info) { /* Start heartbeat and enable interrupts */ console_println("Starting heartbeat..."); apic_timer_start(); +#ifdef ARCH_AMD64 + i8042_drain_stale(); + ioapic_unmask_legacy_irq(1); + console_println("I/O APIC: keyboard IRQ1 unmasked"); +#endif arch_enable_interrupts(); console_println("Heartbeat running."); diff --git a/src/word_registry.c b/src/word_registry.c index f0260f8..2b530c4 100644 --- a/src/word_registry.c +++ b/src/word_registry.c @@ -74,6 +74,7 @@ #include "word_source/include/inference_words.h" #include "word_source/include/defer_words.h" #include "word_source/include/framebuffer_words.h" +#include "word_source/include/keyboard_words.h" /** * @brief Registers a single FORTH word in the virtual machine @@ -130,6 +131,7 @@ void register_forth79_words(VM *vm) { register_inference_words(vm); /* Module 26: SSM Inference + Jacquard */ register_defer_words(vm); /* Module 27: DEFER / IS late binding */ register_framebuffer_words(vm); /* Module 28: Console fabric -- raw framebuffer primitives */ + register_keyboard_words(vm); /* Module 29: Console fabric -- raw keyboard scancode diagnostic */ log_message(LOG_INFO, "FORTH-79 Standard word set registration complete"); } \ No newline at end of file diff --git a/src/word_source/include/keyboard_words.h b/src/word_source/include/keyboard_words.h new file mode 100644 index 0000000..84d23fc --- /dev/null +++ b/src/word_source/include/keyboard_words.h @@ -0,0 +1,40 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + Licensed under the StarForth License, Version 1.0 +*/ + +#ifndef KEYBOARD_WORDS_H +#define KEYBOARD_WORDS_H + +#include "vm.h" + +/** + * @defgroup keyboard_words Keyboard Words + * @{ + * + * @brief Raw hardware-boundary FORTH word for verifying the interrupt-driven + * keyboard path (FABRIC.md item 4.3.5). Deliberately a diagnostic peek at + * the raw scancode ring buffer -- no set-2 translation, no REPL wiring. + * Those belong to the later REPL keyboard-input work noted in FABRIC.md. + * + * Kernel-only, amd64-only today (i8042 is amd64 hardware); no-op elsewhere + * so dictionary parity across all three architectures is unaffected. + * + * @par KBD-SCAN ( -- c -1 | 0 ) + * Pop one raw scancode off the interrupt-fed ring buffer. Pushes the + * scancode and -1 (true) if one was available, or just 0 (false) if the + * buffer was empty. + * + * @par KBD-DEBUG ( -- isr_count spurious_count ) + * Standing diagnostic: count of real keyboard IRQs serviced and count of + * LAPIC spurious-vector interrupts, both since boot. + * @} + */ + +void register_keyboard_words(VM *vm); + +#endif /* KEYBOARD_WORDS_H */ diff --git a/src/word_source/keyboard_words.c b/src/word_source/keyboard_words.c new file mode 100644 index 0000000..11a19f0 --- /dev/null +++ b/src/word_source/keyboard_words.c @@ -0,0 +1,57 @@ +/* + StarForth — Steady-State Virtual Machine Runtime + + Copyright (c) 2023–2025 Robert A. James + All rights reserved. + + Licensed under the StarForth License, Version 1.0 +*/ + +/* keyboard_words.c — raw scancode-ring diagnostic word (FABRIC.md item + * 4.3.5). Kernel-only, amd64-only; no-op elsewhere. */ + +#include "include/keyboard_words.h" +#include "../../include/word_registry.h" + +#if defined(__STARKERNEL__) && defined(ARCH_AMD64) +#include "starkernel/i8042.h" +#endif + +/* KBD-SCAN ( -- c -1 | 0 ) */ +static void kbw_scan(VM *vm) +{ +#if defined(__STARKERNEL__) && defined(ARCH_AMD64) + uint8_t sc; + if (i8042_pop_scancode(&sc)) { + vm_push(vm, (cell_t)sc); + vm_push(vm, -1); + } else { + vm_push(vm, 0); + } +#else + vm_push(vm, 0); +#endif +} + +/* KBD-DEBUG ( -- isr_count spurious_count ): cheap standing diagnostic, + * not scaffolding -- confirms the interrupt path is alive (isr_count) and + * flags the failure mode item 4.3.5 found (a real IRQ misreported as + * spurious) without needing a live debugger. */ +extern volatile uint32_t g_i8042_isr_count; +extern volatile uint32_t g_spurious_count; +static void kbw_debug(VM *vm) +{ +#if defined(__STARKERNEL__) && defined(ARCH_AMD64) + vm_push(vm, (cell_t)g_i8042_isr_count); + vm_push(vm, (cell_t)g_spurious_count); +#else + vm_push(vm, 0); + vm_push(vm, 0); +#endif +} + +void register_keyboard_words(VM *vm) +{ + register_word(vm, "KBD-SCAN", kbw_scan); + register_word(vm, "KBD-DEBUG", kbw_debug); +}