/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. Licensed under the StarForth License, Version 1.0 */ /** * heartbeat.c - Shared M5 heartbeat / TIME-TRUST engine (punch-list item 0.8) * * One implementation of heartbeat_init()/heartbeat_ticks()/heartbeat_trust()/ * heartbeat_state(), shared by all three architectures. Each architecture's * timer.c contributes only heartbeat_read_counter() — the one thing that is * genuinely per-ISA (rdtsc / rdtime / CNTPCT_EL0). * * Top half / bottom half split (FABRIC.md §25.1 item 0.8, per the GAP-A1 * ruling in §16.4/§18.4): heartbeat_tick() is called from interrupt context * and does nothing but read the counter, bump TIME-TICKS, and latch a * pending sample. heartbeat_service() runs on the mainline (the REPL idle * loop) and does the window/variance/trust work. Neither one feeds patron * state — the engine stays on the virtual tick per §18.4, unchanged by this * file. * * Adaptive re-arm period (FABRIC.md §26, ruled 2026-08-03): Loop #7 * (vm_runtime.c) computes an execution-derived stable/volatile signal and * calls heartbeat_set_adaptive_period_ns() with it, rescaled to this file's * kernel-appropriate base (10 ms, matching the 100 Hz rate item 0.1-0.7 * configured) rather than the hosted 10 µs HEARTBEAT_TICK_NS base. Each * architecture's re-arm function reads heartbeat_next_period_ns() and * converts it to raw counter units instead of using a fixed constant. * Single writer (mainline, via vm_tick()'s Loop #7 site), single reader * (the ISR's re-arm call) — no lock needed, per §21.1's finding that * nothing here is actually concurrent on one hart. */ #include "starkernel/timer.h" #if defined(ARCH_AMD64) #define HEARTBEAT_HAS_VARIANCE 1 #else #define HEARTBEAT_HAS_VARIANCE 0 #endif /* Kernel-side adaptive-period base: 10 ms, matching the 100 Hz hardware * rate established throughout items 0.1-0.7. Deliberately NOT * HEARTBEAT_TICK_NS (include/starforth_config.h) -- that constant is the * hosted pthread-worker's 10 µs base and is three orders of magnitude too * fast for a bare-metal ISR period (FABRIC.md §26.3). Loop #7's decision * logic is reused unmodified; only the base it scales differs. */ #define HEARTBEAT_BASE_PERIOD_NS 10000000ULL static TimeTrustState g_heartbeat; /* Top/bottom half handoff. */ static volatile uint64_t g_pending_counter; static volatile int g_pending_valid; /* Adaptive re-arm period, clamped to the same [1/4x, 4x] band Loop #7 * itself enforces on tick_target_ns, so a caller cannot runaway the ISR * rate even if it forwarded an unclamped value. */ static volatile uint64_t g_adaptive_period_ns = HEARTBEAT_BASE_PERIOD_NS; #if HEARTBEAT_HAS_VARIANCE /** * @brief Push a signed delta value into the heartbeat rolling window. */ static void window_push(TimeWindow *w, int64_t delta) { w->deltas[w->pos] = delta; w->pos = (w->pos + 1) % TIME_WINDOW_SIZE; if (w->count < TIME_WINDOW_SIZE) { w->count++; } } /** * @brief Compute the relative variance of heartbeat deltas as a Q48.16 value. */ static q48_16_t window_variance_q48(const TimeWindow *w, uint64_t expected_delta) { if (w->count < 2 || expected_delta == 0) { return 0; } int64_t sum = 0; for (uint32_t i = 0; i < w->count; i++) { sum += w->deltas[i]; } int64_t mean = sum / (int64_t)w->count; uint64_t sum_sq = 0; for (uint32_t i = 0; i < w->count; i++) { int64_t diff = w->deltas[i] - mean; if (diff > 0x7FFFFFFF) diff = 0x7FFFFFFF; if (diff < -0x7FFFFFFF) diff = -0x7FFFFFFF; sum_sq += (uint64_t)(diff * diff); } uint64_t var_tsc = sum_sq / w->count; uint64_t exp_sq = expected_delta; if (exp_sq > 0xFFFFFFFF) { var_tsc >>= 16; exp_sq >>= 8; } exp_sq = exp_sq * exp_sq; if (exp_sq == 0) return 0; if (var_tsc > 0x0000FFFFFFFFFFFFULL) { var_tsc = 0x0000FFFFFFFFFFFFULL; } return (var_tsc << 16) / exp_sq; } /** * @brief Derive TIME-TRUST from a Q48.16 relative variance value. */ static q48_16_t variance_to_trust(q48_16_t variance) { q48_16_t denom = q48_add(Q48_ONE, variance); if (denom == 0) { return Q48_ONE; } return q48_div(Q48_ONE, denom); } #endif /* HEARTBEAT_HAS_VARIANCE */ /** * @brief Initialise the M5 heartbeat / TIME-TRUST subsystem. * * Shared across all three architectures -- the fallback and calibration * formula never differed between them, only what heartbeat_service() does * with the resulting window did. */ void heartbeat_init(uint64_t tsc_hz, uint64_t tick_hz) { g_heartbeat.ticks = 0; g_heartbeat.last_tsc = 0; g_heartbeat.total_samples = 0; g_heartbeat.variance = 0; g_heartbeat.trust = Q48_ONE; g_heartbeat.window.pos = 0; g_heartbeat.window.count = 0; for (int i = 0; i < TIME_WINDOW_SIZE; i++) { g_heartbeat.window.deltas[i] = 0; } g_heartbeat.expected_delta = (tick_hz > 0 && tsc_hz > 0) ? (tsc_hz / tick_hz) : 10000000ULL; g_pending_valid = 0; g_adaptive_period_ns = HEARTBEAT_BASE_PERIOD_NS; } void heartbeat_tick(void) { g_pending_counter = heartbeat_read_counter(); g_heartbeat.ticks++; g_pending_valid = 1; } void heartbeat_service(void) { if (!g_pending_valid) { return; } uint64_t now = g_pending_counter; g_pending_valid = 0; TimeTrustState *s = &g_heartbeat; s->total_samples++; if (s->total_samples == 1) { s->last_tsc = now; return; } uint64_t actual_delta = now - s->last_tsc; s->last_tsc = now; int64_t deviation = (int64_t)actual_delta - (int64_t)s->expected_delta; #if HEARTBEAT_HAS_VARIANCE window_push(&s->window, deviation); s->variance = window_variance_q48(&s->window, s->expected_delta); s->trust = variance_to_trust(s->variance); #else /* RISC-V `time` / AArch64 CNTPCT_EL0 are architecturally invariant * counters -- no statistical quality estimate needed (matches the * per-arch rationale this file replaces). */ (void)deviation; s->trust = Q48_ONE; #endif } uint64_t heartbeat_ticks(void) { return g_heartbeat.ticks; } time_trust_t heartbeat_trust(void) { return g_heartbeat.trust; } const TimeTrustState *heartbeat_state(void) { return &g_heartbeat; } /** * @brief Set the adaptive re-arm period (FABRIC.md §26). * * Called from vm_runtime.c's Loop #7 site on the mainline execution path * (never interrupt context) with a value already rescaled to this file's * HEARTBEAT_BASE_PERIOD_NS. Clamped defensively to the same [1/4x, 4x] * band Loop #7 itself enforces around its own base, so a caller forwarding * an unclamped or wrongly-scaled value cannot run the ISR away. * * @param ns Desired period in nanoseconds for the next re-arm. */ void heartbeat_set_adaptive_period_ns(uint64_t ns) { uint64_t lo = HEARTBEAT_BASE_PERIOD_NS / 4; uint64_t hi = HEARTBEAT_BASE_PERIOD_NS * 4; if (ns < lo) ns = lo; if (ns > hi) ns = hi; g_adaptive_period_ns = ns; } /** * @brief Read the period the next hardware re-arm should use. * * Called from interrupt context by each architecture's re-arm function * (apic_timer_rearm(), riscv64_timer_rearm(), the AArch64 equivalent) in * place of the fixed constant those functions used before item 0.8. * * @return Current adaptive period in nanoseconds. */ uint64_t heartbeat_next_period_ns(void) { return g_adaptive_period_ns; }