diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md index 0e9ed17..6e20369 100644 --- a/capsules/BLOCK_MAP.md +++ b/capsules/BLOCK_MAP.md @@ -1,5 +1,6 @@ # Capsule Block Manifest — Auto-generated - + + diff --git a/include/starkernel/boot_info_offsets.h b/include/starkernel/boot_info_offsets.h index 1fb942d..a8cd7d6 100644 --- a/include/starkernel/boot_info_offsets.h +++ b/include/starkernel/boot_info_offsets.h @@ -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 diff --git a/include/starkernel/fdt.h b/include/starkernel/fdt.h new file mode 100644 index 0000000..3f28501 --- /dev/null +++ b/include/starkernel/fdt.h @@ -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 + +/** + * @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 */ diff --git a/include/starkernel/uefi.h b/include/starkernel/uefi.h index afdd9fc..80dadad 100644 --- a/include/starkernel/uefi.h +++ b/include/starkernel/uefi.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; diff --git a/src/starkernel/arch/riscv64/apic.c b/src/starkernel/arch/riscv64/apic.c index ba2956e..6fbcd58 100644 --- a/src/starkernel/arch/riscv64/apic.c +++ b/src/starkernel/arch/riscv64/apic.c @@ -12,10 +12,127 @@ #include "apic.h" #include "uefi.h" +#include "console.h" +#include "timer.h" #include 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. diff --git a/src/starkernel/arch/riscv64/interrupts.c b/src/starkernel/arch/riscv64/interrupts.c index 5258678..2f6ad73 100644 --- a/src/starkernel/arch/riscv64/interrupts.c +++ b/src/starkernel/arch/riscv64/interrupts.c @@ -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(); } } diff --git a/src/starkernel/arch/riscv64/timer.c b/src/starkernel/arch/riscv64/timer.c index f15249e..aa78bfa 100644 --- a/src/starkernel/arch/riscv64/timer.c +++ b/src/starkernel/arch/riscv64/timer.c @@ -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 /** - * @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; diff --git a/src/starkernel/boot/uefi_loader.c b/src/starkernel/boot/uefi_loader.c index 8be2e7f..05f3aae 100644 --- a/src/starkernel/boot/uefi_loader.c +++ b/src/starkernel/boot/uefi_loader.c @@ -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)"); diff --git a/src/starkernel/hal/fdt.c b/src/starkernel/hal/fdt.c new file mode 100644 index 0000000..86bbc20 --- /dev/null +++ b/src/starkernel/hal/fdt.c @@ -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; +}