riscv64: finish SBI timer verification and clean up stale timer docs

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>
This commit is contained in:
Robert Allan James
2026-08-03 18:23:37 -04:00
co-authored by Claude Sonnet 5
parent accd79fc70
commit 5784d8a1a8
10 changed files with 31347 additions and 42 deletions
+13 -13
View File
@@ -161,19 +161,19 @@ int apic_init(BootInfo *boot_info)
void apic_eoi(void) { }
/*
* @brief Configure the periodic timer (RISC-V stub).
* @brief Compute the expected `time`-counter ticks per heartbeat period.
*
* On x86-64 this calibrates and programs the APIC timer. On RISC-V the
* SBI (Supervisor Binary Interface) @c sbi_set_timer() call or the CLINT
* (Core Local Interrupt) would be used to schedule timer interrupts; that
* driver is deferred. This stub computes @c s_timer_period_tsc — the
* expected number of @c rdcycle ticks between heartbeats — which seeds
* Unlike x86-64's APIC timer, this does not itself arm anything — RISC-V S-mode
* cannot program the timer directly, so arming happens via SBI in
* @c apic_timer_start() below. This function only computes
* @c s_timer_period_tsc, the expected number of @c time-counter ticks between
* heartbeats, which seeds both @c apic_timer_start()'s first deadline and
* @c heartbeat_init()'s @c expected_delta.
*
* Falls back to 10,000,000 cycles if either frequency is zero (a safe
* Falls back to 10,000,000 ticks if either frequency is zero (a safe
* non-zero sentinel preventing division-by-zero in the heartbeat path).
*
* @param tsc_hz Cycle-counter frequency from @c timer_init() (Hz).
* @param tsc_hz `time` counter frequency from @c timer_init() (Hz).
* @param tick_hz Desired heartbeat rate (Hz); 0 → uses fallback.
* @return 0 always.
*/
@@ -245,14 +245,14 @@ void apic_timer_stop(void)
}
/**
* @brief Return the expected cycle-counter ticks per heartbeat period.
* @brief Return the expected `time`-counter ticks per heartbeat period.
*
* Returns @c s_timer_period_tsc, set by @c apic_timer_init() as
* @c tsc_hz / @c tick_hz. This value is consumed by @c heartbeat_init()
* to seed the rolling-window @c expected_delta so that inter-tick
* deviation measurements are relative to the correct nominal interval.
* @c tsc_hz / @c tick_hz. Consumed by @c heartbeat_init() to seed the
* rolling-window @c expected_delta, and by @c apic_timer_start() /
* @c riscv64_timer_rearm() as the SBI deadline step.
*
* @return Expected @c rdcycle ticks per heartbeat period; 10,000,000 if
* @return Expected `time`-counter ticks per heartbeat period; 10,000,000 if
* frequencies were not available at init time.
*/
uint64_t apic_timer_period_tsc(void)
+37 -26
View File
@@ -161,11 +161,13 @@ int timer_init(BootInfo *boot_info)
/**
* @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.
* 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 Cycle-counter frequency in Hz; 1,000,000,000 (assumed 1 GHz).
* @return `time` counter frequency in Hz.
*/
uint64_t timer_tsc_hz(void)
{
@@ -175,7 +177,7 @@ uint64_t timer_tsc_hz(void)
/**
* @brief Return a monotonic nanosecond timestamp.
*
* Reads @c rdcycle(), computes the elapsed delta from @c s_base_count
* 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:
*
@@ -183,11 +185,14 @@ uint64_t timer_tsc_hz(void)
* 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.
* 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.
*/
@@ -198,12 +203,13 @@ uint64_t timer_now_ns(void)
}
/**
* @brief Check for cycle-counter drift (RISC-V stub — always returns 0).
* @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 rdcycle counter has no independent reference to check against at
* this milestone. Always returns 0 (no drift) to satisfy the common
* call site.
* 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.
*/
@@ -212,9 +218,11 @@ 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.
* 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.
@@ -228,11 +236,12 @@ const timer_calibration_record_t *timer_calibration_record(void)
* @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.
* @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 Cycle-counter frequency (Hz); from @c timer_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)
@@ -298,10 +307,12 @@ 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.
* 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.
*/