Files
LithosAnanke/src/starkernel/arch/aarch64/interrupts.c
T
Robert Allan JamesandClaude Sonnet 5 a373c9124a aarch64: virtio-keyboard-pci, interrupt-driven keyboard input (item 4.3.5e)
Punch list §25 item 4.3.5e complete.

Extended virtio_input.c with a GIC-routed interrupt path alongside 4.3.5c's
PLIC one -- same capability walk, feature negotiation, eventq handling
(confirming §27.5.1's prediction that these items would share most of the
driver). aarch64_irq_handler() dispatches to virtio_input_isr() before its
EOIR write, same claim-dispatch-complete ordering riscv64 uses. Verified
with a real QEMU sendkey keypress: exact KEY_A/press match, two real
interrupts serviced, identical result to riscv64. Found (not fixed) an
unrelated pre-existing bug: BYE's cold-reset path faults on aarch64,
discovered incidentally since nobody had exercised it from a monitored
session before. Three-arch acceptance boot clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 12:29:27 -04:00

214 lines
8.9 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* interrupts.c (aarch64) - Exception vector setup and common handler
*/
#include <stdint.h>
#include "arch.h"
#include "console.h"
#include "starkernel/timer.h"
volatile const char *g_sk_fault_word = (void *)0;
/* Defined in isr.S */
extern void aarch64_install_vectors(void);
/* Defined in arch.c. Extern-declared here rather than in the shared arch.h:
* "exception level" has no amd64/riscv64 equivalent, so it does not belong
* in the cross-ISA header — same convention as riscv64_timer_rearm() in
* arch/riscv64/interrupts.c. */
extern int aarch64_current_el(void);
/* Defined in apic.c (items 0.6/0.7). Same extern-in-place convention: GICv2
* and generic-timer register-level details belong to the file that owns
* the MMIO base addresses / EL-aware register selection, not the shared
* apic.h. */
extern uint32_t apic_read_iar(void);
extern uint32_t apic_timer_ppi(void);
extern void apic_eoi_intid(uint32_t intid);
extern void apic_timer_rearm(void);
/* Defined in virtio_input.c (item 4.3.5e). g_virtio_input_gic_intid is 0
* (not a valid SPI INTID -- SPIs start at 32) until
* virtio_input_find_keyboard() finds a real device and computes its routed
* INTID, so this branch is inert on any boot where the device isn't
* present. Same extern-in-place convention as the rest of this file. */
extern uint32_t g_virtio_input_gic_intid;
extern void virtio_input_isr(void);
/* INTID 1023 = "spurious" (GICv2 spec): the CPU interface has nothing
* pending, typically because another interrupt at the same or higher
* priority raced this one to acknowledgement. Architecturally defined,
* not this board's invention, so it is named rather than left as a magic
* number in the comparison below. */
#define GIC_INTID_SPURIOUS 1023u
/**
* @brief Dispatch an IRQ taken at current-EL with SPx (punch-list item 0.7).
*
* Called from @c irq_spx_trampoline in @c isr.S, which has already saved the
* full caller-saved integer and FP/SIMD register sets plus @c ELR_ELx /
* @c SPSR_ELx, and will restore all of it and @c ERET after this function
* returns — unlike @c aarch64_exception_handler(), this path is designed to
* return normally.
*
* Reads @c GICC_IAR once (the GICv2 acknowledgement step — this also moves
* the interrupt to the active state), dispatches on the returned INTID, and
* always completes with the matching @c GICC_EOIR write, spurious or not:
* a spurious read must still not be treated as "nothing to clean up," and
* an unrecognised real INTID still needs the CPU interface unblocked for
* the next one.
*
* For the timer PPI (@c apic_timer_ppi() — 30 at EL1, 26 at EL2, decided
* once in @c apic_init()): @c apic_timer_rearm() runs **first**, before
* @c heartbeat_tick() — the ARM Generic Timer does not auto-reload, so
* skipping this leaves the interrupt condition permanently latched (see
* @c apic_timer_rearm()'s own doc comment). Ordering the re-arm ahead of
* the rest of the dispatch means a fault in @c heartbeat_tick() cannot
* also cost the next tick — same discipline as @c riscv64_timer_rearm()
* (item 0.3).
*
* virtio-input (item 4.3.5e, @c g_virtio_input_gic_intid): dispatched
* **before** the @c GICC_EOIR write below, not after — the ISR itself is
* what deasserts the device's (level-triggered) interrupt line by reading
* its ISR-status capability; EOI'ing first would let the GIC immediately
* re-signal a still-asserted line. Same claim-dispatch-complete ordering
* @c riscv64_interrupt_handler() uses for its own PLIC-routed devices.
*/
void aarch64_irq_handler(void)
{
uint32_t intid = apic_read_iar();
if (intid == apic_timer_ppi()) {
apic_timer_rearm();
heartbeat_tick();
}
if (intid == g_virtio_input_gic_intid) {
virtio_input_isr();
}
if (intid != GIC_INTID_SPURIOUS) {
apic_eoi_intid(intid);
}
}
/**
* @brief Print a 64-bit value as "0xNNNNNNNNNNNNNNNN" to the kernel console.
*
* Formats @p val as a 16-nibble lowercase hex string preceded by "0x" into
* a 19-byte stack buffer (including null terminator) then emits it via
* @c console_puts(). Used by @c aarch64_exception_handler() to print
* @c ESR_EL1, @c ELR_EL1, @c FAR_EL1, and @c SPSR_EL1 in the exception
* diagnostic dump. No heap allocation or libc dependency.
*
* @param val 64-bit value to format.
*/
static void print_hex(uint64_t val)
{
char buf[19];
buf[0] = '0'; buf[1] = 'x';
for (int i = 15; i >= 0; i--) {
int nibble = (int)(val & 0xF);
buf[2 + i] = (char)(nibble < 10 ? '0' + nibble : 'a' + nibble - 10);
val >>= 4;
}
buf[18] = '\0';
console_puts(buf);
}
/**
* @brief AArch64 common exception handler — print diagnostic and spin.
*
* Called from the @c isr.S exception vector table stubs for all exception
* types (synchronous, IRQ, FIQ, SError) that are not handled by a specific
* driver. Reads the four key EL1 exception system registers directly:
*
* - @c ESR_EL1 — Exception Syndrome Register: encodes the exception class
* (EC, bits [31:26]) and instruction-specific syndrome (ISS, bits [24:0]).
* The EC identifies the cause (e.g., 0x21 = data abort from EL1, 0x25 =
* data abort from EL0, 0x15 = SVC64).
* - @c ELR_EL1 — Exception Link Register: the PC of the instruction that
* caused the exception (or the instruction after, for IRQ/FIQ/SError).
* - @c FAR_EL1 — Fault Address Register: the virtual address that triggered
* a Data Abort or Instruction Abort; undefined for other exception types.
* - @c SPSR_EL1 — Saved Program Status Register: the PSTATE at the time of
* exception, including the mode field and interrupt-mask bits.
*
* Prints all four values to the kernel console then enters an infinite
* @c WFE loop to halt the affected core permanently. All exceptions that
* reach this handler are fatal — there is no recovery path.
*
* The global @c g_sk_fault_word is set by the VM FORTH dispatcher just
* before calling each word's function pointer; it names the word executing
* at the time of the fault, aiding post-mortem analysis from the serial log.
* (Currently logged only on the amd64 #GP path; future AArch64 diagnostic
* expansion can include it here.)
*/
void aarch64_exception_handler(void)
{
uint64_t esr, elr, far_val, spsr;
__asm__ volatile ("mrs %0, esr_el1" : "=r"(esr));
__asm__ volatile ("mrs %0, elr_el1" : "=r"(elr));
__asm__ volatile ("mrs %0, far_el1" : "=r"(far_val));
__asm__ volatile ("mrs %0, spsr_el1" : "=r"(spsr));
console_println("*** EXCEPTION (aarch64) ***");
console_puts(" ESR_EL1 = "); print_hex(esr); console_puts("\n");
console_puts(" ELR_EL1 = "); print_hex(elr); console_puts("\n");
console_puts(" FAR_EL1 = "); print_hex(far_val); console_puts("\n");
console_puts(" SPSR_EL1= "); print_hex(spsr); console_puts("\n");
for (;;) {
__asm__ volatile ("wfe" ::: "memory");
}
}
/**
* @brief Install the AArch64 exception vector table (M4 milestone).
*
* First reads and reports the exception level via @c aarch64_current_el()
* (punch-list item 0.4) — this is the earliest point in boot with both a
* working console (up since M1) and a genuine need for the answer (vector
* installation is the first EL-dependent operation). The read itself is
* cheap and safe far earlier than this; only the console print is gated on
* this call site.
*
* Then calls @c aarch64_install_vectors() (defined in @c isr.S) which
* writes the address of the vector table base into @c VBAR_EL1 (Vector
* Base Address Register, EL1). After this instruction all EL1 exceptions
* (synchronous, IRQ, FIQ, SError) dispatch through the 512-byte-aligned
* vector table assembled in @c isr.S.
*
* **@c VBAR_EL1 is written unconditionally regardless of the detected
* level.** If @c aarch64_current_el() reports EL2, this is a known gap:
* exceptions taken at EL2 vector through @c VBAR_EL2, which nothing here
* programs, and installing EL2-aware vectors is punch-list item 0.5's
* scope, not this one's. Item 0.4 establishes the detection every later
* EL-dependent item must consult; it does not yet make use of it here.
*
* On AArch64 there is no IDT and no PIC to disable; the GIC replaces
* both. The @c arch_interrupts_init() name is kept identical across all
* ISAs so that @c kernel_main() can call the same symbol regardless of
* the target architecture.
*
* Must be called after the kernel stack is established (so that the EL1
* SP_EL1 is valid) and before @c arch_enable_interrupts().
*/
void arch_interrupts_init(void)
{
int el = aarch64_current_el();
console_puts("AArch64: running at EL");
console_putc((char)('0' + el));
console_println(el == 2
? " (VBAR_EL2/CNTHP_*_EL2 required — not yet wired, see item 0.5/0.7)"
: "");
aarch64_install_vectors();
}