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>
This commit is contained in:
Robert Allan James
2026-08-03 13:10:17 -04:00
parent f3821ed686
commit accd79fc70
9 changed files with 561 additions and 67 deletions
+116 -45
View File
@@ -5,45 +5,92 @@
*/
/**
* timer.c (riscv64) - Timer using rdcycle counter.
* timer.c (riscv64) - Timer using the `time` CSR.
*
* RISC-V provides rdcycle (CPU cycle counter) and rdtime (wall-clock timer).
* We use rdcycle as the primary timestamp source; frequency is estimated
* at 1 GHz (QEMU virt default) and updated if firmware provides a hint.
* 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 CPU cycle counter (@c rdcycle).
* @brief Read the RISC-V wall-clock counter (@c rdtime, CSR @c time 0xC01).
*
* Issues the @c RDCYCLE pseudo-instruction (a @c CSRRS on @c cycle,
* CSR 0xC00) to read the 64-bit hardware cycle counter. On RISC-V the
* cycle counter is a per-hart monotonically incrementing register whose
* frequency equals the hart's clock rate — the architectural equivalent
* of the x86-64 TSC.
* @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.
*
* Unlike @c CNTPCT_EL0 on AArch64, @c cycle is not architecturally
* synchronised across harts; this is acceptable for the single-hart
* LithosAnanke build. The frequency is not provided by hardware CSR
* (unlike AArch64's @c CNTFRQ_EL0); @c timer_init() assumes 1 GHz
* for QEMU @c virt-machine compatibility.
* This replaces the earlier @c rdcycle() source. Two reasons, and the first
* is not optional:
*
* @return Current 64-bit cycle count; wraps at UINT64_MAX (about 584 years
* at 1 GHz — not a practical concern).
* - **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 rdcycle(void)
static inline uint64_t rdtime(void)
{
uint64_t val;
__asm__ volatile ("rdcycle %0" : "=r"(val));
__asm__ volatile (
"rdtime %0" : "=r"(val));
return val;
}
static uint64_t s_counter_hz = 1000000000ULL; /* assume 1 GHz */
/* 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;
@@ -54,31 +101,44 @@ static TimeTrustState g_heartbeat;
/*
* @brief Initialise the RISC-V timer subsystem (M5 milestone).
*
* On RISC-V there is no architectural CSR that directly reports the
* @c rdcycle frequency (unlike AArch64's @c CNTFRQ_EL0). The timer
* subsystem therefore assumes 1,000,000,000 Hz (1 GHz), which matches
* QEMU's @c virt machine default clock. Real hardware board support would
* need to read the frequency from a device tree or firmware table and
* update @c s_counter_hz before computing @c s_ns_per_tick.
* 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. Snapshots @c rdcycle() into @c s_base_count as the ns origin.
* 2. Computes @c s_ns_per_tick as @c (1e9 << 16) / @c s_counter_hz in
* 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.
* 3. Fills @c s_cal with the assumed frequency and sets
* @c TIMER_TRUST_ABSOLUTE (the @c rdcycle counter is invariant by
* specification once enabled, though its frequency is merely assumed
* rather than measured).
* 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 (device-tree / ACPI pointer); unused
* at this milestone — clock frequency is hard-coded.
* @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)
{
(void)boot_info;
uint32_t hz = 0;
int discovered = 0;
s_base_count = rdcycle();
/* 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;
@@ -87,14 +147,19 @@ int timer_init(BootInfo *boot_info)
s_cal.pit_hz_mean = 0;
s_cal.converged = 1;
s_cal.vm_mode = 1;
s_cal.trust = TIMER_TRUST_ABSOLUTE;
/* 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_println("Timer: RISC-V rdcycle timer initialised.");
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 assumed cycle-counter frequency in Hz.
* @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
@@ -128,7 +193,7 @@ uint64_t timer_tsc_hz(void)
*/
uint64_t timer_now_ns(void)
{
uint64_t delta = rdcycle() - s_base_count;
uint64_t delta = rdtime() - s_base_count;
return s_base_ns + ((delta * s_ns_per_tick) >> 16);
}
@@ -190,18 +255,24 @@ void heartbeat_init(uint64_t tsc_hz, uint64_t tick_hz)
/**
* @brief Record one heartbeat tick and update the inter-tick deviation window.
*
* Reads @c rdcycle() and, if @c last_tsc is non-zero, records the signed
* 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 cycle counter
* Sets @c trust = @c Q48_ONE unconditionally — the RISC-V @c time counter
* is invariant and needs no statistical quality estimate.
*
* Called from the RISC-V timer ISR stub (or its no-op placeholder) at
* each periodic heartbeat period.
* 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 = rdcycle();
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;