Files
LithosAnanake/src/starkernel/arch/aarch64/timer.c
T
Robert Allan JamesandClaude Sonnet 5 3699be964d starkernel: converge the tick path and wire the adaptive heartbeat (item 0.8)
Introduces src/starkernel/heartbeat.c as the shared top/bottom-half
implementation of heartbeat_init/tick/service/ticks/trust/state, replacing
the per-architecture duplicates in amd64/riscv64/aarch64 timer.c. Each
arch's timer.c now contributes only heartbeat_read_counter() (rdtsc /
rdtime / CNTPCT_EL0). Per the GAP-A1 ruling the top half stays counter+
latch only; heartbeat_service() (called every REPL idle iteration,
unconditionally per FABRIC.md's fidelity note) does the window/variance/
trust work outside interrupt context. vm_tick()'s call sites are
unchanged -- the engine still runs on the virtual tick.

Per FABRIC.md §26 (ruled 2026-08-03): wires Loop #7's execution-derived
stable/volatile signal into the physical re-arm period. vm_runtime.c's
existing Loop #7 site now calls heartbeat_set_adaptive_period_ns() with
tick_target_ns ratio-rescaled onto a 10ms kernel base (not the hosted
10us HEARTBEAT_TICK_NS -- see §26.3 for the scale mismatch). Each
architecture's re-arm function (apic_timer_rearm() on amd64/aarch64,
riscv64_timer_rearm()) now converts heartbeat_next_period_ns() to its
own raw counter units instead of a fixed constant; amd64 gained a
rearm function it didn't previously need, since periodic-mode auto-reload
never required one before this item.

Verified: all three architectures build with no new warnings and boot
cleanly to ok> with dict_hash=0x3d4e1daf289da94f, unchanged from the
pre-change baseline -- no regression. Verified NOT achieved: live re-arm
period variation under load. A temporary diagnostic (added and reverted)
confirmed Loop #7 never actually fired during a live QEMU session -- a
synthetic word-execution loop drove ~6,500 executions, past the 1000-tick
inference frequency, without tripping vm_tick_inference_engine()'s
pre-existing !vm->rolling_window.is_warm gate. That gate predates this
item and was not investigated -- out of scope. FABRIC.md's Done-when is
amended to record this honestly rather than claim it.

