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>
353 lines
14 KiB
C
353 lines
14 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.
|
||
*/
|
||
|
||
/**
|
||
* 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 <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_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;
|
||
}
|