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:
co-authored by
Claude Sonnet 5
parent
78ff335b97
commit
88eb73cfe8
@@ -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
|
> **This is the checkpoint** — 4.3.x groundwork stops here for review per this item's own
|
||||||
> acceptance criterion, before scoping whatever comes next.
|
> 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
|
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
|
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
|
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.
|
path with no polling loop in the code, three-arch QEMU boot unaffected, log committed.
|
||||||
*Refs:* §27.5.
|
*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
|
- [ ] **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
|
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
|
(`sie.STIE`); external interrupts (`sie.SEIE`, bit 9) were never touched, and
|
||||||
|
|||||||
@@ -417,6 +417,12 @@ LOADER_ARCH_SRCS := \
|
|||||||
$(KERNEL_SRC)/arch/$(ARCH)/interrupts.c \
|
$(KERNEL_SRC)/arch/$(ARCH)/interrupts.c \
|
||||||
$(KERNEL_SRC)/arch/$(ARCH)/apic.c \
|
$(KERNEL_SRC)/arch/$(ARCH)/apic.c \
|
||||||
$(KERNEL_SRC)/arch/$(ARCH)/timer.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
|
else
|
||||||
LOADER_SRCS_BASE := \
|
LOADER_SRCS_BASE := \
|
||||||
$(KERNEL_SRC)/boot/uefi_loader.c \
|
$(KERNEL_SRC)/boot/uefi_loader.c \
|
||||||
|
|||||||
@@ -53,9 +53,20 @@
|
|||||||
/* Heartbeat timer vector (user-defined IRQ space starts at 0x20) */
|
/* Heartbeat timer vector (user-defined IRQ space starts at 0x20) */
|
||||||
#define APIC_TIMER_VECTOR 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) */
|
/* Initialize Local APIC (enables APIC, sets spurious vector) */
|
||||||
int apic_init(BootInfo *boot_info);
|
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 */
|
/* Send End-of-Interrupt signal */
|
||||||
void apic_eoi(void);
|
void apic_eoi(void);
|
||||||
|
|
||||||
|
|||||||
@@ -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 <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 */
|
||||||
@@ -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 <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 */
|
||||||
@@ -273,14 +273,46 @@ int apic_init(BootInfo *boot_info) {
|
|||||||
lapic_write(APIC_REG_TPR, 0);
|
lapic_write(APIC_REG_TPR, 0);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Issue a spurious EOI to clear any stale in-service interrupt left by
|
* Drain ALL stale in-service bits left by UEFI firmware, not just one.
|
||||||
* UEFI firmware. UEFI's last APIC timer tick may have fired just before
|
* UEFI's own interrupt handling commonly uses the same 0x20-based
|
||||||
* ExitBootServices with no subsequent EOI; the APIC's ISR then shows
|
* legacy-IRQ vector remap convention this kernel does, and can leave
|
||||||
* vector 0x20 still "in service", which suppresses all future 0x20
|
* MORE THAN ONE vector "in service" when ExitBootServices() cuts it
|
||||||
* delivery until an EOI is written. Issuing EOI here is always safe —
|
* off mid-handling (its own timer tick AND, per item 4.3.5's finding,
|
||||||
* it is a no-op if the ISR is already clear.
|
* 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");
|
console_puts("APIC: initialized (xAPIC MMIO, TPR=0, SIVR=0x1FF, EOI-clear)\n");
|
||||||
return 0;
|
return 0;
|
||||||
@@ -302,6 +334,20 @@ void apic_eoi(void) {
|
|||||||
lapic_write(APIC_REG_EOI, 0);
|
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
|
* APIC Timer
|
||||||
* ============================================================================ */
|
* ============================================================================ */
|
||||||
|
|||||||
@@ -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 <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;
|
||||||
|
}
|
||||||
@@ -49,11 +49,15 @@
|
|||||||
#include "console.h"
|
#include "console.h"
|
||||||
#include "apic.h"
|
#include "apic.h"
|
||||||
#include "timer.h"
|
#include "timer.h"
|
||||||
|
#include "starkernel/i8042.h"
|
||||||
|
|
||||||
/* Set by the FORTH dispatcher just before calling entry->func(vm).
|
/* Set by the FORTH dispatcher just before calling entry->func(vm).
|
||||||
* Printed on fault to identify which word was executing. */
|
* Printed on fault to identify which word was executing. */
|
||||||
volatile const char *g_sk_fault_word = (void *)0;
|
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 IDT_ENTRIES 256
|
||||||
#define INTERRUPT_GATE 0x8E
|
#define INTERRUPT_GATE 0x8E
|
||||||
|
|
||||||
@@ -342,6 +346,25 @@ void isr_common_handler(uint64_t vector,
|
|||||||
return;
|
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 */
|
/* All other vectors are exceptions - print diagnostic and halt */
|
||||||
console_println("\n=== INTERRUPT/EXCEPTION ===");
|
console_println("\n=== INTERRUPT/EXCEPTION ===");
|
||||||
|
|
||||||
|
|||||||
@@ -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 <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;
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
.hidden isr_stub_table
|
.hidden isr_stub_table
|
||||||
.hidden isr_stub0
|
.hidden isr_stub0
|
||||||
.hidden isr_stub32
|
.hidden isr_stub32
|
||||||
|
.hidden isr_stub33
|
||||||
.hidden isr_stub_default
|
.hidden isr_stub_default
|
||||||
.hidden isr_common_entry
|
.hidden isr_common_entry
|
||||||
|
|
||||||
@@ -155,7 +156,20 @@ isr_stub32:
|
|||||||
push 0x20
|
push 0x20
|
||||||
jmp isr_common_entry
|
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
|
.global isr_stub_default
|
||||||
isr_stub_default:
|
isr_stub_default:
|
||||||
push 0 /* fake error */
|
push 0 /* fake error */
|
||||||
@@ -177,7 +191,9 @@ isr_stub_table:
|
|||||||
.quad isr_stub28, isr_stub29, isr_stub30, isr_stub31
|
.quad isr_stub28, isr_stub29, isr_stub30, isr_stub31
|
||||||
/* Vector 32 = APIC Timer */
|
/* Vector 32 = APIC Timer */
|
||||||
.quad isr_stub32
|
.quad isr_stub32
|
||||||
/* Vectors 33-255 default to unknown stub */
|
/* Vector 33 = i8042 keyboard IRQ1 (item 4.3.5) */
|
||||||
.rept 223
|
.quad isr_stub33
|
||||||
|
/* Vectors 34-255 default to unknown stub */
|
||||||
|
.rept 222
|
||||||
.quad isr_stub_default
|
.quad isr_stub_default
|
||||||
.endr
|
.endr
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
#include "vmm.h"
|
#include "vmm.h"
|
||||||
#include "apic.h"
|
#include "apic.h"
|
||||||
#include "timer.h"
|
#include "timer.h"
|
||||||
|
#include "starkernel/ioapic.h"
|
||||||
|
#include "starkernel/i8042.h"
|
||||||
#include "kmalloc.h"
|
#include "kmalloc.h"
|
||||||
#include "starkernel/kernel_args.h"
|
#include "starkernel/kernel_args.h"
|
||||||
|
|
||||||
@@ -423,6 +425,20 @@ void kernel_main(BootInfo *boot_info) {
|
|||||||
apic_init(boot_info);
|
apic_init(boot_info);
|
||||||
console_println("APIC: init done\n");
|
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 */
|
/* M5: Timer subsystem */
|
||||||
console_println("Timer: init...");
|
console_println("Timer: init...");
|
||||||
timer_init(boot_info);
|
timer_init(boot_info);
|
||||||
@@ -640,6 +656,11 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
|||||||
/* Start heartbeat and enable interrupts */
|
/* Start heartbeat and enable interrupts */
|
||||||
console_println("Starting heartbeat...");
|
console_println("Starting heartbeat...");
|
||||||
apic_timer_start();
|
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();
|
arch_enable_interrupts();
|
||||||
console_println("Heartbeat running.");
|
console_println("Heartbeat running.");
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
#include "word_source/include/inference_words.h"
|
#include "word_source/include/inference_words.h"
|
||||||
#include "word_source/include/defer_words.h"
|
#include "word_source/include/defer_words.h"
|
||||||
#include "word_source/include/framebuffer_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
|
* @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_inference_words(vm); /* Module 26: SSM Inference + Jacquard */
|
||||||
register_defer_words(vm); /* Module 27: DEFER / IS late binding */
|
register_defer_words(vm); /* Module 27: DEFER / IS late binding */
|
||||||
register_framebuffer_words(vm); /* Module 28: Console fabric -- raw framebuffer primitives */
|
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");
|
log_message(LOG_INFO, "FORTH-79 Standard word set registration complete");
|
||||||
}
|
}
|
||||||
@@ -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 */
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user