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>
This commit is contained in:
Robert Allan James
2026-08-04 00:01:48 -04:00
co-authored by Claude Sonnet 5
parent da4cb14702
commit 3699be964d
23 changed files with 42170 additions and 449 deletions
+16 -1
View File
@@ -32,6 +32,7 @@
#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
@@ -92,6 +93,7 @@ static inline uint32_t mmio_read32(uintptr_t base, uint32_t offset)
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).
@@ -227,6 +229,7 @@ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz)
} else {
s_timer_period_tsc = 10000000;
}
s_counter_hz_apic = tsc_hz;
return 0;
}
@@ -305,11 +308,23 @@ void apic_timer_start(void)
* @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)s_timer_period_tsc;
uint64_t tval = (uint64_t)(uint32_t)period_ticks;
if (aarch64_current_el() == 2) {
__asm__ volatile ("msr cnthp_tval_el2, %0" :: "r"(tval));
+10 -123
View File
@@ -75,10 +75,6 @@ static uint64_t s_base_ns = 0; /* ns offset at timer_init() */
static timer_calibration_record_t s_cal;
/* ─── Heartbeat state ────────────────────────────────────────────────── */
static TimeTrustState g_heartbeat;
/* ─── timer_init ─────────────────────────────────────────────────────── */
/*
@@ -229,128 +225,19 @@ const timer_calibration_record_t *timer_calibration_record(void)
/* ─── Heartbeat ──────────────────────────────────────────────────────── */
/*
* @brief Initialise the heartbeat rolling-window state.
*
* Zeroes the @c TimeTrustState @c g_heartbeat and sets up the expected
* inter-tick interval as @c tsc_hz / @c tick_hz system-counter ticks. On
* AArch64 "TSC" refers to @c CNTPCT_EL0; the variable name is preserved
* for cross-ISA consistency.
*
* If either @p tsc_hz or @p tick_hz is zero, @c expected_delta falls back
* to 10,000,000 ticks (≈ 160 ms at 62.5 MHz) which is a safe non-zero
* sentinel preventing division-by-zero in the rolling window.
*
* Initial @c trust is set to @c Q48_ONE (full trust) because the ARM
* Generic Timer is invariant by architecture and requires no initial
* warm-up period unlike the x86 TSC.
*
* @param tsc_hz System counter frequency (Hz); from @c timer_tsc_hz().
* @param tick_hz Heartbeat interrupt rate (Hz); from @c apic_timer_init().
*/
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;
}
/**
* @brief Record one heartbeat tick and update the inter-tick deviation window.
* @brief Read the raw counter the aarch64 heartbeat is paced against.
*
* Called from the timer ISR (or its AArch64 stub equivalent) at each
* periodic heartbeat. Reads @c CNTPCT_EL0 and, if @c last_tsc is
* non-zero, computes the signed deviation:
* 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().
*
* @code
* delta = (now - last_tsc) - expected_delta
* @endcode
*
* Stores @p delta into the circular @c window.deltas[] buffer at position
* @c (window.pos % TIME_WINDOW_SIZE) and advances the pointer. On AArch64,
* @c trust is unconditionally set to @c Q48_ONE (full confidence) because
* the Generic Timer is invariant and needs no statistical quality estimate.
* The x86-64 path uses @c variance_to_trust() instead.
*
* Increments both @c total_samples (lifetime count) and @c ticks (monotonic
* heartbeat counter used by @c heartbeat_ticks()).
* @return Current @c CNTPCT_EL0 value.
*/
void heartbeat_tick(void)
uint64_t heartbeat_read_counter(void)
{
uint64_t now = cntpct_read();
if (g_heartbeat.last_tsc != 0) {
int64_t delta = (int64_t)(now - g_heartbeat.last_tsc)
- (int64_t)g_heartbeat.expected_delta;
uint32_t pos = g_heartbeat.window.pos % TIME_WINDOW_SIZE;
g_heartbeat.window.deltas[pos] = delta;
g_heartbeat.window.pos++;
if (g_heartbeat.window.count < TIME_WINDOW_SIZE) {
g_heartbeat.window.count++;
}
g_heartbeat.total_samples++;
}
g_heartbeat.last_tsc = now;
g_heartbeat.ticks++;
g_heartbeat.trust = Q48_ONE; /* Simplified: full trust on ARM */
}
/**
* @brief Return the total number of heartbeat ticks since @c heartbeat_init().
*
* @c ticks is incremented unconditionally on every @c heartbeat_tick() call,
* including the first tick where no delta is recorded (because @c last_tsc
* is still zero). It therefore counts timer interrupts from boot, not valid
* delta samples.
*
* @return Monotonic tick counter; starts at 0, incremented at each heartbeat.
*/
uint64_t heartbeat_ticks(void)
{
return g_heartbeat.ticks;
}
/**
* @brief Return the current TIME-TRUST quality metric in Q48.16 format.
*
* On AArch64 this always returns @c Q48_ONE (the value 1.0 in Q48.16,
* i.e., @c 0x0000000000010000) because the ARM Generic Timer is invariant
* by architecture and no statistical quality degradation is expected.
*
* The x86-64 implementation derives trust from the rolling-window variance
* of inter-tick TSC deviations using @c variance_to_trust().
*
* @return TIME-TRUST as Q48.16; always @c Q48_ONE on AArch64.
*/
time_trust_t heartbeat_trust(void)
{
return g_heartbeat.trust;
}
/**
* @brief Return a pointer to the heartbeat @c TimeTrustState.
*
* Provides read access to the full @c g_heartbeat structure for callers
* that need to inspect the rolling window contents, variance estimate, or
* expected delta — for example, the VM bootstrap parity collector
* (@c sk_parity_collect()) and the @c sk_hal_time_trust() HAL accessor.
*
* The pointer is valid for the lifetime of the kernel (module-static storage).
*
* @return Pointer to @c g_heartbeat; never NULL.
*/
const TimeTrustState *heartbeat_state(void)
{
return &g_heartbeat;
return cntpct_read();
}
+32
View File
@@ -53,6 +53,7 @@
#include "console.h"
#include "vmm.h"
#include "arch.h"
#include "starkernel/timer.h"
/* ============================================================================
* APIC Register Offsets
@@ -102,6 +103,7 @@ static uint64_t lapic_phys_base = LAPIC_DEFAULT_PHYS;
static uint32_t timer_initial_count = 0;
static uint64_t timer_period_tsc_ticks = 0;
static uint32_t timer_tick_hz = 0;
static uint32_t s_apic_hz = 0; /* Calibrated APIC timer frequency (item 0.8/§26) */
/* ============================================================================
* MSR access (freestanding — no libgcc, no libc)
@@ -422,6 +424,7 @@ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz) {
/* Compute initial count for desired tick rate */
timer_initial_count = apic_hz / tick_hz;
timer_tick_hz = tick_hz;
s_apic_hz = apic_hz;
/* Compute expected TSC ticks per heartbeat (for TIME-TRUST variance) */
if (tsc_hz > 0) {
@@ -563,6 +566,35 @@ void apic_timer_stop(void) {
lapic_write(APIC_REG_LVT_TIMER, lvt);
}
/**
* @brief Re-arm the APIC timer at the current adaptive period (item 0.8, §26).
*
* amd64's APIC timer runs in periodic mode: hardware auto-reloads
* @c APIC_REG_TIMER_ICR on every expiry with no software intervention, unlike
* riscv64's one-shot SBI deadline or aarch64's one-shot @c CNTP_TVAL_EL0 --
* neither of which needed a rearm function before this item for the same
* reason this one now exists. To make the period adaptive, this function
* recomputes @c timer_initial_count from @c heartbeat_next_period_ns() (§26)
* and @c s_apic_hz, then writes it to @c APIC_REG_TIMER_ICR. A periodic-mode
* ICR write takes effect immediately and restarts the countdown at the new
* value -- the same mechanism @c apic_timer_start() already relies on to
* force QEMU TCG's emulated APIC to begin counting.
*
* Called from @c isr_common_handler() on every @c APIC_TIMER_VECTOR
* interrupt, before @c heartbeat_tick() -- same ordering discipline as
* riscv64/aarch64's rearm-before-heartbeat_tick(), so a fault in
* @c heartbeat_tick() cannot also cost the next tick.
*/
void apic_timer_rearm(void) {
uint64_t period_ns = heartbeat_next_period_ns();
uint64_t new_count = ((uint64_t)s_apic_hz * period_ns) / 1000000000ULL;
if (new_count == 0) {
new_count = 1;
}
timer_initial_count = (uint32_t)new_count;
lapic_write(APIC_REG_TIMER_ICR, timer_initial_count);
}
/**
* @brief Return the expected TSC-tick count per APIC heartbeat period.
*
+1
View File
@@ -334,6 +334,7 @@ void isr_common_handler(uint64_t vector,
{
/* Handle APIC timer interrupt (heartbeat) */
if (vector == APIC_TIMER_VECTOR) {
apic_timer_rearm();
heartbeat_tick();
/* Acknowledge interrupt and return (don't halt!) */
+10 -216
View File
@@ -1336,225 +1336,19 @@ const timer_calibration_record_t *timer_calibration_record(void)
* 5. Derive TIME-TRUST from variance
*/
static TimeTrustState g_heartbeat;
/**
* @brief Push a signed delta value into the heartbeat rolling window.
* @brief Read the raw counter the amd64 heartbeat is paced against.
*
* Overwrites the oldest entry at @c w->pos (circular), then advances @c pos
* modulo @c TIME_WINDOW_SIZE and increments @c count up to the window
* capacity. Called on every heartbeat tick to record the deviation of the
* actual inter-tick TSC delta from @c expected_delta.
* Item 0.8 (FABRIC.md §25.1): the shared heartbeat.c owns
* heartbeat_init()/heartbeat_tick()/heartbeat_service()/heartbeat_ticks()/
* heartbeat_trust()/heartbeat_state() and the variance/trust math that used
* to live in this file. This is the one piece that stays per-architecture
* -- the same @c rdtsc() the rest of this file's TSC calibration already
* uses, not a separate/different source.
*
* @param w Heartbeat window to update.
* @param delta Signed deviation (actual_delta - expected_delta) in TSC ticks.
* @return Current TSC value.
*/
static void window_push(TimeWindow *w, int64_t delta)
uint64_t heartbeat_read_counter(void)
{
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.
*
* Computes the population variance of the signed deviation samples in @p w,
* then normalises by @c expected_delta² to produce a dimensionless relative
* variance. The result is represented in Q48.16 fixed-point (value 65536 =
* 1.0 = 100% relative variance). Returns 0 if fewer than 2 samples are
* present or @p expected_delta is 0.
*
* Overflow guards: large deviations are clamped to ±0x7FFFFFFF ticks before
* squaring; @c var_tsc is shifted right if it exceeds @c 0x0000FFFFFFFFFFFF.
*
* @param w Rolling window of inter-tick TSC deviations.
* @param expected_delta Nominal inter-tick TSC delta (tsc_hz / tick_hz).
* @return Relative variance as Q48.16, or 0 if insufficient data.
*/
static q48_16_t window_variance_q48(const TimeWindow *w, uint64_t expected_delta)
{
if (w->count < 2 || expected_delta == 0) {
return 0;
}
/* Compute mean */
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;
/* Compute variance = sum((delta - mean)^2) / count */
uint64_t sum_sq = 0;
for (uint32_t i = 0; i < w->count; i++) {
int64_t diff = w->deltas[i] - mean;
/* Clamp to prevent overflow */
if (diff > 0x7FFFFFFF) diff = 0x7FFFFFFF;
if (diff < -0x7FFFFFFF) diff = -0x7FFFFFFF;
sum_sq += (uint64_t)(diff * diff);
}
uint64_t var_tsc = sum_sq / w->count;
/* Normalize by expected_delta^2 to get relative variance */
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;
/* Clamp to avoid overflow when shifting */
if (var_tsc > 0x0000FFFFFFFFFFFFULL) {
var_tsc = 0x0000FFFFFFFFFFFFULL;
}
return (var_tsc << 16) / exp_sq;
}
/**
* @brief Derive TIME-TRUST from a Q48.16 relative variance value.
*
* Applies the formula @c trust = 1 / (1 + variance) in Q48.16 arithmetic:
* @c denom = Q48_ONE + variance, then @c trust = Q48_ONE / denom. Returns
* @c Q48_ONE (maximum trust = 1.0) if @p denom would be zero (defensive).
*
* As variance → 0, trust → 1.0 (perfect timing). As variance → ∞, trust
* → 0.0 (unusable). The Q48.16 output is stored in @c TimeTrustState.trust
* and exported to the VM via @c heartbeat_trust().
*
* @param variance Q48.16 relative variance from @c window_variance_q48().
* @return Q48.16 time-trust value in range (0, 1].
*/
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);
}
/**
* @brief Initialise the M5 heartbeat / TIME-TRUST subsystem.
*
* Zeros the @c g_heartbeat state: tick count, TSC anchor, sample counter,
* variance, and rolling window. Sets the initial trust to @c Q48_ONE (1.0,
* maximum trust) so the VM starts with a confident time signal.
*
* Computes @c expected_delta = @p tsc_hz / @p tick_hz to calibrate the
* deviation window. Falls back to 10 ms at 1 GHz if either argument is zero.
*
* Called from @c kernel_main() after @c timer_init() establishes
* @c tsc_hz_locked.
*
* @param tsc_hz Locked TSC frequency in Hz (from @c timer_tsc_hz()).
* @param tick_hz Target heartbeat rate in Hz (typically 100).
*/
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;
}
if (tick_hz > 0 && tsc_hz > 0) {
g_heartbeat.expected_delta = tsc_hz / tick_hz;
} else {
g_heartbeat.expected_delta = 10000000; /* 10ms at 1GHz */
}
}
/**
* @brief Process one heartbeat tick — update tick count, variance, and TIME-TRUST.
*
* Called by the APIC timer ISR at @c tick_hz (100 Hz by default). On each call:
* 1. Reads the current TSC via @c rdtsc().
* 2. Increments @c ticks and @c total_samples.
* 3. On the first sample, records @c last_tsc and returns (no delta yet).
* 4. Computes the signed deviation of the actual inter-tick TSC delta from
* @c expected_delta and pushes it into the rolling window.
* 5. Recomputes @c variance and @c trust from the updated window.
*
* Must only be called from interrupt context. Does not take a lock; the
* single-threaded kernel guarantees no concurrent access.
*/
void heartbeat_tick(void)
{
uint64_t now = rdtsc();
TimeTrustState *s = &g_heartbeat;
s->ticks++;
s->total_samples++;
/* First tick: just record TSC */
if (s->total_samples == 1) {
s->last_tsc = now;
return;
}
/* Compute delta from last tick */
uint64_t actual_delta = now - s->last_tsc;
s->last_tsc = now;
/* Compute deviation from expected (signed) */
int64_t deviation = (int64_t)actual_delta - (int64_t)s->expected_delta;
/* Add to rolling window */
window_push(&s->window, deviation);
/* Recompute variance and trust */
s->variance = window_variance_q48(&s->window, s->expected_delta);
s->trust = variance_to_trust(s->variance);
}
/**
* @brief Return the total number of heartbeat ticks since @c heartbeat_init().
*
* The tick count monotonically increases with each @c heartbeat_tick() call.
* Used by the HAL shim (@c sk_hal_heartbeat_ticks()) and the VM's @c M5
* time-trust bridge.
*
* @return Number of heartbeat ticks elapsed.
*/
uint64_t heartbeat_ticks(void)
{
return g_heartbeat.ticks;
}
/**
* @brief Return the current TIME-TRUST value as Q48.16 fixed-point.
*
* Returns @c g_heartbeat.trust, updated on every tick by @c variance_to_trust().
* Value is in range (0, 1]: @c Q48_ONE (65536) = perfect trust, values near
* zero indicate high inter-tick jitter.
*
* @return Current time-trust as a @c time_trust_t (Q48.16 alias).
*/
time_trust_t heartbeat_trust(void)
{
return g_heartbeat.trust;
}
/**
* @brief Return a pointer to the full heartbeat / TIME-TRUST state.
*
* Provides read-only access to @c g_heartbeat for diagnostic output or
* parity logging. The caller must not write through the returned pointer.
*
* @return Pointer to the static @c TimeTrustState (never NULL).
*/
const TimeTrustState *heartbeat_state(void)
{
return &g_heartbeat;
return rdtsc();
}
+18 -2
View File
@@ -17,6 +17,7 @@
#include <stdint.h>
static uint64_t s_timer_period_tsc = 0;
static uint64_t s_time_hz = 0; /* `time` CSR frequency (item 0.8/§26) */
/* ---------------------------------------------------------------------------
* SBI (Supervisor Binary Interface)
@@ -110,16 +111,30 @@ static int sbi_set_timer(uint64_t deadline)
* no error anywhere — the single most likely silent failure of this driver.
* Every path out of a timer interrupt must reach this function.
*
* The step is @c heartbeat_next_period_ns() (item 0.8, §26) converted to
* `time`-counter ticks via @c s_time_hz, not the fixed @c s_timer_period_tsc
* used to seed the very first deadline in @c apic_timer_start() -- the
* latter remains @c apic_timer_period_tsc()'s return value for
* @c heartbeat_init()'s initial @c expected_delta, unchanged.
*
* Called from @c riscv64_interrupt_handler() in @c interrupts.c on
* @c scause cause 5.
*/
void riscv64_timer_rearm(void)
{
uint64_t now;
uint64_t step;
if (!s_sbi_time_ok) return;
s_next_deadline += s_timer_period_tsc;
step = (s_time_hz > 0)
? (heartbeat_next_period_ns() * s_time_hz) / 1000000000ULL
: s_timer_period_tsc;
if (step == 0) {
step = 1;
}
s_next_deadline += step;
/* If servicing ran long enough that the next deadline is already behind
* us, resynchronise rather than burn through a backlog of instant
@@ -127,7 +142,7 @@ void riscv64_timer_rearm(void)
now = rdtime();
if (s_next_deadline <= now)
{
s_next_deadline = now + s_timer_period_tsc;
s_next_deadline = now + step;
}
sbi_set_timer(s_next_deadline);
@@ -183,6 +198,7 @@ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz)
s_timer_period_tsc = tsc_hz / tick_hz;
else
s_timer_period_tsc = 10000000;
s_time_hz = tsc_hz;
return 0;
}
+14 -92
View File
@@ -96,7 +96,6 @@ static uint64_t s_base_count = 0;
static uint64_t s_base_ns = 0;
static timer_calibration_record_t s_cal;
static TimeTrustState g_heartbeat;
/*
* @brief Initialise the RISC-V timer subsystem (M5 milestone).
@@ -232,99 +231,22 @@ const timer_calibration_record_t *timer_calibration_record(void)
return &s_cal;
}
/*
* @brief Initialise the heartbeat rolling-window state.
/**
* @brief Read the raw counter the riscv64 heartbeat is paced against.
*
* Zeroes @c g_heartbeat and sets the expected inter-tick interval as
* @c tsc_hz / @c tick_hz `time`-counter ticks. Falls back to 10,000,000
* ticks if either argument is zero. Initial @c trust is @c Q48_ONE
* (full confidence) — the RISC-V @c time counter is architecturally
* invariant regardless of which frequency source populated @c tsc_hz.
* 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(). This is the one piece that stays
* per-architecture -- the same @c rdtime() the timer deadline is armed
* against, not @c rdcycle() or any other source. Must read the same
* counter the deadline was programmed against: @c expected_delta is
* derived from timebase-frequency and is therefore in @c time units;
* measuring the interval with @c cycle instead would difference two
* unrelated clocks.
*
* @param tsc_hz `time` counter frequency (Hz); from @c timer_tsc_hz().
* @param tick_hz Heartbeat rate (Hz); from @c apic_timer_init().
* @return Current `time` CSR value.
*/
void heartbeat_init(uint64_t tsc_hz, uint64_t tick_hz)
uint64_t heartbeat_read_counter(void)
{
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;
return rdtime();
}
/**
* @brief Record one heartbeat tick and update the inter-tick deviation window.
*
* Reads @c rdtime() and, if @c last_tsc is non-zero, records the signed
* deviation @c ((now - last_tsc) - expected_delta) into the circular
* @c window.deltas[] buffer. Increments @c ticks and @c total_samples.
* Sets @c trust = @c Q48_ONE unconditionally — the RISC-V @c time counter
* is invariant and needs no statistical quality estimate.
*
* Must read the same counter the deadline was programmed against.
* @c expected_delta is derived from @c timebase-frequency and is therefore in
* @c time units; measuring the interval with @c cycle instead would difference
* two unrelated clocks and produce exactly the wrong-expected-interval defect
* that switching off @c rdcycle was meant to remove.
*
* Called from @c riscv64_interrupt_handler() on @c scause cause 5, after the
* timer has been re-armed.
*/
void heartbeat_tick(void)
{
uint64_t now = rdtime();
if (g_heartbeat.last_tsc != 0) {
int64_t delta = (int64_t)(now - g_heartbeat.last_tsc)
- (int64_t)g_heartbeat.expected_delta;
uint32_t pos = g_heartbeat.window.pos % TIME_WINDOW_SIZE;
g_heartbeat.window.deltas[pos] = delta;
g_heartbeat.window.pos++;
if (g_heartbeat.window.count < TIME_WINDOW_SIZE)
g_heartbeat.window.count++;
g_heartbeat.total_samples++;
}
g_heartbeat.last_tsc = now;
g_heartbeat.ticks++;
g_heartbeat.trust = Q48_ONE;
}
/**
* @brief Return the total number of heartbeat ticks since @c heartbeat_init().
*
* @return Monotonic tick counter; incremented once per @c heartbeat_tick() call.
*/
uint64_t heartbeat_ticks(void) { return g_heartbeat.ticks; }
/**
* @brief Return the current TIME-TRUST quality metric in Q48.16 format.
*
* Always returns @c Q48_ONE on RISC-V because the @c time counter's *rate
* stability* is invariant by specification — this tracks jitter in the
* tick-to-tick interval, and is orthogonal to whether the frequency value
* itself was discovered or is the fallback (see @c timer_calibration_record()
* for that distinction). The x86-64 implementation derives this from
* rolling-window variance.
*
* @return TIME-TRUST as Q48.16; always @c Q48_ONE on RISC-V.
*/
time_trust_t heartbeat_trust(void) { return g_heartbeat.trust; }
/**
* @brief Return a pointer to the heartbeat @c TimeTrustState.
*
* Provides read access to the full @c g_heartbeat structure for
* @c sk_parity_collect(), @c sk_hal_time_trust(), and other consumers.
* The pointer is valid for the lifetime of the kernel.
*
* @return Pointer to @c g_heartbeat; never NULL.
*/
const TimeTrustState *heartbeat_state(void) { return &g_heartbeat; }