Files
LithosAnanake/src/starkernel/arch/aarch64/apic.c
T
Robert Allan JamesandClaude Sonnet 5 2a2d7c5e5e aarch64: GIC SPI wiring for virtio-input (item 4.3.5d)
Punch list §25 item 4.3.5d complete.

New apic_spi_enable(intid) generalizes item 0.6's PPI-only sequence to one
explicit SPI (IPRIORITYR/ISENABLER/ITARGETSR, ICFGR read-checked not
written). Verified via a software-pended SPI (GICD_ISPENDR, no device
needed) through the existing generic IRQ dispatch, which needed no changes.
Found and fixed a real bug during verification: PSTATE.I is still set at
apic_init()'s point in boot, so the first self-test run correctly latched
but never took the interrupt. Self-test code reverted after capturing
evidence -- interrupts.c has zero net diff, only apic_spi_enable() remains,
unused until 4.3.5e. Three-arch acceptance boot clean.

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

436 lines
18 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.
*/
/**
* apic.c (aarch64) - Minimal GICv2 driver + ARM Generic Timer (items 0.6/0.7/
* 4.3.5d).
*
* "Minimal" is deliberate and stays deliberate: item 0.6 enabled exactly one
* PPI (the timer); item 4.3.5d adds the ability to enable exactly one SPI
* (for virtio-keyboard-pci, item 4.3.5e), still by explicit per-INTID call,
* not a general GIC driver -- no SGI support, no GICv3/ITS, no SPI beyond
* the one 4.3.5e will actually enable.
*
* 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 "timer.h"
#include <stdint.h>
/* 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_ITARGETSR 0x800 /* item 4.3.5d -- SPI target-CPU byte array */
#define GICD_ICFGR 0xC00
#define GICD_ISPENDR 0x200 /* item 4.3.5d -- software set-pending, SPIs only */
/* 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;
static uint64_t s_counter_hz_apic = 0; /* CNTFRQ_EL0 (item 0.8/§26) */
/**
* @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 Enable and route one SPI (item 4.3.5d).
*
* Extends item 0.6's PPI-only init just far enough to bring up one Shared
* Peripheral Interrupt -- still not a general GIC driver, still one
* explicit INTID per call, same bounded-scope posture 0.6 used.
*
* Programs, for @p intid (expected range: SPIs, INTID >= 32):
* 1. @c GICD_IPRIORITYR (byte-indexed, same pattern @c apic_init() already
* uses for the timer PPI) -- @c TIMER_PRIORITY, below @c PMR_ALLOW_ALL.
* 2. @c GICD_ISENABLER at @c 0x100 + 4*(intid/32) -- PPIs/SGIs (0-31) live
* in word 0 (@c GICD_ISENABLER0); SPIs beyond use word @c intid/32, same
* architecture layout, not board-specific.
* 3. @c GICD_ITARGETSR (byte-indexed like IPRIORITYR) -- routes to CPU 0
* only; this target has no @c -smp, so no other bit is ever valid.
* 4. @c GICD_ICFGR is read back, not written, unless the readback disagrees
* with the expected level-triggered configuration -- FABRIC.md §27.5.1's
* decoded QEMU `interrupt-map` says PCI legacy INTx on this board already
* is level-triggered by default, so this keeps item 0.6's "don't touch
* ICFGR unless forced to" posture rather than writing it unconditionally.
*
* Does **not** touch @c GICD_CTLR or @c GICC_CTLR/@c PMR -- @c apic_init()
* already enabled the distributor and CPU interface for the timer PPI, and
* those are global, not per-INTID; nothing here needs to repeat that.
*
* @param intid Target SPI's INTID (32 + GIC SPI number).
*/
void apic_spi_enable(uint32_t intid)
{
{
uint32_t reg_off = GICD_IPRIORITYR + (intid & ~3u);
uint32_t shift = (intid & 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);
}
{
uint32_t reg_off = GICD_ISENABLER0 + 4u * (intid / 32u);
uint32_t bit = intid % 32u;
mmio_write32(GICD_BASE_PA, reg_off, 1u << bit);
}
{
uint32_t reg_off = GICD_ITARGETSR + (intid & ~3u);
uint32_t shift = (intid & 3u) * 8u;
uint32_t val = mmio_read32(GICD_BASE_PA, reg_off);
val = (val & ~(0xFFu << shift)) | (0x01u << shift); /* CPU 0 only */
mmio_write32(GICD_BASE_PA, reg_off, val);
}
{
uint32_t reg_off = GICD_ICFGR + 4u * (intid / 16u);
uint32_t bitpos = (intid % 16u) * 2u;
uint32_t val = mmio_read32(GICD_BASE_PA, reg_off);
uint32_t level_expected = 0u; /* level-triggered = the "trigger mode"
* bit (bit 1 of the 2-bit field) clear */
if (((val >> bitpos) & 0x2u) != level_expected) {
val = (val & ~(0x3u << bitpos)) | (level_expected << bitpos);
mmio_write32(GICD_BASE_PA, reg_off, val);
}
}
}
/**
* @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;
}
s_counter_hz_apic = tsc_hz;
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.
*
* The step written is @c heartbeat_next_period_ns() (item 0.8, §26)
* converted to @c CNTPCT_EL0 ticks via @c s_counter_hz_apic, not the fixed
* @c s_timer_period_tsc @c apic_timer_start() used for the very first arm
* -- that value remains @c apic_timer_period_tsc()'s return for
* @c heartbeat_init()'s initial @c expected_delta, unchanged.
*/
void apic_timer_rearm(void)
{
uint64_t period_ticks = (s_counter_hz_apic > 0)
? (heartbeat_next_period_ns() * s_counter_hz_apic) / 1000000000ULL
: s_timer_period_tsc;
if (period_ticks == 0) {
period_ticks = 1;
}
/* uint64_t, zero-extended -- see apic_timer_start() for why. */
uint64_t tval = (uint64_t)(uint32_t)period_ticks;
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;
}