Files
LithosAnanke/src/starkernel/arch/riscv64/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

253 lines
9.0 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) 2023–2025 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* timer.c (riscv64) - Timer using the `time` CSR.
*
* RISC-V provides `cycle` (per-hart clock cycles) and `time` (fixed-frequency
* wall clock). The primary timestamp source is `time`, read via rdtime, and
* its rate comes from the devicetree property timebase-frequency.
*
* This was `cycle` at an assumed 1 GHz until punch-list item 0.3. Two things
* forced the change: the SBI TIME extension arms deadlines against `time`, so
* mixing the two counters would compare unrelated clocks; and `cycle` has no
* discoverable frequency, so every heartbeat variance and TIME-TRUST figure
* riscv64 produced before this was measured against a wrong expected interval
* (FABRIC.md §16.2). Figures from before and after are not comparable.
*/
#include "timer.h"
#include "console.h"
#include "q48_16.h"
#include "uefi.h"
#include "starkernel/fdt.h"
#include <stdint.h>
/**
* @brief Read the RISC-V wall-clock counter (@c rdtime, CSR @c time 0xC01).
*
* @c time is the memory-mapped real-time counter mandated by the privileged
* spec: fixed frequency, common to all harts, and — unlike @c cycle — its
* rate is discoverable, published by firmware as the devicetree property
* @c timebase-frequency.
*
* This replaces the earlier @c rdcycle() source. Two reasons, and the first
* is not optional:
*
* - **The SBI TIME extension is defined against @c time.** @c sbi_set_timer()
* takes an absolute value on this counter, so arming a timer from a
* @c cycle reading would compare two unrelated clocks.
* - **@c cycle's frequency is not discoverable**, which is why the previous
* implementation hardcoded an assumed 1 GHz. Every heartbeat variance and
* TIME-TRUST figure riscv64 has produced was therefore computed against a
* wrong expected interval.
*
* @return Current 64-bit @c time value.
*/
static inline uint64_t rdtime(void)
{
uint64_t val;
__asm__ volatile (
"rdtime %0" : "=r"(val));
return val;
}
/* Fallback when firmware publishes no devicetree, or none carrying
* timebase-frequency. 10 MHz is the QEMU virt machine's value. Named rather
* than inlined so that a boot running on the fallback is greppable and
* obviously distinct from a discovered rate. */
#define RISCV_TIMEBASE_HZ_FALLBACK 10000000ULL
/**
* @brief Print @p val in decimal via @c console_putc().
*
* Same shape as the file-local helper in @c arch/amd64/timer.c — there is no
* shared decimal printer in @c console.h, and the freestanding build has no
* @c printf(). Prints "0" for zero.
*
* @param val Value to print.
*/
static void print_dec(uint64_t val)
{
char buf[32];
int i = 0;
if (val == 0)
{
console_putc('0');
return;
}
while (val > 0 && i < (int)sizeof(buf))
{
buf[i++] = (char)('0' + (val % 10));
val /= 10;
}
while (i-- > 0)
{
console_putc(buf[i]);
}
}
static uint64_t s_counter_hz = RISCV_TIMEBASE_HZ_FALLBACK;
static uint64_t s_ns_per_tick = 0;
static uint64_t s_base_count = 0;
static uint64_t s_base_ns = 0;
static timer_calibration_record_t s_cal;
/*
* @brief Initialise the RISC-V timer subsystem (M5 milestone).
*
* RISC-V has no CSR reporting the counter frequency (unlike AArch64's
* @c CNTFRQ_EL0), so the rate is read from the devicetree property
* @c timebase-frequency, located via the blob the firmware publishes under
* @c EFI_DTB_TABLE_GUID and carried in @c BootInfo::dtb.
*
* Steps performed:
* 1. Reads @c timebase-frequency from @c boot_info->dtb when present;
* otherwise keeps @c RISCV_TIMEBASE_HZ_FALLBACK and says so on the console.
* 2. Snapshots @c rdtime() into @c s_base_count as the ns origin.
* 3. Computes @c s_ns_per_tick as @c (1e9 << 16) / @c s_counter_hz in
* Q16.16 fixed-point to avoid floating-point in the freestanding build.
* 4. Fills @c s_cal, setting @c TIMER_TRUST_ABSOLUTE only when the rate came
* from firmware. On the fallback the counter is still invariant, but its
* scaling to real time is a guess — which is @c TIMER_TRUST_RELATIVE.
*
* @param boot_info Kernel @c BootInfo; @c ::dtb supplies the counter rate.
* NULL, or a NULL/invalid blob, selects the fallback.
* @return 0 always.
*/
int timer_init(BootInfo *boot_info)
{
uint32_t hz = 0;
int discovered = 0;
/* Discover the counter rate rather than assuming it. The devicetree is
* the only source: RISC-V has no CNTFRQ_EL0 equivalent. boot_info->dtb is
* NULL when firmware published no devicetree, in which case the named
* fallback stands and the banner says so. */
if (boot_info && fdt_valid(boot_info->dtb))
{
if (fdt_prop_u32(boot_info->dtb, "timebase-frequency", &hz) && hz != 0)
{
s_counter_hz = (uint64_t)hz;
discovered = 1;
}
}
s_base_count = rdtime();
s_base_ns = 0;
s_ns_per_tick = (1000000000ULL << 16) / s_counter_hz;
s_cal.tsc_hz_mean = s_counter_hz;
s_cal.hpet_hz = 0;
s_cal.pit_hz_mean = 0;
s_cal.converged = 1;
s_cal.vm_mode = 1;
/* ABSOLUTE only when the rate came from firmware. On the fallback the
* counter is still monotonic and invariant, but its scaling to real time
* is a guess, which is exactly the RELATIVE case. */
s_cal.trust = discovered ? TIMER_TRUST_ABSOLUTE : TIMER_TRUST_RELATIVE;
console_puts("Timer: RISC-V time CSR @ ");
print_dec(s_counter_hz);
console_println(discovered ? " Hz (devicetree)" : " Hz (FALLBACK, no devicetree)");
return 0;
}
/**
* @brief Return the `time` counter frequency in Hz.
*
* Returns @c s_counter_hz — discovered from the devicetree's
* @c timebase-frequency when available, otherwise
* @c RISCV_TIMEBASE_HZ_FALLBACK. Used by @c apic_timer_init() to compute the
* expected inter-tick period and by the shim time backend to convert `time`
* counts to nanoseconds.
*
* @return `time` counter frequency in Hz.
*/
uint64_t timer_tsc_hz(void)
{
return s_counter_hz;
}
/**
* @brief Return a monotonic nanosecond timestamp.
*
* Reads @c rdtime(), computes the elapsed 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 64-bit product of @c delta × @c s_ns_per_tick wraps once elapsed real
* time reaches 2^48 ns (~3.26 days) — this bound is **independent of
* @c s_counter_hz**, since @c s_ns_per_tick scales inversely with frequency
* and the two effects cancel. Verified by direct computation, not carried
* over from the earlier @c cycle-based comment here, which claimed "~18
* seconds" and did not match the arithmetic either. Beyond ~3.26 days
* uptime @c timer_now_ns() wraps and produces incorrect values — a
* theoretical concern only for this single-hart POST-only kernel.
*
* @return Monotonic nanosecond count since @c timer_init(), starting at 0.
*/
uint64_t timer_now_ns(void)
{
uint64_t delta = rdtime() - s_base_count;
return s_base_ns + ((delta * s_ns_per_tick) >> 16);
}
/**
* @brief Check for `time` counter drift (RISC-V stub — always returns 0).
*
* On x86-64 this cross-checks the TSC against the HPET. The RISC-V @c time
* counter has no independent reference to check against at this milestone —
* and per the privileged spec it is architecturally required to be a fixed,
* synchronised rate across harts, unlike @c cycle. Always returns 0 (no
* drift) to satisfy the common call site.
*
* @return 0 always.
*/
int timer_check_drift_now(void) { return 0; }
/**
* @brief Return a pointer to the timer calibration record.
*
* Returns @c &s_cal, populated by @c timer_init() with the discovered (or
* fallback) frequency and the corresponding trust level — @c
* TIMER_TRUST_ABSOLUTE when @c timebase-frequency came from the devicetree,
* @c TIMER_TRUST_RELATIVE on the fallback. Consumed by @c kernel_main() for
* boot-log reporting and by the heartbeat subsystem's trust init.
*
* @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;
}
/**
* @brief Read the raw counter the riscv64 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(). 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.
*
* @return Current `time` CSR value.
*/
uint64_t heartbeat_read_counter(void)
{
return rdtime();
}