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:
@@ -1,5 +1,6 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-08-03T16:51:19Z -->
|
||||
|
||||
<!-- Generated by mkcapsule --manifest 2026-08-03T17:01:21Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
@@ -15,19 +15,25 @@
|
||||
* 16: memory_map_descriptor_size u64 (8)
|
||||
* 24: runtime_services ptr (8)
|
||||
* 32: acpi_table ptr (8)
|
||||
* 40: framebuffer FramebufferInfo (32)
|
||||
* 40: dtb ptr (8)
|
||||
* 48: framebuffer FramebufferInfo (32)
|
||||
* .base ptr (8)
|
||||
* .size u64 (8)
|
||||
* .width u32 (4)
|
||||
* .height u32 (4)
|
||||
* .pixels_per_scanline u32 (4)
|
||||
* .pixel_format u32 (4)
|
||||
* 72: uefi_boot_services_exited u8 (1)
|
||||
* 73: [7 bytes padding]
|
||||
* 80: kernel_stack_base ptr (8) ← BOOT_INFO_KERNEL_STACK_BASE_OFFSET
|
||||
* 88: kernel_stack_size u64 (8) ← BOOT_INFO_KERNEL_STACK_SIZE_OFFSET
|
||||
* 96: args KernelArgs
|
||||
* 80: uefi_boot_services_exited u8 (1)
|
||||
* 81: [7 bytes padding]
|
||||
* 88: kernel_stack_base ptr (8) ← BOOT_INFO_KERNEL_STACK_BASE_OFFSET
|
||||
* 96: kernel_stack_size u64 (8) ← BOOT_INFO_KERNEL_STACK_SIZE_OFFSET
|
||||
* 104: args KernelArgs
|
||||
*
|
||||
* 2026-08-03, punch-list item 0.3: `dtb` inserted at 40, shifting everything
|
||||
* below it by 8. The _Static_asserts in uefi_loader.c caught the stale
|
||||
* constants immediately — that is what they are for; do not silence them by
|
||||
* moving a field, fix the offsets.
|
||||
*/
|
||||
|
||||
#define BOOT_INFO_KERNEL_STACK_BASE_OFFSET 80
|
||||
#define BOOT_INFO_KERNEL_STACK_SIZE_OFFSET 88
|
||||
#define BOOT_INFO_KERNEL_STACK_BASE_OFFSET 88
|
||||
#define BOOT_INFO_KERNEL_STACK_SIZE_OFFSET 96
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* fdt.h - Minimal flattened-devicetree reader
|
||||
*
|
||||
* Just enough of the Devicetree Specification v0.4 §5 to pull values out of
|
||||
* the blob the UEFI firmware publishes under EFI_DTB_TABLE_GUID. Read-only,
|
||||
* no allocation, no tree construction — it walks the structure block each
|
||||
* call, which is fine for the handful of boot-time lookups the kernel needs.
|
||||
*
|
||||
* Deliberately not a general devicetree library. Added for punch-list item
|
||||
* 0.3 (riscv64 timebase-frequency); item 0.6 will need node-scoped `reg`
|
||||
* lookups for the aarch64 GIC and may extend this.
|
||||
*/
|
||||
|
||||
#ifndef STARKERNEL_FDT_H
|
||||
#define STARKERNEL_FDT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief Test whether @p fdt points at a valid flattened devicetree.
|
||||
*
|
||||
* Checks the 0xd00dfeed magic and that the structure and strings blocks lie
|
||||
* inside totalsize. Does not validate the token stream.
|
||||
*
|
||||
* @param fdt Candidate blob; NULL is safe and returns 0.
|
||||
* @return 1 if the header is usable, 0 otherwise.
|
||||
*/
|
||||
int fdt_valid(const void* fdt);
|
||||
|
||||
/**
|
||||
* @brief Find the first property with @p name anywhere in the tree.
|
||||
*
|
||||
* Scans the structure block in document order and returns the first match
|
||||
* regardless of which node it belongs to. That is sufficient for properties
|
||||
* which are uniform across a machine (timebase-frequency being the case this
|
||||
* was written for) and is *not* sufficient for anything node-scoped.
|
||||
*
|
||||
* @param fdt Blob, already checked with @c fdt_valid().
|
||||
* @param name Property name, NUL-terminated.
|
||||
* @param len_out Receives the property length in bytes; may be NULL.
|
||||
* @return Pointer to the property value inside @p fdt, or NULL if not found.
|
||||
* The value is big-endian as stored in the blob.
|
||||
*/
|
||||
const void* fdt_find_prop(const void* fdt, const char* name, uint32_t* len_out);
|
||||
|
||||
/**
|
||||
* @brief Read a single-cell (32-bit) property by name.
|
||||
*
|
||||
* Convenience over @c fdt_find_prop() that also handles the big-endian
|
||||
* conversion. Fails if the property is absent or not exactly 4 bytes.
|
||||
*
|
||||
* @param fdt Blob, already checked with @c fdt_valid().
|
||||
* @param name Property name, NUL-terminated.
|
||||
* @param out Receives the host-order value on success; untouched on failure.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
int fdt_prop_u32(const void* fdt, const char* name, uint32_t* out);
|
||||
|
||||
#endif /* STARKERNEL_FDT_H */
|
||||
@@ -383,6 +383,14 @@ typedef struct {
|
||||
static const EFI_GUID EFI_ACPI_20_TABLE_GUID = {0x8868e871,0xe4f1,0x11d3,{0xbc,0x22,0x00,0x80,0xc7,0x3c,0x88,0x81}};
|
||||
static const EFI_GUID EFI_ACPI_TABLE_GUID = {0xeb9d2d30,0x2d88,0x11d3,{0x9a,0x16,0x00,0x90,0x27,0x3f,0xc1,0x4d}};
|
||||
|
||||
/* Devicetree Blob GUID (UEFI 2.10 §4.6, "Devicetree Tables").
|
||||
* On QEMU virt for riscv64 and aarch64 the firmware publishes the FDT here;
|
||||
* it is the only route to timebase-frequency (riscv64, item 0.3) and to the
|
||||
* GIC base addresses and timer PPI (aarch64, item 0.6). */
|
||||
static const EFI_GUID EFI_DTB_TABLE_GUID = {
|
||||
0xb1b621d5, 0xf19c, 0x41a5, {0x83, 0x0b, 0xd9, 0x15, 0x2c, 0x69, 0xaa, 0xe0}
|
||||
};
|
||||
|
||||
/* Graphics Output Protocol */
|
||||
typedef enum {
|
||||
PixelRedGreenBlueReserved8BitPerColor,
|
||||
@@ -627,6 +635,11 @@ typedef struct {
|
||||
UINTN memory_map_descriptor_size;
|
||||
EFI_RUNTIME_SERVICES *runtime_services;
|
||||
void *acpi_table;
|
||||
/* Devicetree blob, located by EFI_DTB_TABLE_GUID in the configuration
|
||||
* table; NULL when the firmware publishes none (amd64 typically, and any
|
||||
* platform that is ACPI-only). Consumers must handle NULL rather than
|
||||
* assume presence. */
|
||||
void* dtb;
|
||||
FramebufferInfo framebuffer;
|
||||
UINT8 uefi_boot_services_exited;
|
||||
|
||||
|
||||
@@ -12,10 +12,127 @@
|
||||
|
||||
#include "apic.h"
|
||||
#include "uefi.h"
|
||||
#include "console.h"
|
||||
#include "timer.h"
|
||||
#include <stdint.h>
|
||||
|
||||
static uint64_t s_timer_period_tsc = 0;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* SBI (Supervisor Binary Interface)
|
||||
*
|
||||
* RISC-V S-mode cannot program the timer directly: the CLINT's mtimecmp is an
|
||||
* M-mode register. The timer is armed by asking the SEE (OpenSBI, running in
|
||||
* M-mode beneath EDK2) via ECALL.
|
||||
*
|
||||
* Calling convention, SBI v0.2+ (SBI spec §3): a7 = EID, a6 = FID,
|
||||
* a0.. = arguments; returns a0 = error, a1 = value.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
#define SBI_EXT_BASE 0x10UL
|
||||
#define SBI_BASE_FID_PROBE_EXT 3UL
|
||||
|
||||
#define SBI_EXT_TIME 0x54494D45UL /* "TIME" */
|
||||
#define SBI_TIME_FID_SET_TIMER 0UL
|
||||
|
||||
#define SBI_SUCCESS 0L
|
||||
|
||||
/* sie.STIE — supervisor timer interrupt enable (Privileged Spec §4.1.3) */
|
||||
#define SIE_STIE (1UL << 5)
|
||||
|
||||
typedef struct
|
||||
{
|
||||
long error;
|
||||
long value;
|
||||
} sbiret_t;
|
||||
|
||||
/** @brief Issue an SBI ECALL with one argument. */
|
||||
static sbiret_t sbi_call1(unsigned long eid, unsigned long fid,
|
||||
unsigned long arg0)
|
||||
{
|
||||
register unsigned long r_a0 __asm__("a0") = arg0;
|
||||
register unsigned long r_a1 __asm__("a1") = 0;
|
||||
register unsigned long r_a6 __asm__("a6") = fid;
|
||||
register unsigned long r_a7 __asm__("a7") = eid;
|
||||
sbiret_t ret;
|
||||
|
||||
__asm__ volatile (
|
||||
"ecall"
|
||||
: "+r"(r_a0), "+r"(r_a1)
|
||||
: "r"(r_a6), "r"(r_a7)
|
||||
: "memory");
|
||||
|
||||
ret.error = (long)r_a0;
|
||||
ret.value = (long)r_a1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the RISC-V @c time CSR.
|
||||
*
|
||||
* Deadlines handed to @c sbi_set_timer() are absolute values on this counter.
|
||||
* @c arch/riscv64/timer.c keeps its own copy of this accessor; duplicating
|
||||
* four instructions is preferable to widening @c timer.h with an
|
||||
* architecture-specific accessor that only these two files can use.
|
||||
*/
|
||||
static inline uint64_t rdtime(void)
|
||||
{
|
||||
uint64_t val;
|
||||
__asm__ volatile (
|
||||
"rdtime %0" : "=r"(val));
|
||||
return val;
|
||||
}
|
||||
|
||||
/* Set once in apic_timer_start(): 1 when the TIME extension probed present,
|
||||
* 0 when the timer could not be armed at all. */
|
||||
static int s_sbi_time_ok = 0;
|
||||
/* Absolute `time` value of the next expected interrupt. Advanced by period
|
||||
* rather than recomputed from "now" so that a late tick does not push the
|
||||
* whole schedule out; see riscv64_timer_rearm(). */
|
||||
static uint64_t s_next_deadline = 0;
|
||||
|
||||
/**
|
||||
* @brief Arm the SBI timer for @p deadline.
|
||||
* @return 1 on success, 0 if the SEE rejected the call.
|
||||
*/
|
||||
static int sbi_set_timer(uint64_t deadline)
|
||||
{
|
||||
sbiret_t r = sbi_call1(SBI_EXT_TIME, SBI_TIME_FID_SET_TIMER,
|
||||
(unsigned long)deadline);
|
||||
return r.error == SBI_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Re-arm the one-shot SBI timer and account the tick.
|
||||
*
|
||||
* **The SBI timer is one-shot by nature.** Servicing a timer interrupt without
|
||||
* programming the next deadline leaves the heartbeat stopped permanently, with
|
||||
* no error anywhere — the single most likely silent failure of this driver.
|
||||
* Every path out of a timer interrupt must reach this function.
|
||||
*
|
||||
* Called from @c riscv64_interrupt_handler() in @c interrupts.c on
|
||||
* @c scause cause 5.
|
||||
*/
|
||||
void riscv64_timer_rearm(void)
|
||||
{
|
||||
uint64_t now;
|
||||
|
||||
if (!s_sbi_time_ok) return;
|
||||
|
||||
s_next_deadline += s_timer_period_tsc;
|
||||
|
||||
/* If servicing ran long enough that the next deadline is already behind
|
||||
* us, resynchronise rather than burn through a backlog of instant
|
||||
* interrupts. */
|
||||
now = rdtime();
|
||||
if (s_next_deadline <= now)
|
||||
{
|
||||
s_next_deadline = now + s_timer_period_tsc;
|
||||
}
|
||||
|
||||
sbi_set_timer(s_next_deadline);
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief Initialise the interrupt controller (RISC-V PLIC stub).
|
||||
*
|
||||
@@ -70,20 +187,62 @@ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start periodic timer delivery (RISC-V stub).
|
||||
* @brief Start timer delivery via the SBI TIME extension.
|
||||
*
|
||||
* On x86-64 this unmasks the APIC timer. On RISC-V a periodic timer would
|
||||
* be armed via CLINT or SBI here; the driver is deferred. No-op stub.
|
||||
* Probes for the TIME extension first. If the SEE does not provide it the
|
||||
* timer is **not** armed and the condition is reported loudly rather than
|
||||
* papered over with the legacy EID 0x00 call: a heartbeat that silently never
|
||||
* ticks is far worse to diagnose than one that says why at boot.
|
||||
*
|
||||
* On success: computes the first absolute deadline, arms it, and sets
|
||||
* @c sie.STIE. Global delivery is gated separately by @c sstatus.SIE, which
|
||||
* @c arch_enable_interrupts() sets.
|
||||
*/
|
||||
void apic_timer_start(void) { }
|
||||
void apic_timer_start(void)
|
||||
{
|
||||
sbiret_t probe;
|
||||
|
||||
probe = sbi_call1(SBI_EXT_BASE, SBI_BASE_FID_PROBE_EXT, SBI_EXT_TIME);
|
||||
if (probe.error != SBI_SUCCESS || probe.value == 0)
|
||||
{
|
||||
console_println("SBI: TIME extension ABSENT - timer NOT armed, "
|
||||
"heartbeat will not tick");
|
||||
s_sbi_time_ok = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
s_sbi_time_ok = 1;
|
||||
s_next_deadline = rdtime() + s_timer_period_tsc;
|
||||
|
||||
if (!sbi_set_timer(s_next_deadline))
|
||||
{
|
||||
console_println("SBI: set_timer REJECTED - timer NOT armed");
|
||||
s_sbi_time_ok = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
__asm__ volatile (
|
||||
"csrs sie, %0"
|
||||
::
|
||||
"r"(SIE_STIE) : "memory");
|
||||
console_println("SBI: timer armed (TIME extension)");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop periodic timer delivery (RISC-V stub).
|
||||
* @brief Stop timer delivery by masking @c sie.STIE.
|
||||
*
|
||||
* On x86-64 this masks the APIC timer. On RISC-V a periodic timer would
|
||||
* be disarmed via CLINT or SBI here; the driver is deferred. No-op stub.
|
||||
* The SBI timer cannot be cancelled outright — masking the enable bit is the
|
||||
* supported way to stop delivery. Any deadline already programmed simply goes
|
||||
* unserviced.
|
||||
*/
|
||||
void apic_timer_stop(void) { }
|
||||
void apic_timer_stop(void)
|
||||
{
|
||||
__asm__ volatile (
|
||||
"csrc sie, %0"
|
||||
::
|
||||
"r"(SIE_STIE) : "memory");
|
||||
s_sbi_time_ok = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return the expected cycle-counter ticks per heartbeat period.
|
||||
|
||||
@@ -17,6 +17,12 @@ volatile const char *g_sk_fault_word = (void *)0;
|
||||
|
||||
extern void riscv64_install_vectors(void);
|
||||
|
||||
/* Defined in arch/riscv64/apic.c. Declared here rather than in the shared
|
||||
* starkernel/apic.h because re-arming is specific to the one-shot SBI timer
|
||||
* and has no meaning for the amd64 periodic APIC timer. Same extern-in-place
|
||||
* convention as riscv64_install_vectors above. */
|
||||
extern void riscv64_timer_rearm(void);
|
||||
|
||||
/* scause cause codes for supervisor-mode interrupts (RISC-V Privileged Spec
|
||||
* §4.1.9, Table "Supervisor cause register values"). Only the timer is used;
|
||||
* software (1) and external (9) interrupts are not enabled. */
|
||||
@@ -30,11 +36,11 @@ extern void riscv64_install_vectors(void);
|
||||
* Unlike @c riscv64_exception_handler() this **returns** — the trap entry
|
||||
* restores the caller-saved register set and issues @c SRET.
|
||||
*
|
||||
* Supervisor timer (cause 5) is routed to @c heartbeat_tick(). No timer is
|
||||
* armed yet: arming via the SBI TIME extension, enabling @c sie.STIE, and the
|
||||
* mandatory per-tick re-arm are punch-list item 0.3. Until then this path is
|
||||
* unreachable, which is why item 0.2 accepts on "boots with no regression"
|
||||
* rather than on having observed an interrupt.
|
||||
* Supervisor timer (cause 5) re-arms the one-shot SBI timer and then accounts
|
||||
* the tick. **Re-arm comes first**: the SBI timer fires once per programmed
|
||||
* deadline, so any return path that skips the re-arm stops the heartbeat
|
||||
* permanently and silently. Ordering it ahead of @c heartbeat_tick() means a
|
||||
* fault in the bookkeeping cannot also cost the next tick.
|
||||
*
|
||||
* Any other cause is ignored rather than fatal: an unexpected-but-harmless
|
||||
* asynchronous interrupt should not take the kernel down, and none are
|
||||
@@ -47,6 +53,7 @@ void riscv64_interrupt_handler(uint64_t scause)
|
||||
uint64_t cause = scause & ~SCAUSE_INTERRUPT_BIT;
|
||||
|
||||
if (cause == SCAUSE_S_TIMER) {
|
||||
riscv64_timer_rearm();
|
||||
heartbeat_tick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -773,6 +773,7 @@ EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable
|
||||
/* Fill BootInfo fields that do NOT require EBS first */
|
||||
g_boot_info.runtime_services = SystemTable->RuntimeServices;
|
||||
g_boot_info.acpi_table = NULL;
|
||||
g_boot_info.dtb = NULL;
|
||||
g_boot_info.framebuffer.base = NULL;
|
||||
g_boot_info.framebuffer.size = 0;
|
||||
g_boot_info.framebuffer.width = 0;
|
||||
@@ -805,6 +806,19 @@ EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable
|
||||
if (!g_boot_info.acpi_table) {
|
||||
g_boot_info.acpi_table = acpi10_table;
|
||||
}
|
||||
|
||||
/* Devicetree blob, separate pass because the ACPI loop above breaks
|
||||
* early on a 2.0 hit. Absent on ACPI-only platforms; consumers must
|
||||
* handle NULL. riscv64 needs it for timebase-frequency (item 0.3) and
|
||||
* aarch64 for the GIC bases and timer PPI (item 0.6). */
|
||||
for (UINTN i = 0; i < SystemTable->NumberOfTableEntries; ++i)
|
||||
{
|
||||
if (guid_equals(&config_tables[i].VendorGuid, &EFI_DTB_TABLE_GUID))
|
||||
{
|
||||
g_boot_info.dtb = config_tables[i].VendorTable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_checkpoint(SystemTable, 6, L"Boot info collected (ACPI table located)");
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* fdt.c - Minimal flattened-devicetree reader
|
||||
*
|
||||
* Devicetree Specification v0.4 §5. Everything in the blob is big-endian; the
|
||||
* kernel targets are little-endian, so every field goes through be32().
|
||||
*/
|
||||
|
||||
#include "starkernel/fdt.h"
|
||||
|
||||
/* Structure block tokens (DT spec §5.4.1) */
|
||||
#define FDT_BEGIN_NODE 0x00000001u
|
||||
#define FDT_END_NODE 0x00000002u
|
||||
#define FDT_PROP 0x00000003u
|
||||
#define FDT_NOP 0x00000004u
|
||||
#define FDT_END 0x00000009u
|
||||
|
||||
#define FDT_MAGIC 0xd00dfeedu
|
||||
|
||||
/* Header layout (DT spec §5.2), all fields big-endian u32 */
|
||||
typedef struct
|
||||
{
|
||||
uint32_t magic;
|
||||
uint32_t totalsize;
|
||||
uint32_t off_dt_struct;
|
||||
uint32_t off_dt_strings;
|
||||
uint32_t off_mem_rsvmap;
|
||||
uint32_t version;
|
||||
uint32_t last_comp_version;
|
||||
uint32_t boot_cpuid_phys;
|
||||
uint32_t size_dt_strings;
|
||||
uint32_t size_dt_struct;
|
||||
} fdt_header_t;
|
||||
|
||||
/**
|
||||
* @brief Byte-swap a big-endian 32-bit field from the blob.
|
||||
*
|
||||
* Read bytewise rather than as a @c uint32_t load: structure-block tokens are
|
||||
* only guaranteed 4-byte aligned relative to the block start, and property
|
||||
* values carry no alignment guarantee at all.
|
||||
*/
|
||||
static uint32_t be32(const void* p)
|
||||
{
|
||||
const unsigned char* b = (const unsigned char*)p;
|
||||
return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) |
|
||||
((uint32_t)b[2] << 8) | (uint32_t)b[3];
|
||||
}
|
||||
|
||||
/** @brief Freestanding string compare; returns 1 when equal. */
|
||||
static int str_eq(const char* a, const char* b)
|
||||
{
|
||||
while (*a && (*a == *b))
|
||||
{
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
int fdt_valid(const void* fdt)
|
||||
{
|
||||
const fdt_header_t* h = (const fdt_header_t*)fdt;
|
||||
uint32_t total, off_struct, size_struct, off_strings, size_strings;
|
||||
|
||||
if (!fdt) return 0;
|
||||
if (be32(&h->magic) != FDT_MAGIC) return 0;
|
||||
|
||||
total = be32(&h->totalsize);
|
||||
off_struct = be32(&h->off_dt_struct);
|
||||
size_struct = be32(&h->size_dt_struct);
|
||||
off_strings = be32(&h->off_dt_strings);
|
||||
size_strings = be32(&h->size_dt_strings);
|
||||
|
||||
/* Both blocks must lie inside the blob. Written as subtraction against
|
||||
* total so a wrapped sum cannot smuggle an out-of-range block past the
|
||||
* check. */
|
||||
if (total < sizeof(fdt_header_t)) return 0;
|
||||
if (off_struct > total || size_struct > total - off_struct) return 0;
|
||||
if (off_strings > total || size_strings > total - off_strings) return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const void* fdt_find_prop(const void* fdt, const char* name, uint32_t* len_out)
|
||||
{
|
||||
const fdt_header_t* h = (const fdt_header_t*)fdt;
|
||||
const unsigned char *base, *p, *end, *strings;
|
||||
uint32_t size_struct;
|
||||
|
||||
if (!fdt_valid(fdt) || !name) return (void*)0;
|
||||
|
||||
base = (const unsigned char*)fdt;
|
||||
size_struct = be32(&h->size_dt_struct);
|
||||
p = base + be32(&h->off_dt_struct);
|
||||
end = p + size_struct;
|
||||
strings = base + be32(&h->off_dt_strings);
|
||||
|
||||
while (p + 4 <= end)
|
||||
{
|
||||
uint32_t token = be32(p);
|
||||
p += 4;
|
||||
|
||||
if (token == FDT_BEGIN_NODE)
|
||||
{
|
||||
/* NUL-terminated node name, padded to a 4-byte boundary */
|
||||
const unsigned char* q = p;
|
||||
while (q < end && *q) q++;
|
||||
if (q >= end) break;
|
||||
p = (const unsigned char*)(((uintptr_t)(q + 1) + 3u) & ~(uintptr_t)3u);
|
||||
}
|
||||
else if (token == FDT_PROP)
|
||||
{
|
||||
uint32_t len, nameoff;
|
||||
const unsigned char* val;
|
||||
|
||||
if (p + 8 > end) break;
|
||||
len = be32(p);
|
||||
nameoff = be32(p + 4);
|
||||
p += 8;
|
||||
val = p;
|
||||
if (len > (uint32_t)(end - p)) break;
|
||||
|
||||
if (str_eq((const char*)(strings + nameoff), name))
|
||||
{
|
||||
if (len_out) *len_out = len;
|
||||
return (const void*)val;
|
||||
}
|
||||
|
||||
p = (const unsigned char*)(((uintptr_t)(p + len) + 3u) & ~(uintptr_t)3u);
|
||||
}
|
||||
else if (token == FDT_END_NODE || token == FDT_NOP)
|
||||
{
|
||||
/* no payload */
|
||||
}
|
||||
else
|
||||
{
|
||||
/* FDT_END, or a token this reader does not know: stop. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (void*)0;
|
||||
}
|
||||
|
||||
int fdt_prop_u32(const void* fdt, const char* name, uint32_t* out)
|
||||
{
|
||||
uint32_t len = 0;
|
||||
const void* val = fdt_find_prop(fdt, name, &len);
|
||||
|
||||
if (!val || len != 4 || !out) return 0;
|
||||
*out = be32(val);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user