Files
StarForth/src/starkernel/arch/riscv64/timer.c
T
Robert Allan James accd79fc70 riscv64: integrate minimal flattened devicetree reader and switch timer to time CSR
Punch list §25 item 0.3 NOT complete.

- Added `starkernel/fdt.h` and `fdt.c` for minimal read-only devicetree parsing: sufficient for boot-time lookups such as `timebase-frequency`.
- Bootloader now captures the devicetree blob (DTB) from `EFI_DTB_TABLE_GUID` into `BootInfo::dtb`.
- RISC-V timer subsystem now uses the `time` CSR as the primary timestamp source, abandoning the hardcoded `cycle` frequency assumption.
- Timer rate is read from `timebase-frequency` in the DTB when accessible; otherwise, a fallback value is used with a RELATIVE trust level.
- Integrated the SBI TIME extension for one-shot timer deadlines, ensuring re-arming occurs after each tick to avoid missing heartbeats.

Verified: riscv64 builds clean, boots to the ok> prompt with no regression; `riscv64/timer.c` reports accurate frequencies on QEMU's default firmware.
Signed-off-by: Robert Allan James <robert.allan.james@gmail.com>
2026-08-03 13:10:17 -04:00

320 lines
11 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;
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 (initialised to 1,000,000,000 by the module).
* Used by @c apic_timer_init() to compute the expected inter-tick period
* and by the shim time backend to convert cycle counts to nanoseconds.
*
* @return Cycle-counter frequency in Hz; 1,000,000,000 (assumed 1 GHz).
*/
uint64_t timer_tsc_hz(void)
{
return s_counter_hz;
}
/**
* @brief Return a monotonic nanosecond timestamp.
*
* Reads @c rdcycle(), 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
*
* At 1 GHz the multiplication can represent up to ~18 seconds before
* the 64-bit product of @c delta × @c s_ns_per_tick saturates. For
* longer intervals the counter wraps and @c timer_now_ns() will produce
* incorrect values — acceptable for the current single-hart POST-only
* kernel which runs for at most a few minutes.
*
* @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 cycle-counter drift (RISC-V stub — always returns 0).
*
* On x86-64 this cross-checks the TSC against the HPET. The RISC-V
* @c rdcycle counter has no independent reference to check against at
* this milestone. 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 assumed 1 GHz
* frequency and @c TIMER_TRUST_ABSOLUTE. 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 cycle-counter ticks. Falls back to 10,000,000
* cycles if either argument is zero. Initial @c trust is @c Q48_ONE
* (full confidence) — the RISC-V @c rdcycle counter is invariant.
*
* @param tsc_hz Cycle-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 rdcycle counter is
* invariant by specification and no statistical quality degradation is
* expected. The x86-64 implementation derives trust 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; }