/* 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-0.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 #include /* FABRIC-2.md §I.5, 2026-09-04: real hypervisor-vs-hardware detection. * s_cal.vm_mode was hardcoded to 1 unconditionally below -- see * aarch64/timer.c's own running_under_hypervisor() doc comment for why * that's wrong to reuse as a general "are we in QEMU" signal elsewhere * (contrib-capsule trust-tier enforcement, §I.5). RISC-V has no ACPI * here (this file's own devicetree-only timebase-frequency discovery * above is the proof) but does have a devicetree, already parsed for * exactly one other property -- the root node's "compatible" property * carries QEMU's own machine-model string ("qemu" appears in it for the * virt board) on every QEMU riscv64 target; real hardware vendors set * their own compatible strings, never this one. bytes_contain() is a * tiny local substring search -- no strstr dependency assumed available * in this translation unit. */ static int bytes_contain(const uint8_t *hay, uint32_t haylen, const char *needle) { size_t nlen = strlen(needle); if (nlen == 0 || haylen < nlen) return 0; for (uint32_t i = 0; i + nlen <= haylen; i++) { if (memcmp(hay + i, needle, nlen) == 0) return 1; } return 0; } static int running_under_hypervisor(BootInfo *boot_info) { if (!boot_info || !fdt_valid(boot_info->dtb)) return 0; uint32_t len = 0; const void *prop = fdt_find_prop(boot_info->dtb, "compatible", &len); if (!prop) return 0; return bytes_contain((const uint8_t *) prop, len, "qemu"); } /** * @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 = running_under_hypervisor(boot_info) ? 1 : 0; /* 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-0.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(); }