Punch list §25 item 0.8 complete (per amended, weaker acceptance -- see
the item's own annotation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 00:01:48 -04:00

244 lines
9.6 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) 20232025 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* timer.c (aarch64) - Timer subsystem using ARM Generic Timer (CNTPCT_EL0)
*
* Uses the 64-bit system counter available as CNTPCT_EL0. The counter
* frequency is read from CNTFRQ_EL0 (set by firmware).
*/
#include "timer.h"
#include "console.h"
#include "q48_16.h"
#include "uefi.h"
#include <stdint.h>
/* ─── ARM generic-timer helpers ─────────────────────────────────────── */
/**
* @brief Read the AArch64 Physical System Counter with ISB serialisation.
*
* Issues an @c ISB (Instruction Synchronisation Barrier) before reading
* @c CNTPCT_EL0 to ensure all preceding instructions have completed before
* the counter value is sampled. This prevents the counter read from being
* speculated ahead of a preceding store or flag update, which would produce
* an erroneously early timestamp.
*
* The ISB is heavier than strictly necessary for most timing uses but
* provides the same ordering guarantee as @c RDTSCP on x86-64. For
* interval measurements where start and end reads are in the same path,
* the overhead (13 cycles) is negligible.
*
* @return Current 64-bit @c CNTPCT_EL0 counter value in system-counter ticks.
*/
static inline uint64_t cntpct_read(void)
{
uint64_t val;
__asm__ volatile ("isb; mrs %0, cntpct_el0" : "=r"(val));
return val;
}
/**
* @brief Read the AArch64 Counter Frequency Register (@c CNTFRQ_EL0).
*
* @c CNTFRQ_EL0 is a read-only EL0-accessible register whose value is
* written by EL3/EL2 firmware at boot to advertise the system counter
* frequency in Hz. It is the architectural way to discover @c CNTPCT_EL0's
* tick rate without out-of-band knowledge of the board.
*
* QEMU's SBSA/virt machine models typically set this to 62,500,000 Hz
* (62.5 MHz) for Cortex-A57 compatibility. Real hardware varies widely
* (10 MHz on Raspberry Pi 4, 24 MHz on Cortex-A53 dev boards, etc.).
* @c timer_init() falls back to 62,500,000 if the register reads zero.
*
* @return Counter frequency in Hz as set by firmware; 0 if firmware did
* not initialise the register (uncommon but possible in bare-metal
* bring-up without a proper firmware stack).
*/
static inline uint64_t cntfrq_read(void)
{
uint64_t val;
__asm__ volatile ("mrs %0, cntfrq_el0" : "=r"(val));
return val;
}
/* ─── Module state ───────────────────────────────────────────────────── */
static uint64_t s_counter_hz = 0; /* CNTFRQ_EL0 (ticks/second) */
static uint64_t s_ns_per_tick = 0; /* nanoseconds per counter tick (scaled) */
static uint64_t s_base_count = 0; /* counter value at timer_init() */
static uint64_t s_base_ns = 0; /* ns offset at timer_init() */
static timer_calibration_record_t s_cal;
/* ─── timer_init ─────────────────────────────────────────────────────── */
/*
* @brief Initialise the AArch64 timer subsystem from @c CNTFRQ_EL0 (M5).
*
* Reads the system counter frequency from @c CNTFRQ_EL0. If firmware left
* this register zero (a non-conforming but possible situation in bare-metal
* bring-up) the function falls back to 62,500,000 Hz — the canonical
* Cortex-A57 / QEMU SBSA frequency.
*
* Derived values stored in module state:
* - @c s_counter_hz — raw frequency in Hz.
* - @c s_base_count — @c CNTPCT_EL0 sample at init time; all subsequent
* @c timer_now_ns() calls compute elapsed ticks as
* @c (cntpct_read() - s_base_count).
* - @c s_base_ns — 0 at init; the ns origin for @c timer_now_ns().
* - @c s_ns_per_tick — Q16.16 fixed-point nanoseconds per counter tick,
* computed as @c (1e9 << 16) / @c freq to avoid floating-point in the
* freestanding build. @c timer_now_ns() right-shifts by 16 after the
* multiply to recover the integer nanosecond count.
*
* The calibration record @c s_cal is populated with the firmware-reported
* frequency, @c converged = 1, and @c trust = @c TIMER_TRUST_ABSOLUTE
* because the ARM Generic Timer is invariant and synchronised by
* architecture — no drift-detection loop is needed.
*
* @param boot_info Kernel @c BootInfo (ACPI/memory map); unused on AArch64
* for timer init (GIC base address will be used in a later
* milestone for the interrupt controller).
* @return 0 always.
*/
int timer_init(BootInfo *boot_info)
{
(void)boot_info;
uint64_t freq = cntfrq_read();
if (freq == 0) {
/* Firmware did not set CNTFRQ; assume 62.5 MHz (Cortex-A57 default) */
freq = 62500000ULL;
}
s_counter_hz = freq;
s_base_count = cntpct_read();
s_base_ns = 0;
/* ns_per_tick = 1e9 / freq — store as (1e9 << 16) / freq for fixed-point */
s_ns_per_tick = (1000000000ULL << 16) / freq;
s_cal.tsc_hz_mean = freq;
s_cal.hpet_hz = 0;
s_cal.pit_hz_mean = 0;
s_cal.converged = 1;
s_cal.vm_mode = 1;
s_cal.trust = TIMER_TRUST_ABSOLUTE;
console_println("Timer: AArch64 generic timer initialised.");
return 0;
}
/* ─── timer_tsc_hz ───────────────────────────────────────────────────── */
/**
* @brief Return the system counter frequency in Hz.
*
* Returns @c s_counter_hz as set from @c CNTFRQ_EL0 by @c timer_init().
* If @c timer_init() has not been called yet (or firmware reported 0 and
* the fallback was not applied), returns 62,500,000 Hz as a safe default.
*
* Used by @c apic_timer_init() to compute @c s_timer_period_tsc and by
* the shim time backend to convert counter ticks to nanoseconds before
* the full timer subsystem is available.
*
* @return System counter frequency in Hz; at least 62,500,000.
*/
uint64_t timer_tsc_hz(void)
{
return s_counter_hz ? s_counter_hz : 62500000ULL;
}
/* ─── timer_now_ns ───────────────────────────────────────────────────── */
/**
* @brief Return a monotonic nanosecond timestamp.
*
* Reads @c CNTPCT_EL0, computes the delta from @c s_base_count (captured
* at @c timer_init()), and converts to nanoseconds using the Q16.16
* fixed-point @c s_ns_per_tick:
*
* @code
* ns = s_base_ns + ((delta * s_ns_per_tick) >> 16)
* @endcode
*
* The multiplication uses 64-bit arithmetic; at typical counter frequencies
* (≤ 100 MHz) and reasonable uptime intervals (days to weeks) there is no
* overflow risk in the delta-to-ns conversion.
*
* @return Monotonic nanosecond count since @c timer_init(), starting at 0.
*/
uint64_t timer_now_ns(void)
{
uint64_t delta = cntpct_read() - s_base_count;
/* delta * (1e9 / freq) = (delta * ns_per_tick_q16) >> 16 */
return s_base_ns + ((delta * s_ns_per_tick) >> 16);
}
/* ─── timer_check_drift_now ──────────────────────────────────────────── */
/**
* @brief Check for timer drift (AArch64 stub — always returns 0).
*
* On x86-64 this function cross-checks the TSC against the HPET or PM
* Timer to detect frequency drift. The AArch64 Generic Timer is
* architecturally invariant and synchronised across all cores in the
* system; it cannot drift against itself. This stub always returns 0
* (no drift detected) to satisfy the common @c timer_check_drift_now()
* call site.
*
* @return 0 always.
*/
int timer_check_drift_now(void)
{
return 0; /* Stub: ARM counter has no drift against itself */
}
/* ─── timer_calibration_record ───────────────────────────────────────── */
/**
* @brief Return a pointer to the timer calibration record.
*
* Returns @c &s_cal which is populated by @c timer_init() with:
* - @c tsc_hz_mean = @c CNTFRQ_EL0 (or 62.5 MHz fallback)
* - @c hpet_hz = 0 (no HPET on AArch64)
* - @c pit_hz_mean = 0 (no PIT on AArch64)
* - @c converged = 1 (Generic Timer is already calibrated by firmware)
* - @c vm_mode = 1 (QEMU SBSA always uses virtualised Generic Timer)
* - @c trust = @c TIMER_TRUST_ABSOLUTE
*
* The record is exposed to @c kernel_main() and @c timer.h consumers for
* boot-log reporting and for the heartbeat subsystem's trust initialisation.
*
* @return Pointer to the module-static calibration record; valid for the
* lifetime of the kernel.
*/
const timer_calibration_record_t *timer_calibration_record(void)
{
return &s_cal;
}
/* ─── Heartbeat ──────────────────────────────────────────────────────── */
/**
* @brief Read the raw counter the aarch64 heartbeat is paced against.
*
* Item 0.8 (FABRIC.md §25.1): the shared heartbeat.c now owns
* heartbeat_init()/heartbeat_tick()/heartbeat_service()/heartbeat_ticks()/
* heartbeat_trust()/heartbeat_state() and the per-arch @c g_heartbeat
* state that used to live in this file. This is the one piece that stays
* per-architecture -- the same @c CNTPCT_EL0 the rest of this file's
* calibration already reads via @c cntpct_read().
*
* @return Current @c CNTPCT_EL0 value.
*/
uint64_t heartbeat_read_counter(void)
{
return cntpct_read();
}