/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James. All rights reserved. Licensed under the StarForth License, Version 1.0. */ /** * apic.c (aarch64) - Minimal GICv2 driver + ARM Generic Timer (items 0.6/0.7). * * "Minimal" is deliberate and stays deliberate: exactly one interrupt (the * non-secure EL1 physical timer PPI, or its EL2 hypervisor-timer counterpart * if ever run at EL2) is enabled. This is not a general GIC driver and must * not grow into one -- no SPI support, no SGI support, no GICv3/ITS. * * Base addresses and the PPI INTID are QEMU-virt-machine constants, not * device-tree-discovered, and that is a deliberate, recorded exception * rather than an oversight (FABRIC.md item 0.6, GAP-B1 follow-up): * `fdt_valid(boot_info->dtb)` fails on this system's aarch64 firmware * (qemu-efi-aarch64 2025.11-3ubuntu7 does not forward a devicetree to the * guest), confirmed live rather than assumed. The values below were not * recalled from memory either -- they were read out of QEMU 10.2.1's own * internal devicetree (`qemu-system-aarch64 -machine virt,dumpdtb=...`, * decoded with this tree's own fdt.c reader) and are therefore correct for * this exact QEMU version, though not guaranteed stable across others. * Register *offsets* within each block (GICD_CTLR, GICC_IAR, etc.) are * fixed by the GICv2 architecture, not the board, and were cross-checked * against Linux's own driver header (linux/irqchip/arm-gic.h, from the * linux-headers package installed on the build host) rather than recalled * either. */ #include "apic.h" #include "uefi.h" #include "console.h" #include /* Defined in arch.c (item 0.4). Same extern-in-place convention as * interrupts.c: "exception level" has no cross-ISA meaning. */ extern int aarch64_current_el(void); /* ---- QEMU virt machine constants (see file header) --------------------- */ #define GICD_BASE_PA 0x08000000UL #define GICC_BASE_PA 0x08010000UL /* Non-secure EL1 physical timer PPI = 30 (INTID 16+14); EL2 hypervisor * timer PPI = 26 (INTID 16+10). Both read directly out of QEMU's own DT * dump for the `timer` node's four-entry `interrupts` property (secure, * non-secure, virtual, hypervisor, in that architected order) -- not the * architecture-generic PPI numbers assumed from memory. */ #define TIMER_PPI_EL1 30u #define TIMER_PPI_EL2 26u /* GICv2 Distributor register byte offsets (ARM IHI 0048B; confirmed against * Linux's arm-gic.h, see file header). */ #define GICD_CTLR 0x000 #define GICD_ISENABLER0 0x100 #define GICD_IPRIORITYR 0x400 #define GICD_ICFGR 0xC00 /* GICv2 CPU Interface register byte offsets. */ #define GICC_CTLR 0x00 #define GICC_PMR 0x04 #define GICC_IAR 0x0C #define GICC_EOIR 0x10 #define GICD_CTLR_ENABLE_GRP0 (1u << 0) #define GICC_CTLR_ENABLE_GRP0 (1u << 0) /* Priority: GICv2 compares as "interrupt allowed if priority < PMR", lower * value = more urgent. One interrupt is configured, so the specific value * only has to be numerically below the PMR mask set in apic_init(). */ #define TIMER_PRIORITY 0x80u #define PMR_ALLOW_ALL 0xFFu /* MMIO accessed as identity/physical addresses: arch_mmu_init() for aarch64 * is still the M3 stub (page-table bring-up deferred), so whatever mapping * EDK2 left active at ExitBootServices governs -- the same assumption * amd64/timer.c already documents and relies on for its own MMIO (HPET). */ static inline void mmio_write32(uintptr_t base, uint32_t offset, uint32_t val) { *(volatile uint32_t *)(base + offset) = val; } static inline uint32_t mmio_read32(uintptr_t base, uint32_t offset) { return *(volatile uint32_t *)(base + offset); } /* Set once by apic_init(), read by aarch64_irq_handler() to know which PPI * to expect -- computed once (EL cannot change post-boot, same rationale * as item 0.5's el2_mode_flag) rather than re-derived per interrupt. */ static uint32_t s_timer_ppi = TIMER_PPI_EL1; static uint64_t s_timer_period_tsc = 0; /** * @brief Initialise the GICv2 distributor and CPU interface (M4 milestone). * * Enables exactly one interrupt source: the physical timer PPI for the * exception level detected in item 0.4 (30 at EL1, 26 at EL2 — see * @c TIMER_PPI_EL1 / @c TIMER_PPI_EL2 above). Sequence: * * 1. Set this interrupt's priority (@c GICD_IPRIORITYR) below the CPU * interface's priority mask, so it is never itself masked out. * 2. Enable it in @c GICD_ISENABLER0 (PPIs 16–31 live in the first word). * 3. Enable the distributor, Group 0 only (@c GICD_CTLR) — this deployment * has no GICv2 security-extension split in use. * 4. Set the CPU interface priority mask to allow everything through * (@c GICC_PMR = 0xFF) and enable the CPU interface (@c GICC_CTLR). * * Deliberately not configured: @c GICD_ICFGR (trigger sensitivity). The * ARM generic timer's PPI is architecturally level-sensitive; this is not * a general GIC driver and item 0.6's own scope does not call for * reconfiguring it. * * @param boot_info Unused — see the file header for why this does not read * @c boot_info->dtb despite item 0.6 asking for it. * @return 0 always. */ int apic_init(BootInfo *boot_info) { (void)boot_info; s_timer_ppi = (aarch64_current_el() == 2) ? TIMER_PPI_EL2 : TIMER_PPI_EL1; /* IPRIORITYR is byte-indexed, one byte per INTID. */ { uint32_t reg_off = GICD_IPRIORITYR + (s_timer_ppi & ~3u); uint32_t shift = (s_timer_ppi & 3u) * 8u; uint32_t val = mmio_read32(GICD_BASE_PA, reg_off); val = (val & ~(0xFFu << shift)) | (TIMER_PRIORITY << shift); mmio_write32(GICD_BASE_PA, reg_off, val); } mmio_write32(GICD_BASE_PA, GICD_ISENABLER0, 1u << s_timer_ppi); mmio_write32(GICD_BASE_PA, GICD_CTLR, GICD_CTLR_ENABLE_GRP0); mmio_write32(GICC_BASE_PA, GICC_PMR, PMR_ALLOW_ALL); mmio_write32(GICC_BASE_PA, GICC_CTLR, GICC_CTLR_ENABLE_GRP0); console_puts("GICv2: distributor+CPU interface enabled, PPI "); console_putc((char)('0' + s_timer_ppi / 10)); console_putc((char)('0' + s_timer_ppi % 10)); console_println(""); return 0; } /** * @brief Read the GIC CPU interface's Interrupt Acknowledge Register. * * Reading @c GICC_IAR returns the INTID of the highest-priority pending * interrupt and simultaneously moves it to the active state — this is the * GICv2 acknowledgement step. Called from @c aarch64_irq_handler() in * @c interrupts.c to identify which interrupt fired; the matching * @c apic_eoi_intid() call below completes it. * * @return INTID (10 bits used by GICv2). */ uint32_t apic_read_iar(void) { return mmio_read32(GICC_BASE_PA, GICC_IAR); } /** * @brief Return the timer PPI this build is watching for. * * Set once in @c apic_init() from @c aarch64_current_el() (30 at EL1, 26 at * EL2). @c aarch64_irq_handler() compares @c apic_read_iar()'s result * against this rather than a hardcoded constant. */ uint32_t apic_timer_ppi(void) { return s_timer_ppi; } /** * @brief Signal End of Interrupt to the GIC CPU interface. * * Writes @p intid back to @c GICC_EOIR, matching the value @c GICC_IAR * returned in @c aarch64_irq_handler(). Required after every acknowledged * interrupt or the GIC considers it permanently active and will not * re-signal it. * * @param intid INTID as read from @c GICC_IAR (10 bits used by GICv2). */ void apic_eoi_intid(uint32_t intid) { mmio_write32(GICC_BASE_PA, GICC_EOIR, intid); } /** * @brief Signal End of Interrupt (common cross-ISA signature — see below). * * The shared @c apic.h interface has no way to pass which interrupt is * being completed, but GICv2's @c GICC_EOIR needs the exact INTID @c GICC_IAR * returned, not just "an" EOI. @c aarch64_irq_handler() calls * @c apic_eoi_intid() directly with that value instead; this form exists * only to satisfy the common call site and is not used on the interrupt * path GIC brings up. */ void apic_eoi(void) { } /** * @brief Compute the expected system-counter ticks per heartbeat period. * * Computes @c s_timer_period_tsc (the expected number of @c CNTPCT_EL0 * ticks between heartbeats), used both by @c heartbeat_init() to seed the * rolling-window @c expected_delta and by @c apic_timer_start() / * @c apic_timer_rearm() (item 0.7) as the @c TVAL step written on every * arm and re-arm. * * If either @p tsc_hz or @p tick_hz is zero, a fallback of 10,000,000 * ticks is stored (approximately 160 ms at 62.5 MHz, a safe non-zero * sentinel that prevents division-by-zero in the heartbeat path). * * @param tsc_hz System counter frequency from @c timer_init() (Hz). * @param tick_hz Desired heartbeat rate (Hz); 0 → uses fallback. * @return 0 always. */ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz) { if (tick_hz > 0 && tsc_hz > 0) { s_timer_period_tsc = tsc_hz / tick_hz; } else { s_timer_period_tsc = 10000000; } return 0; } /** * @brief Arm the ARM Generic Timer and enable its interrupt (item 0.7). * * Writes @c CNTP_TVAL_EL0 (or @c CNTHP_TVAL_EL2 at EL2 — @c * aarch64_current_el(), same EL-aware discipline as items 0.4–0.6) to * @c s_timer_period_tsc, then sets @c CTL.ENABLE=1, @c CTL.IMASK=0. * * @c TVAL is a signed 32-bit *relative* countdown — writing it sets * "fire @c s_timer_period_tsc ticks from this instant," unlike riscv64's * SBI interface (item 0.3), which takes an absolute deadline the caller * must compute. No "now" reading is needed here at all. * * The @c ISB after the @c CTL write is required, not decorative: per the * architecture, and confirmed against Linux's own * @c arch_timer_reg_write_cp15() (arch/arm64/include/asm/arch_timer.h), * only the *control* register write needs synchronising before the * enable/mask state is guaranteed visible to the interrupt pipeline — * @c TVAL/@c CVAL writes do not carry the same requirement, which is why * @c apic_timer_rearm() below omits it. * * @c s_timer_period_tsc must already be set by @c apic_timer_init(). If * it is still 0, @c TVAL=0 fires immediately and continuously until * @c apic_timer_rearm() runs — a correctly-functioning if oddly-paced * timer, not a hang. */ void apic_timer_start(void) { /* CNTx_TVAL_EL is architecturally 32-bit (bits[63:32] RES0 on write), * but MSR to a system register is always a 64-bit (Xn) instruction * form -- there is no 32-bit encoding to match a narrower operand. * Truncate to 32 bits ourselves, then zero-extend back to 64 for the * operand: this supplies explicit, provably-correct zeros in the RES0 * field rather than depending on how the hardware treats whatever * upper bits a narrower operand happened to leave there -- a * distinction we could not confirm against a local authoritative * source (unlike the GICv2 offsets and CTL bit layout above, * cross-checked against Linux headers on this system; TVAL is not, * since Linux's own driver only ever uses the 64-bit CVAL form and * never exercises this path). */ uint64_t tval = (uint64_t)(uint32_t)s_timer_period_tsc; if (aarch64_current_el() == 2) { __asm__ volatile ("msr cnthp_tval_el2, %0" :: "r"(tval)); __asm__ volatile ("msr cnthp_ctl_el2, %0" :: "r"((uint64_t)1) : "memory"); } else { __asm__ volatile ("msr cntp_tval_el0, %0" :: "r"(tval)); __asm__ volatile ("msr cntp_ctl_el0, %0" :: "r"((uint64_t)1) : "memory"); } __asm__ volatile ("isb" ::: "memory"); } /** * @brief Re-arm the ARM Generic Timer for the next period (item 0.7). * * Writes a fresh @c TVAL — the entirety of a periodic re-arm, since @c TVAL * is relative to "now" rather than an absolute deadline. Unlike riscv64's * SBI re-arm (item 0.3), there is no drift-correction bookkeeping: each * write means "@c s_timer_period_tsc ticks from this instant," so time * spent servicing the interrupt before this call simply lengthens that one * period rather than accumulating error across ticks. * * **This is the one call that must never be skipped.** The ARM Generic * Timer does not auto-reload: once @c TVAL crosses zero, @c CTL.ISTATUS * latches and stays asserted — a level condition — until software writes * a new @c TVAL or clears @c CTL.ENABLE. A return path that skips this * leaves the interrupt permanently pending, which the GIC would deliver * again the instant it is EOI'd: a real interrupt storm, not a * hypothetical one (this is exactly the failure this driver was checked * against during item 0.6's own verification, and confirmed absent there * only because nothing armed the timer yet). * * Called from @c aarch64_irq_handler() (interrupts.c) before * @c heartbeat_tick() — same ordering discipline as * @c riscv64_timer_rearm() (item 0.3): re-arm first, so a fault in the * rest of the handler cannot also cost the next tick. */ void apic_timer_rearm(void) { /* uint64_t, zero-extended -- see apic_timer_start() for why. */ uint64_t tval = (uint64_t)(uint32_t)s_timer_period_tsc; if (aarch64_current_el() == 2) { __asm__ volatile ("msr cnthp_tval_el2, %0" :: "r"(tval)); } else { __asm__ volatile ("msr cntp_tval_el0, %0" :: "r"(tval)); } } /** * @brief Stop timer delivery by clearing CTL.ENABLE (item 0.7). * * Per the architecture, @c ENABLE=0 unconditionally stops the count and * de-asserts the interrupt condition regardless of @c IMASK — a single * write is sufficient, no separate mask step needed. */ void apic_timer_stop(void) { if (aarch64_current_el() == 2) { __asm__ volatile ("msr cnthp_ctl_el2, %0" :: "r"((uint64_t)0) : "memory"); } else { __asm__ volatile ("msr cntp_ctl_el0, %0" :: "r"((uint64_t)0) : "memory"); } __asm__ volatile ("isb" ::: "memory"); } /** * @brief Return the expected system-counter ticks per heartbeat period. * * Returns @c s_timer_period_tsc, set by @c apic_timer_init() as * @c tsc_hz / @c tick_hz. This value is consumed by @c heartbeat_init() * to seed the rolling-window @c expected_delta so that inter-tick * deviation measurements are relative to the correct nominal interval. * * @return Expected @c CNTPCT_EL0 ticks per heartbeat period; 10,000,000 * if frequencies were not available at init time. */ uint64_t apic_timer_period_tsc(void) { return s_timer_period_tsc; }