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>
244 lines
9.6 KiB
C
244 lines
9.6 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.
|
||
*/
|
||
|
||
/**
|
||
* 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 (1–3 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();
|
||
}
|