Punch list §25 item 0.7 complete. This is what finally makes items 0.5 and 0.6 provably work end to end. apic_timer_start(): writes CNTP_TVAL_EL0 (or CNTHP_TVAL_EL2 at EL2 -- aarch64_current_el(), same EL-aware discipline as 0.4-0.6) to s_timer_period_tsc, then CTL.ENABLE=1/IMASK=0, followed by an ISB. The ISB is not decorative: confirmed against Linux's own arch_timer_reg_write_cp15() (arch/arm64/include/asm/arch_timer.h) that only the *control* register write needs synchronising before the enable/mask state is guaranteed visible to the interrupt pipeline -- TVAL/CVAL writes do not carry the same requirement, which is why apic_timer_rearm() omits it. TVAL is architecturally 32-bit but MSR-to-system-register is always a 64-bit instruction form -- passing a uint32_t operand directly failed to build (-Wasm-operand-widths). Fixed by truncating to 32 bits ourselves then zero-extending back to 64 for the operand, which supplies explicit, provably-correct zeros in the RES0 upper field rather than depending on unverified hardware behaviour -- Linux's own driver never exercises this path (it always uses the 64-bit CVAL form instead), so there was no local source to confirm the alternative against. apic_timer_rearm() (new): re-writes TVAL only, no ISB needed. TVAL is relative to "now," not an absolute deadline like riscv64's SBI interface (item 0.3), so there is no drift-correction bookkeeping -- each write means "N ticks from this instant." Wired into aarch64_irq_handler() and called *first*, before heartbeat_tick(), matching riscv64_timer_rearm()'s ordering discipline exactly: the ARM Generic Timer does not auto-reload, so a return path that skips this leaves the interrupt condition latched, which the GIC would redeliver the instant it's EOI'd -- a real storm, the same class of failure item 0.6's verification investigated (and that time found absent, because nothing was armed yet). Verified: builds clean; every generated instruction checked against disassembly, not just reviewed by eye (both EL branches, correct TVAL/CTL register names, single shared ISB in apic_timer_start(), no ISB in apic_timer_rearm()). Boots to ok> with no regression, dict_hash 0x3d4e1daf289da94f unchanged. Rate measured directly against real wall-clock time via QEMU's own -d int trap trace (same method as riscv64's item 0.3), two independent windows: 1,090 interrupts over 11.05 s (98.679 Hz) and 4,031 over 40.88 s (98.614 Hz) -- consistent across both, so this is a real, small, systematic bias (~1.3-1.4% slow), not measurement noise from polling granularity, which would have shrunk with the longer window and did not. Attributed to genuine per-interrupt service latency: TVAL is rewritten mid-ISR, so the trampoline save/restore, GICC_IAR read, EL branch and GICC_EOIR write all lengthen the effective period slightly versus the nominal 10 ms, inherent to any relative-countdown re-arm scheme. Reported as measured, not smoothed over. The interrupt sustained continuously across both windows with no stall and no storm, which is the primary evidence re-arm-every-tick is correct; the small rate bias is overhead, not a defect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
195 lines
8.0 KiB
C
195 lines
8.0 KiB
C
/*
|
||
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);
|
||
|
||
/* 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).
|
||
*/
|
||
void aarch64_irq_handler(void)
|
||
{
|
||
uint32_t intid = apic_read_iar();
|
||
|
||
if (intid == apic_timer_ppi()) {
|
||
apic_timer_rearm();
|
||
heartbeat_tick();
|
||
}
|
||
|
||
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();
|
||
}
|