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
+6
View File
@@ -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 \
+11
View File
@@ -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);
+81
View File
@@ -0,0 +1,81 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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 <stdint.h>
/* 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 */
+68
View File
@@ -0,0 +1,68 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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 <stdint.h>
/**
* 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 */
+53 -7
View File
@@ -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
* ============================================================================ */
+150
View File
@@ -0,0 +1,150 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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 <stdint.h>
#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;
}
+23
View File
@@ -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 ===");
+319
View File
@@ -0,0 +1,319 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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 <stddef.h>
#include <stdint.h>
#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;
}
+19 -3
View File
@@ -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
+21
View File
@@ -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.");
+2
View File
@@ -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");
}
+40
View File
@@ -0,0 +1,40 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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 */
+57
View File
@@ -0,0 +1,57 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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);
}