/* 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 #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(); }