Punch list §25 item 0.3 complete.
The functional work (FDT reader, `time` CSR switch, SBI TIME extension
arm/re-arm, sie.STIE) was committed separately by Captain Bob as accd79f,
honestly labeled "NOT complete." This finishes it: stale documentation
cleanup, then rigorous verification against the item's literal acceptance
text, which the prior commit had not yet done.
Doc cleanup: every remaining reference to `rdcycle` / "assumed 1 GHz" in
riscv64/timer.c and riscv64/apic.c rewritten to describe the actual `time`
CSR / SBI-armed behavior. Caught and corrected my own arithmetic error in
the process: a draft claimed the ns-overflow bound improved to "~58 years"
under the new counter; direct computation shows the bound is actually
~3.26 days (2^48 ns) and is *frequency-independent* -- s_ns_per_tick and
tick rate scale inversely and cancel. Verified with a script before
writing the final comment, not asserted.
Verification, since "boots to prompt" was not sufficient for this item's
acceptance ("heartbeat_ticks() advances ... within measurement noise"):
- No FORTH word exposes heartbeat_ticks() to the REPL, and adding one would
be a new primitive outside this item's scope. GDB-over-QEMU-stub reads of
the static counter failed ("Cannot access memory"), likely a virtual-vs-
runtime address mismatch; abandoned rather than debugged further, since a
better instrument was available.
- QEMU's own `-d int` trap tracing (independent of all guest code) shows
2,797 `cause:5 desc=s_timer` interrupts delivered by the time boot reaches
the prompt, and the interleaved `desc=supervisor_ecall` entries are exactly
the SBI set_timer calls from riscv64_timer_rearm(), confirming the full
interrupt -> handler -> re-arm -> SBI loop.
- Measured rate over a real 10.127 s window: 1,013 further interrupts,
100.028 Hz observed against 100 Hz configured -- 0.03% deviation. The
sustained, non-decaying rate is itself proof the one-shot re-arm succeeds
on every cycle, not just the first.
- Confirmed regression-free on amd64 and aarch64 too: uefi_loader.c, uefi.h,
boot_info_offsets.h and the new fdt.c/fdt.h are shared across all three
builds (amd64 also consumes the offset constants via kernel_entry.S), so
all three were rebuilt and booted. Identical dict_hash
0x3d4e1daf289da94f on all three, matching the item 0.1/0.2 baseline.
Reported, not fixed, per rule 3:
- DOE_INJECT=1's EXEC-DOE now fails as "UNKNOWN WORD" against the pruned
Hera-alone capsule -- doe.4th was never loaded by init.4th even before
item 0.1's prune, so this is a pre-existing gap the prune surfaced, not a
new defect. Discovered while chasing why a 200-rep injected DoE run
produced no new CSV rows after 25 minutes; it had failed in the first
millisecond, not run the whole time.
- repl.c's own comment claims a "Heartbeat: N ticks" diagnostic prints to
the serial log; no such print exists anywhere in the tree.
- riscv64/arch.c's arch_read_timestamp() still uses rdcycle with a stale
1 GHz comment, but it has zero callers on this architecture -- dead code,
left alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
331 lines
12 KiB
C
331 lines
12 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 (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;
|
||
static TimeTrustState g_heartbeat;
|
||
|
||
/*
|
||
* @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 Initialise the heartbeat rolling-window state.
|
||
*
|
||
* 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.
|
||
*
|
||
* @param tsc_hz `time` counter frequency (Hz); from @c timer_tsc_hz().
|
||
* @param tick_hz Heartbeat 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.
|
||
*
|
||
* 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; }
|