Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization: - Move StarForth v3 engine to v3/ (src/, include/, Makefile) - Move kernel to kernel/ (src/, include/, linker/, Makefile) - Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md) - Move FABRIC-0..4.md to docs/fabric/ - Move ONTOLOGY.md and ROADMAP.md to docs/ Board infrastructure: - Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/ - Each board has board.mk (ISA, CPU flags, boot recipe) and README.md - Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET - make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board - ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet) - scripts/mkdiskimage.sh builds disk images for all boards Docs pipeline: - docs/book/ with LaTeX master (main.tex) and Makefile - pandoc converts Markdown to LaTeX at build time - Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks) - make docs builds single PDF (754 pages, 0 missing characters) - make docs TARGET=<board> adds board appendix - build/docs/<book|board>/meta.tex stamps git commit into PDF Bug fixes: - 42 include paths that only worked by accident now use correct relative paths - clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build) - Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved - Doxyfile, .clang-tidy, README.md, Kconfig paths updated Verified: - Hosted v3 build passes 1012 tests, 0 failures - SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE) - Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes - make clean TARGET=<board> removes only that board and its ISA objects - make all builds all boards, hosted v3, and docs in one run Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# src/starkernel/arch/riscv64/
|
||||
|
||||
RISC-V 64-bit architecture support for LithosAnanke.
|
||||
|
||||
- `arch.c` — architecture init entry point.
|
||||
- `apic.c` — PLIC (Platform-Level Interrupt Controller) stub, named
|
||||
`apic.c` to match the shared per-arch file-naming convention across
|
||||
amd64/aarch64/riscv64 (RISC-V has no Local APIC). Satisfies the common
|
||||
`apic_init()`/EOI call sites without real PLIC bring-up yet; deferred to
|
||||
a later milestone.
|
||||
- `timer.c` — timer setup (heartbeat tick source).
|
||||
- `interrupts.c` — trap vector setup and interrupt dispatch.
|
||||
- `boot.S` — early boot assembly.
|
||||
- `isr.S` — interrupt service routine stubs.
|
||||
|
||||
No `kernel_entry.S` — see `Makefile.starkernel` for how the riscv64 loader
|
||||
image is produced (ELF → `scripts/gen_pe_reloc.py`-patched relocations →
|
||||
`objcopy` → PE).
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* apic.c (riscv64) - APIC stub. No Local APIC on RISC-V; interrupt
|
||||
* controller is PLIC (Platform-Level Interrupt Controller). apic_init()
|
||||
* calls into plic.c for the real PLIC bring-up (item 4.3.5b); apic_eoi()
|
||||
* stays a no-op since PLIC completion happens per-source in the trap
|
||||
* handler (claim/complete), not via a single shared EOI register.
|
||||
*/
|
||||
|
||||
#include "apic.h"
|
||||
#include "uefi.h"
|
||||
#include "console.h"
|
||||
#include "timer.h"
|
||||
#include "starkernel/plic.h"
|
||||
#include <stdint.h>
|
||||
|
||||
static uint64_t s_timer_period_tsc = 0;
|
||||
static uint64_t s_time_hz = 0; /* `time` CSR frequency (item 0.8/§26) */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 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.
|
||||
*
|
||||
* The step is @c heartbeat_next_period_ns() (item 0.8, §26) converted to
|
||||
* `time`-counter ticks via @c s_time_hz, not the fixed @c s_timer_period_tsc
|
||||
* used to seed the very first deadline in @c apic_timer_start() -- the
|
||||
* latter remains @c apic_timer_period_tsc()'s return value for
|
||||
* @c heartbeat_init()'s initial @c expected_delta, unchanged.
|
||||
*
|
||||
* Called from @c riscv64_interrupt_handler() in @c interrupts.c on
|
||||
* @c scause cause 5.
|
||||
*/
|
||||
void riscv64_timer_rearm(void)
|
||||
{
|
||||
uint64_t now;
|
||||
uint64_t step;
|
||||
|
||||
if (!s_sbi_time_ok) return;
|
||||
|
||||
step = (s_time_hz > 0)
|
||||
? (heartbeat_next_period_ns() * s_time_hz) / 1000000000ULL
|
||||
: s_timer_period_tsc;
|
||||
if (step == 0) {
|
||||
step = 1;
|
||||
}
|
||||
|
||||
s_next_deadline += step;
|
||||
|
||||
/* 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 + step;
|
||||
}
|
||||
|
||||
sbi_set_timer(s_next_deadline);
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief Initialise the interrupt controller: PLIC bring-up (item 4.3.5b).
|
||||
*
|
||||
* On x86-64 this function configures the Local APIC. On RISC-V the
|
||||
* Platform-Level Interrupt Controller (PLIC) handles external interrupt
|
||||
* routing -- @c plic_init() (arch/riscv64/plic.c) maps it, sets the S-mode
|
||||
* context's threshold, and enables @c sie.SEIE. No source is enabled here;
|
||||
* that is each future consumer's own job (4.3.5c for virtio-keyboard).
|
||||
*
|
||||
* @param boot_info Kernel boot information; @c dtb is passed through to
|
||||
* @c plic_init() for real-hardware PLIC base discovery as
|
||||
* of the 2026-09-05 fix (FABRIC-3.md §V.3 item 3) -- see
|
||||
* plic.c's file header for the discovery mechanism.
|
||||
* @return 0 always.
|
||||
*/
|
||||
int apic_init(BootInfo *boot_info)
|
||||
{
|
||||
plic_init(boot_info ? boot_info->dtb : (void *) 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signal End of Interrupt to the interrupt controller (RISC-V stub).
|
||||
*
|
||||
* On x86-64 this writes to the Local APIC EOI register. On RISC-V EOI
|
||||
* is handled by reading the PLIC claim/complete register; that path is
|
||||
* not yet wired. This stub is a no-op that satisfies the common interface.
|
||||
*/
|
||||
void apic_eoi(void) { }
|
||||
|
||||
/*
|
||||
* @brief Compute the expected `time`-counter ticks per heartbeat period.
|
||||
*
|
||||
* 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 ticks if either frequency is zero (a safe
|
||||
* non-zero sentinel preventing division-by-zero in the heartbeat path).
|
||||
*
|
||||
* @param tsc_hz `time` counter frequency from @c timer_init() (Hz).
|
||||
* @param tick_hz Desired heartbeat rate (Hz); 0 → uses fallback.
|
||||
* @return 0 always.
|
||||
*/
|
||||
int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz)
|
||||
{
|
||||
if (tick_hz > 0 && tsc_hz > 0)
|
||||
s_timer_period_tsc = tsc_hz / tick_hz;
|
||||
else
|
||||
s_timer_period_tsc = 10000000;
|
||||
s_time_hz = tsc_hz;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start timer delivery via the SBI TIME extension.
|
||||
*
|
||||
* 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)
|
||||
{
|
||||
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 timer delivery by masking @c sie.STIE.
|
||||
*
|
||||
* 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)
|
||||
{
|
||||
__asm__ volatile (
|
||||
"csrc sie, %0"
|
||||
::
|
||||
"r"(SIE_STIE) : "memory");
|
||||
s_sbi_time_ok = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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. 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 `time`-counter ticks per heartbeat period; 10,000,000 if
|
||||
* frequencies were not available at init time.
|
||||
*/
|
||||
uint64_t apic_timer_period_tsc(void)
|
||||
{
|
||||
return s_timer_period_tsc;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* arch.c (riscv64) - Architecture abstractions for StarKernel on RISC-V 64-bit
|
||||
*/
|
||||
|
||||
#include "arch.h"
|
||||
#include "console.h"
|
||||
#include <stdint.h>
|
||||
|
||||
extern void riscv64_install_vectors(void);
|
||||
extern char __kernel_start[]; /* same pattern pmm.c:67 already uses */
|
||||
|
||||
/* Item 4.3.5a. Prints satp's MODE/PPN fields and a known running address
|
||||
* as a permanent boot record (same diagnostic-verbosity convention as
|
||||
* apic.c's "AArch64: running at EL..." on the other ISA). Confirmed live,
|
||||
* 2026-08-08: firmware (EDK2 RISC-V) leaves satp.MODE=10 (Sv57) active at
|
||||
* kernel entry -- not Sv39/Sv48 as this file's own arch_mmu_init() stub
|
||||
* comment assumed; that comment is stale, corrected below. __kernel_start
|
||||
* printing here (0xbdd56982 in the confirming boot) landed inside the
|
||||
* UEFI-reported total physical RAM window (0x80000000 + 1020 MB), near the
|
||||
* top past the PMM-free region -- consistent with the kernel's own running
|
||||
* range being identity-mapped (VA==PA) under firmware's Sv57 table, not
|
||||
* some high canonical virtual range. That is the basis for the Bare-mode
|
||||
* switch below being safe *when firmware leaves a translation active at
|
||||
* all* -- see arch_early_init()'s own comment (FABRIC-3.md §V.3 item 7
|
||||
* fix, 2026-09-04) for why that no longer covers every boot path this
|
||||
* kernel now has. */
|
||||
static void print_hex64(uint64_t val)
|
||||
{
|
||||
char buf[19];
|
||||
buf[0] = '0'; buf[1] = 'x';
|
||||
for (int i = 15; i >= 0; i--) {
|
||||
int nibble = (int)(val & 0xF);
|
||||
buf[2 + i] = (char)(nibble < 10 ? '0' + nibble : 'a' + nibble - 10);
|
||||
val >>= 4;
|
||||
}
|
||||
buf[18] = '\0';
|
||||
console_puts(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform the earliest RISC-V architecture initialisation (no-op).
|
||||
*
|
||||
* RISC-V has no GDT or IDT; privilege separation is handled by the
|
||||
* machine/supervisor/user execution modes (M/S/U), and the trap vector
|
||||
* table is installed by writing @c stvec in @c arch_interrupts_init()
|
||||
* via @c riscv64_install_vectors(). This stub satisfies the common
|
||||
* @c arch_early_init() call site in @c kernel_main() without any
|
||||
* RISC-V-specific action.
|
||||
*
|
||||
* **Correction, 2026-09-04 (FABRIC-3.md §V.3 item 7's code audit).** The
|
||||
* unconditional Bare-mode switch this function used to perform was
|
||||
* justified entirely by behaviour observed under this system's QEMU/EDK2
|
||||
* RISC-V firmware (@c satp.MODE=10/Sv57, kernel identity-mapped within
|
||||
* it — see the comment on @c print_hex64() above). That observation
|
||||
* cannot apply to the native boot path (U-Boot+OpenSBI, no EDK2 at all,
|
||||
* FABRIC-3.md §V.1) this kernel now also has, and OpenSBI's own S-mode
|
||||
* handoff convention leaves @c satp already at 0 (Bare) in the common
|
||||
* case — meaning the switch below was very likely running as a harmless
|
||||
* no-op there, but on an unverified assumption rather than a checked one.
|
||||
* Rather than build a general Sv39/Sv48/Sv57 page-table walker just to
|
||||
* *prove* the running address is identity-mapped before switching (out of
|
||||
* proportion to this item's real scope), the fix narrows what actually
|
||||
* needs the QEMU-specific safety argument: **skip the switch entirely
|
||||
* when @c satp is already 0.** That is the expected case on both a
|
||||
* from-Bare native boot and any firmware that already left translation
|
||||
* off, and it needs no safety argument at all — there is nothing to
|
||||
* switch away from. The unconditional-switch path still exists for the
|
||||
* confirmed QEMU/EDK2 case (@c mode != 0), where the identity-mapping
|
||||
* argument above remains the actual justification, unchanged.
|
||||
*/
|
||||
void arch_early_init(void)
|
||||
{
|
||||
/* No GDT/IDT on RISC-V; privilege via exception levels */
|
||||
|
||||
uint64_t satp;
|
||||
__asm__ volatile ("csrr %0, satp" : "=r"(satp));
|
||||
uint64_t mode = satp >> 60;
|
||||
uint64_t ppn = satp & 0xFFFFFFFFFFFULL;
|
||||
|
||||
console_println("riscv64 item 4.3.5a: satp state at kernel entry (before switch)");
|
||||
console_puts(" satp.MODE = "); print_hex64(mode); console_puts("\n");
|
||||
console_puts(" satp.PPN = "); print_hex64(ppn); console_puts("\n");
|
||||
console_puts(" __kernel_start = "); print_hex64((uint64_t)__kernel_start); console_puts("\n");
|
||||
|
||||
if (mode == 0) {
|
||||
/* Already Bare -- the expected case on a native (OpenSBI) boot.
|
||||
* Nothing to switch away from, so no identity-mapping assumption
|
||||
* is needed at all: skipping is strictly safer than an
|
||||
* unconditional csrw+sfence that would just write back the same
|
||||
* zero value. */
|
||||
console_println("riscv64: satp already Bare -- no switch needed (item 4.3.5a)");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Explicit switch to Bare mode (satp.MODE=0): this kernel builds no
|
||||
* riscv64 page table of its own (vmm.c's is x86-64-shaped and never
|
||||
* activated here, per its own load_cr3() no-op outside __x86_64__) and
|
||||
* has no present use for virtual memory on this ISA, so there is no
|
||||
* reason to inherit firmware's mapping -- which is confirmed, for the
|
||||
* QEMU/EDK2 case this branch was written for, to have at least one
|
||||
* hole (PLIC_THRESHOLD, FABRIC-0.md item 4.3.5a). ExitBootServices()
|
||||
* has already completed several checkpoints before this function
|
||||
* runs on that path (ConOut/GOP done, BootServices exited per the
|
||||
* "[CKPT 008]" trace), so nothing downstream depends on firmware's
|
||||
* table surviving there. On the native boot path, reaching this
|
||||
* branch at all means OpenSBI's handoff left satp non-zero -- not
|
||||
* the case this kernel has confirmed, so the identity-mapping safety
|
||||
* argument is unverified here; flagged, not guessed past. Single asm
|
||||
* block: the CSR write and SFENCE.VMA must not be separated by a
|
||||
* compiler-scheduled memory access (RISC-V Privileged Spec §4.2.1,
|
||||
* SFENCE.VMA ordering). */
|
||||
__asm__ volatile (
|
||||
"csrw satp, x0\n"
|
||||
"sfence.vma\n"
|
||||
::: "memory"
|
||||
);
|
||||
|
||||
console_println("riscv64: satp cleared -- Bare mode, explicit (item 4.3.5a)");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enable supervisor-mode interrupts (RISC-V CSRSI sstatus).
|
||||
*
|
||||
* Issues @c CSRSI @c sstatus, @c 0x2 which sets bit 1 (@c SIE —
|
||||
* Supervisor Interrupt Enable) in the @c sstatus CSR. While @c SIE = 1,
|
||||
* pending supervisor-mode interrupts are taken. The @c memory clobber
|
||||
* prevents the compiler from reordering stores across the barrier.
|
||||
*
|
||||
* Must be called after @c arch_interrupts_init() has installed the
|
||||
* @c stvec trap handler, and after the PLIC driver has been configured
|
||||
* to forward the desired interrupt sources.
|
||||
*/
|
||||
void arch_enable_interrupts(void)
|
||||
{
|
||||
__asm__ volatile ("csrsi sstatus, 0x2" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Disable supervisor-mode interrupts (RISC-V CSRCI sstatus).
|
||||
*
|
||||
* Issues @c CSRCI @c sstatus, @c 0x2 which clears bit 1 (@c SIE) in
|
||||
* @c sstatus, masking all supervisor-mode interrupts. Machine-mode
|
||||
* interrupts are unaffected. Used around critical sections in the
|
||||
* single-threaded kernel. The @c memory clobber acts as a compiler
|
||||
* barrier; pair with a @c FENCE instruction for a hardware barrier.
|
||||
*/
|
||||
void arch_disable_interrupts(void)
|
||||
{
|
||||
__asm__ volatile ("csrci sstatus, 0x2" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Halt the core until the next interrupt (RISC-V WFI).
|
||||
*
|
||||
* Issues the @c WFI (Wait For Interrupt) instruction in supervisor mode.
|
||||
* The core enters a low-power state and resumes when a pending interrupt
|
||||
* is detected (or after a platform-specific timeout). In the kernel panic
|
||||
* path this is called inside an infinite loop with @c SIE cleared to stop
|
||||
* the processor permanently. In the idle path it reduces power consumption
|
||||
* between heartbeat ticks.
|
||||
*
|
||||
* The @c memory clobber prevents store-sinking across the @c WFI.
|
||||
*/
|
||||
void arch_halt(void)
|
||||
{
|
||||
__asm__ volatile ("wfi" ::: "memory");
|
||||
}
|
||||
|
||||
void arch_cold_reset(void)
|
||||
{
|
||||
arch_disable_interrupts();
|
||||
/* SBI SRST extension (0x53525354): sbi_system_reset, cold reboot = type 1 */
|
||||
register unsigned long a7 __asm__("a7") = 0x53525354UL;
|
||||
register unsigned long a6 __asm__("a6") = 0UL;
|
||||
register unsigned long a0 __asm__("a0") = 1UL; /* cold reboot */
|
||||
register unsigned long a1 __asm__("a1") = 0UL; /* no reason */
|
||||
__asm__ volatile ("ecall" : "+r"(a0) : "r"(a1), "r"(a6), "r"(a7) : "memory");
|
||||
for (;;) __asm__ volatile ("wfi" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the RISC-V CPU cycle counter (@c rdcycle).
|
||||
*
|
||||
* Issues the @c RDCYCLE pseudo-instruction (a @c CSRRS on @c cycle, CSR
|
||||
* 0xC00) to read the 64-bit hardware performance counter that counts
|
||||
* elapsed clock cycles since reset. On RISC-V the cycle counter is the
|
||||
* architectural equivalent of the x86-64 TSC.
|
||||
*
|
||||
* Unlike the AArch64 @c CNTPCT_EL0, the RISC-V @c cycle register is a
|
||||
* per-hart counter and is not architecturally guaranteed to be synchronised
|
||||
* across harts. For single-core LithosAnanke this is not a concern.
|
||||
* Frequency is not architecturally specified; @c timer_init() assumes
|
||||
* 1 GHz for QEMU @c virt board compatibility.
|
||||
*
|
||||
* @return Current 64-bit cycle counter value; frequency platform-dependent.
|
||||
*/
|
||||
uint64_t arch_read_timestamp(void)
|
||||
{
|
||||
uint64_t val;
|
||||
__asm__ volatile ("rdcycle %0" : "=r"(val));
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialise the RISC-V MMU (stub — deferred to a later milestone).
|
||||
*
|
||||
* Full Sv39/Sv48 page-table setup (SATP, PMP, etc.) is deferred to a
|
||||
* later milestone. This stub satisfies the common @c arch_mmu_init()
|
||||
* call site shared across all three supported ISAs. The VMM subsystem
|
||||
* (@c vmm.c) handles page-table management independently of this hook.
|
||||
*/
|
||||
void arch_mmu_init(void)
|
||||
{
|
||||
/* Stub: Sv39/Sv48 bring-up deferred */
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* boot.S (riscv64) - Entry stub and linker-script symbol substitutes.
|
||||
*
|
||||
* Defines __text_start, __text_end, __kernel_start, __kernel_end so
|
||||
* hal.c and pmm.c resolve. They all point to the image base, which is
|
||||
* sufficient for the monolithic loader build (UEFI manages page ownership).
|
||||
*/
|
||||
.text
|
||||
.global __text_start
|
||||
.global __kernel_start
|
||||
__text_start:
|
||||
__kernel_start:
|
||||
|
||||
.global arch_bootstrap
|
||||
arch_bootstrap:
|
||||
ret
|
||||
|
||||
.global __text_end
|
||||
.global __kernel_end
|
||||
__text_end:
|
||||
__kernel_end:
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* interrupts.c (riscv64) - Trap vector setup and common exception handler
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "arch.h"
|
||||
#include "console.h"
|
||||
#include "starkernel/timer.h"
|
||||
#include "starkernel/plic.h"
|
||||
|
||||
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);
|
||||
|
||||
/* Defined in virtio_input.c (item 4.3.5c). g_virtio_input_plic_source is 0
|
||||
* (plic_claim()'s own "nothing pending" sentinel) until
|
||||
* virtio_input_find_keyboard() finds a real device and computes its
|
||||
* routed source -- so this branch is inert on any boot where the device
|
||||
* isn't present, same as the generic dispatch was inert before 4.3.5c. */
|
||||
extern uint32_t g_virtio_input_plic_source;
|
||||
extern void virtio_input_isr(void);
|
||||
|
||||
/* scause cause codes for supervisor-mode interrupts (RISC-V Privileged Spec
|
||||
* §4.1.9, Table "Supervisor cause register values"). */
|
||||
#define SCAUSE_INTERRUPT_BIT (1ULL << 63)
|
||||
#define SCAUSE_S_TIMER 5ULL
|
||||
#define SCAUSE_S_EXTERNAL 9ULL
|
||||
|
||||
/* Count of real PLIC claims serviced since boot (item 4.3.5b). Cheap
|
||||
* standing diagnostic, same shape as amd64's g_i8042_isr_count/
|
||||
* g_spurious_count -- confirms the claim/dispatch/complete path is alive
|
||||
* without needing a live debugger. Nothing enables any PLIC source by
|
||||
* default, so this stays 0 unless a future consumer (or a live test)
|
||||
* explicitly calls plic_enable(). */
|
||||
volatile uint32_t g_plic_claim_count = 0;
|
||||
|
||||
/**
|
||||
* @brief Dispatch an asynchronous supervisor interrupt.
|
||||
*
|
||||
* Called from @c riscv64_trap_entry in @c isr.S when @c scause has bit 63 set.
|
||||
* Unlike @c riscv64_exception_handler() this **returns** — the trap entry
|
||||
* restores the caller-saved register set and issues @c SRET.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Supervisor external (cause 9): claim/dispatch/complete through the PLIC
|
||||
* (substrate, item 4.3.5b). Source 10 (UART) stays unclaimed by anything
|
||||
* here -- 4.3.5b's own synthetic-interrupt self-test that used it was run
|
||||
* once and reverted, not a standing consumer. virtio-input (item 4.3.5c)
|
||||
* is the first real per-source dispatch, the same shape as i8042's
|
||||
* keyboard branch in amd64's isr_common_handler().
|
||||
*
|
||||
* Any other cause is ignored rather than fatal: an unexpected-but-harmless
|
||||
* asynchronous interrupt should not take the kernel down.
|
||||
*
|
||||
* @param scause Raw @c scause value, interrupt bit still set.
|
||||
*/
|
||||
void riscv64_interrupt_handler(uint64_t scause)
|
||||
{
|
||||
uint64_t cause = scause & ~SCAUSE_INTERRUPT_BIT;
|
||||
|
||||
if (cause == SCAUSE_S_TIMER) {
|
||||
riscv64_timer_rearm();
|
||||
heartbeat_tick();
|
||||
} else if (cause == SCAUSE_S_EXTERNAL) {
|
||||
uint32_t irq = plic_claim();
|
||||
if (irq != 0) {
|
||||
g_plic_claim_count++;
|
||||
if (irq == g_virtio_input_plic_source) {
|
||||
virtio_input_isr();
|
||||
}
|
||||
plic_complete(irq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print a 64-bit value as "0xNNNNNNNNNNNNNNNN" to the kernel console.
|
||||
*
|
||||
* Formats @p val as a 16-nibble lowercase hex string preceded by "0x" into
|
||||
* a 19-byte stack buffer (including null terminator) and emits it via
|
||||
* @c console_puts(). Used by @c riscv64_exception_handler() to print
|
||||
* @c scause, @c sepc, and @c stval in the exception diagnostic dump.
|
||||
* No heap allocation or libc dependency.
|
||||
*
|
||||
* @param val 64-bit value to format.
|
||||
*/
|
||||
static void print_hex(uint64_t val)
|
||||
{
|
||||
char buf[19];
|
||||
buf[0] = '0'; buf[1] = 'x';
|
||||
for (int i = 15; i >= 0; i--) {
|
||||
int nibble = (int)(val & 0xF);
|
||||
buf[2 + i] = (char)(nibble < 10 ? '0' + nibble : 'a' + nibble - 10);
|
||||
val >>= 4;
|
||||
}
|
||||
buf[18] = '\0';
|
||||
console_puts(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RISC-V common trap handler — print diagnostic and spin.
|
||||
*
|
||||
* Called from the @c isr.S trap vector for all synchronous exceptions
|
||||
* and asynchronous interrupts that are not handled by a specific driver.
|
||||
* Reads three supervisor-mode CSRs directly:
|
||||
*
|
||||
* - @c scause — Supervisor Cause Register. Bit 63 distinguishes
|
||||
* interrupts (1) from exceptions (0); bits [62:0] encode the cause
|
||||
* code (e.g., 0 = instruction address misaligned, 2 = illegal
|
||||
* instruction, 5 = load access fault, 13 = store page fault,
|
||||
* 8 = ecall from U-mode). See RISC-V Privileged Spec §4.1.9.
|
||||
* - @c sepc — Supervisor Exception Program Counter. The PC of the
|
||||
* instruction that caused the trap (or the return address for
|
||||
* interrupt handlers); execution resumes from @c sepc on SRET.
|
||||
* - @c stval — Supervisor Trap Value. For load/store page faults this
|
||||
* holds the faulting virtual address; for illegal instruction faults
|
||||
* it holds the faulting instruction encoding; zero for other causes.
|
||||
*
|
||||
* Prints all three values to the kernel console then enters an infinite
|
||||
* @c WFI loop to halt the core permanently. All exceptions that reach
|
||||
* this handler are fatal in the current single-hart kernel.
|
||||
*
|
||||
* @c g_sk_fault_word is set by the VM FORTH dispatcher and names the
|
||||
* word executing at the time of the trap, aiding post-mortem serial
|
||||
* log analysis.
|
||||
*/
|
||||
void riscv64_exception_handler(void)
|
||||
{
|
||||
uint64_t scause, sepc, stval;
|
||||
__asm__ volatile ("csrr %0, scause" : "=r"(scause));
|
||||
__asm__ volatile ("csrr %0, sepc" : "=r"(sepc));
|
||||
__asm__ volatile ("csrr %0, stval" : "=r"(stval));
|
||||
|
||||
console_println("*** EXCEPTION (riscv64) ***");
|
||||
console_puts(" scause = "); print_hex(scause); console_puts("\n");
|
||||
console_puts(" sepc = "); print_hex(sepc); console_puts("\n");
|
||||
console_puts(" stval = "); print_hex(stval); console_puts("\n");
|
||||
|
||||
for (;;) {
|
||||
__asm__ volatile ("wfi" ::: "memory");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Install the RISC-V supervisor trap vector table (M4 milestone).
|
||||
*
|
||||
* Calls @c riscv64_install_vectors() (defined in @c isr.S) which writes
|
||||
* the trap vector base address into @c stvec (Supervisor Trap Vector Base
|
||||
* Address Register, CSR 0x105). The vector mode field (bits [1:0]) is
|
||||
* set to @c 0 (Direct mode) so all traps dispatch to the single base
|
||||
* address, or @c 1 (Vectored mode) so async interrupts dispatch to
|
||||
* @c stvec + 4*cause.
|
||||
*
|
||||
* On RISC-V there is no IDT and no legacy PIC; the PLIC replaces both.
|
||||
* The @c arch_interrupts_init() name is shared across all three ISAs so
|
||||
* that @c kernel_main() calls the same symbol regardless of target.
|
||||
*
|
||||
* Must be called after the supervisor stack (@c sp) is established and
|
||||
* before @c arch_enable_interrupts().
|
||||
*/
|
||||
void arch_interrupts_init(void)
|
||||
{
|
||||
riscv64_install_vectors();
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* isr.S (riscv64) - Trap vector installation.
|
||||
* Routes all S-mode exceptions to a common C handler.
|
||||
*/
|
||||
|
||||
.section .text
|
||||
.balign 4
|
||||
|
||||
/* Hidden for consistency with the amd64 isr.S convention. Neither symbol
|
||||
* here is ever address-read from C (interrupts.c only *calls*
|
||||
* riscv64_install_vectors(); the trap entry itself is entirely
|
||||
* self-contained in this file), so this isn't fixing a live bug the way
|
||||
* the amd64 .hidden directives do -- it's defensive hardening against the
|
||||
* same GOT-indirection class of issue should that ever change. */
|
||||
.hidden riscv64_trap_entry
|
||||
.hidden riscv64_install_vectors
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Trap frame layout (offsets from sp), 512 bytes, 16-byte aligned.
|
||||
*
|
||||
* FABRIC-3.md SXXVIII (Stage 0, preemptive-context-switch trap-frame parity,
|
||||
* 2026-09-13): originally only the LP64D psABI's *caller-saved* registers
|
||||
* were saved here, on the reasoning that a C handler preserves callee-saved
|
||||
* registers itself. That reasoning holds for an ordinary function call, but
|
||||
* not for an asynchronous trap: the interrupted code's own compiled
|
||||
* instructions have no call site to justify losing state at, so a value
|
||||
* genuinely live in a callee-saved register at the moment of interrupt has
|
||||
* no ABI guarantee of surviving. s0-s11/fs0-fs11 are now saved too, so this
|
||||
* frame is, in principle, sufficient to resume execution anywhere it was
|
||||
* taken. Not yet copied into any per-VM saved context (that's Stage 2) --
|
||||
* this stage only makes the frame itself complete.
|
||||
*
|
||||
* integer caller-saved (16): ra, t0-t6, a0-a7
|
||||
* integer callee-saved (12): s0-s11
|
||||
* FP caller-saved (20): ft0-ft11, fa0-fa7 + fcsr
|
||||
* FP callee-saved (12): fs0-fs11
|
||||
* supervisor CSRs (2): sepc, sstatus
|
||||
*
|
||||
* The FP half (caller- and callee-saved alike) is saved only when
|
||||
* sstatus.FS != Off. Nothing in boot.S or kernel_entry.S programs FS, so its
|
||||
* state is whatever firmware left; touching an f-register while FS == Off
|
||||
* raises an illegal-instruction trap, which inside the trap handler would be
|
||||
* unrecoverable. The kernel image does contain real FP code (fld/fmul.d/fcvt
|
||||
* on the Q48.16 and statistics paths), so the save cannot simply be omitted
|
||||
* -- it has to be conditional.
|
||||
* ------------------------------------------------------------------------- */
|
||||
.equ TF_RA, 0
|
||||
.equ TF_T0, 8
|
||||
.equ TF_T1, 16
|
||||
.equ TF_T2, 24
|
||||
.equ TF_T3, 32
|
||||
.equ TF_T4, 40
|
||||
.equ TF_T5, 48
|
||||
.equ TF_T6, 56
|
||||
.equ TF_A0, 64
|
||||
.equ TF_A1, 72
|
||||
.equ TF_A2, 80
|
||||
.equ TF_A3, 88
|
||||
.equ TF_A4, 96
|
||||
.equ TF_A5, 104
|
||||
.equ TF_A6, 112
|
||||
.equ TF_A7, 120
|
||||
.equ TF_S0, 128 /* s0-s11 : 128 .. 216 */
|
||||
.equ TF_SEPC, 224
|
||||
.equ TF_SSTATUS,232
|
||||
.equ TF_FCSR, 240
|
||||
.equ TF_F0, 248 /* ft0-ft7 : 248 .. 304 */
|
||||
.equ TF_FA0, 312 /* fa0-fa7 : 312 .. 368 */
|
||||
.equ TF_FT8, 376 /* ft8-ft11 : 376 .. 400 */
|
||||
.equ TF_FS0, 408 /* fs0-fs11 : 408 .. 496 */
|
||||
.equ TF_SIZE, 512
|
||||
|
||||
/* sstatus.FS occupies bits [14:13]; 0 = Off */
|
||||
.equ SSTATUS_FS_SHIFT, 13
|
||||
.equ SSTATUS_FS_MASK, 3
|
||||
|
||||
/* Direct-mode trap entry: stvec[1:0] = 0 means all traps branch here.
|
||||
* stvec requires 4-byte alignment. */
|
||||
.balign 4
|
||||
.global riscv64_trap_entry
|
||||
riscv64_trap_entry:
|
||||
addi sp, sp, -TF_SIZE
|
||||
|
||||
sd ra, TF_RA(sp)
|
||||
sd t0, TF_T0(sp)
|
||||
sd t1, TF_T1(sp)
|
||||
sd t2, TF_T2(sp)
|
||||
sd t3, TF_T3(sp)
|
||||
sd t4, TF_T4(sp)
|
||||
sd t5, TF_T5(sp)
|
||||
sd t6, TF_T6(sp)
|
||||
sd a0, TF_A0(sp)
|
||||
sd a1, TF_A1(sp)
|
||||
sd a2, TF_A2(sp)
|
||||
sd a3, TF_A3(sp)
|
||||
sd a4, TF_A4(sp)
|
||||
sd a5, TF_A5(sp)
|
||||
sd a6, TF_A6(sp)
|
||||
sd a7, TF_A7(sp)
|
||||
|
||||
sd s0, TF_S0+0(sp)
|
||||
sd s1, TF_S0+8(sp)
|
||||
sd s2, TF_S0+16(sp)
|
||||
sd s3, TF_S0+24(sp)
|
||||
sd s4, TF_S0+32(sp)
|
||||
sd s5, TF_S0+40(sp)
|
||||
sd s6, TF_S0+48(sp)
|
||||
sd s7, TF_S0+56(sp)
|
||||
sd s8, TF_S0+64(sp)
|
||||
sd s9, TF_S0+72(sp)
|
||||
sd s10, TF_S0+80(sp)
|
||||
sd s11, TF_S0+88(sp)
|
||||
|
||||
csrr t0, sepc
|
||||
sd t0, TF_SEPC(sp)
|
||||
csrr t0, sstatus
|
||||
sd t0, TF_SSTATUS(sp)
|
||||
|
||||
/* Save FP only if FS != Off */
|
||||
srli t1, t0, SSTATUS_FS_SHIFT
|
||||
andi t1, t1, SSTATUS_FS_MASK
|
||||
beqz t1, 1f
|
||||
|
||||
frcsr t2
|
||||
sd t2, TF_FCSR(sp)
|
||||
fsd ft0, TF_F0+0(sp)
|
||||
fsd ft1, TF_F0+8(sp)
|
||||
fsd ft2, TF_F0+16(sp)
|
||||
fsd ft3, TF_F0+24(sp)
|
||||
fsd ft4, TF_F0+32(sp)
|
||||
fsd ft5, TF_F0+40(sp)
|
||||
fsd ft6, TF_F0+48(sp)
|
||||
fsd ft7, TF_F0+56(sp)
|
||||
fsd fa0, TF_FA0+0(sp)
|
||||
fsd fa1, TF_FA0+8(sp)
|
||||
fsd fa2, TF_FA0+16(sp)
|
||||
fsd fa3, TF_FA0+24(sp)
|
||||
fsd fa4, TF_FA0+32(sp)
|
||||
fsd fa5, TF_FA0+40(sp)
|
||||
fsd fa6, TF_FA0+48(sp)
|
||||
fsd fa7, TF_FA0+56(sp)
|
||||
fsd ft8, TF_FT8+0(sp)
|
||||
fsd ft9, TF_FT8+8(sp)
|
||||
fsd ft10, TF_FT8+16(sp)
|
||||
fsd ft11, TF_FT8+24(sp)
|
||||
fsd fs0, TF_FS0+0(sp)
|
||||
fsd fs1, TF_FS0+8(sp)
|
||||
fsd fs2, TF_FS0+16(sp)
|
||||
fsd fs3, TF_FS0+24(sp)
|
||||
fsd fs4, TF_FS0+32(sp)
|
||||
fsd fs5, TF_FS0+40(sp)
|
||||
fsd fs6, TF_FS0+48(sp)
|
||||
fsd fs7, TF_FS0+56(sp)
|
||||
fsd fs8, TF_FS0+64(sp)
|
||||
fsd fs9, TF_FS0+72(sp)
|
||||
fsd fs10, TF_FS0+80(sp)
|
||||
fsd fs11, TF_FS0+88(sp)
|
||||
1:
|
||||
|
||||
/* scause bit 63 set => interrupt (negative when read as signed). */
|
||||
csrr a0, scause
|
||||
bltz a0, 2f
|
||||
|
||||
/* Synchronous exception: fatal, does not return. */
|
||||
call riscv64_exception_handler
|
||||
9: wfi
|
||||
j 9b
|
||||
|
||||
2: /* Interrupt: a0 already holds scause for the C dispatcher. */
|
||||
call riscv64_interrupt_handler
|
||||
|
||||
/* Restore. Reload the saved sstatus first -- it decides whether the FP
|
||||
* half was written, and it must be restored *after* the f-registers so
|
||||
* that FS is back to the interrupted context's value on exit. */
|
||||
ld t0, TF_SSTATUS(sp)
|
||||
srli t1, t0, SSTATUS_FS_SHIFT
|
||||
andi t1, t1, SSTATUS_FS_MASK
|
||||
beqz t1, 3f
|
||||
|
||||
ld t2, TF_FCSR(sp)
|
||||
fscsr t2
|
||||
fld ft0, TF_F0+0(sp)
|
||||
fld ft1, TF_F0+8(sp)
|
||||
fld ft2, TF_F0+16(sp)
|
||||
fld ft3, TF_F0+24(sp)
|
||||
fld ft4, TF_F0+32(sp)
|
||||
fld ft5, TF_F0+40(sp)
|
||||
fld ft6, TF_F0+48(sp)
|
||||
fld ft7, TF_F0+56(sp)
|
||||
fld fa0, TF_FA0+0(sp)
|
||||
fld fa1, TF_FA0+8(sp)
|
||||
fld fa2, TF_FA0+16(sp)
|
||||
fld fa3, TF_FA0+24(sp)
|
||||
fld fa4, TF_FA0+32(sp)
|
||||
fld fa5, TF_FA0+40(sp)
|
||||
fld fa6, TF_FA0+48(sp)
|
||||
fld fa7, TF_FA0+56(sp)
|
||||
fld ft8, TF_FT8+0(sp)
|
||||
fld ft9, TF_FT8+8(sp)
|
||||
fld ft10, TF_FT8+16(sp)
|
||||
fld ft11, TF_FT8+24(sp)
|
||||
fld fs0, TF_FS0+0(sp)
|
||||
fld fs1, TF_FS0+8(sp)
|
||||
fld fs2, TF_FS0+16(sp)
|
||||
fld fs3, TF_FS0+24(sp)
|
||||
fld fs4, TF_FS0+32(sp)
|
||||
fld fs5, TF_FS0+40(sp)
|
||||
fld fs6, TF_FS0+48(sp)
|
||||
fld fs7, TF_FS0+56(sp)
|
||||
fld fs8, TF_FS0+64(sp)
|
||||
fld fs9, TF_FS0+72(sp)
|
||||
fld fs10, TF_FS0+80(sp)
|
||||
fld fs11, TF_FS0+88(sp)
|
||||
3:
|
||||
csrw sstatus, t0
|
||||
ld t0, TF_SEPC(sp)
|
||||
csrw sepc, t0
|
||||
|
||||
ld ra, TF_RA(sp)
|
||||
ld t0, TF_T0(sp)
|
||||
ld t1, TF_T1(sp)
|
||||
ld t2, TF_T2(sp)
|
||||
ld t3, TF_T3(sp)
|
||||
ld t4, TF_T4(sp)
|
||||
ld t5, TF_T5(sp)
|
||||
ld t6, TF_T6(sp)
|
||||
ld a0, TF_A0(sp)
|
||||
ld a1, TF_A1(sp)
|
||||
ld a2, TF_A2(sp)
|
||||
ld a3, TF_A3(sp)
|
||||
ld a4, TF_A4(sp)
|
||||
ld a5, TF_A5(sp)
|
||||
ld a6, TF_A6(sp)
|
||||
ld a7, TF_A7(sp)
|
||||
|
||||
ld s0, TF_S0+0(sp)
|
||||
ld s1, TF_S0+8(sp)
|
||||
ld s2, TF_S0+16(sp)
|
||||
ld s3, TF_S0+24(sp)
|
||||
ld s4, TF_S0+32(sp)
|
||||
ld s5, TF_S0+40(sp)
|
||||
ld s6, TF_S0+48(sp)
|
||||
ld s7, TF_S0+56(sp)
|
||||
ld s8, TF_S0+64(sp)
|
||||
ld s9, TF_S0+72(sp)
|
||||
ld s10, TF_S0+80(sp)
|
||||
ld s11, TF_S0+88(sp)
|
||||
|
||||
addi sp, sp, TF_SIZE
|
||||
sret
|
||||
|
||||
.global riscv64_install_vectors
|
||||
riscv64_install_vectors:
|
||||
la t0, riscv64_trap_entry
|
||||
csrw stvec, t0
|
||||
ret
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* kernel_entry.S — kernel stack setup and entry trampoline (riscv64)
|
||||
*
|
||||
* UEFI hands control to kernel_main() running on EDK2's own boot-time stack.
|
||||
* Unlike amd64 (see arch/amd64/kernel_entry.S, which has carried this same
|
||||
* trampoline since early on), riscv64 previously called straight into
|
||||
* kernel_main_impl with no stack switch at all. The VM bootstrap's call
|
||||
* depth (27 chained word-registration modules, physics/SSM init, capsule
|
||||
* birth, Tripod fleet spawn) overflowed EDK2's riscv64 boot stack partway
|
||||
* through vm_init_with_host()'s return, corrupting the return address and
|
||||
* producing a wild jump shortly after — observed as a load/instruction
|
||||
* page fault (scause 0xd or 0xc) whose exact address drifted between
|
||||
* builds while the crash always landed in this same call region. amd64
|
||||
* never had this problem because kernel_entry.S already switches to a
|
||||
* dedicated 2 MiB BSS stack before anything deep runs; aarch64 apparently
|
||||
* gets away with EDK2's default boot stack being large enough, but that's
|
||||
* incidental, not a guarantee.
|
||||
*
|
||||
* kernel_main is the symbol the UEFI loader calls (extern in uefi_loader.c).
|
||||
* kernel_main_impl is defined in kernel_main.c and contains all the C code.
|
||||
*
|
||||
* Calling convention: RISC-V LP64D — boot_info pointer arrives in a0 and is
|
||||
* passed unchanged to kernel_main_impl via the tail-call.
|
||||
*/
|
||||
|
||||
#include "starkernel/boot_info_offsets.h"
|
||||
|
||||
/* 2 MiB kernel stack in BSS — fallback when no dynamic stack requested */
|
||||
.section .bss
|
||||
.align 4
|
||||
.global g_kernel_stack
|
||||
g_kernel_stack:
|
||||
.space 0x200000
|
||||
g_kernel_stack_top:
|
||||
.global g_kernel_stack_top
|
||||
|
||||
.section .text
|
||||
.extern kernel_main_impl
|
||||
|
||||
/*
|
||||
* kernel_main — stack-switch trampoline, then tail-call kernel_main_impl.
|
||||
*
|
||||
* a0 = BootInfo* (RISC-V LP64D ABI — first argument register)
|
||||
*
|
||||
* Stack selection (checked in order):
|
||||
* 1. boot_info->kernel_stack_base != 0 → use loader-allocated stack
|
||||
* (top = base + size, aligned down to 16)
|
||||
* 2. Otherwise → fall back to g_kernel_stack (2 MiB BSS)
|
||||
*
|
||||
* Never returns; kernel_main_impl runs the REPL forever or panics.
|
||||
*/
|
||||
.global kernel_main
|
||||
kernel_main:
|
||||
ld t0, BOOT_INFO_KERNEL_STACK_BASE_OFFSET(a0)
|
||||
bnez t0, .Ldynamic_stack
|
||||
|
||||
/* BSS fallback */
|
||||
la t0, g_kernel_stack_top
|
||||
j .Lstack_ready
|
||||
|
||||
.Ldynamic_stack:
|
||||
ld t1, BOOT_INFO_KERNEL_STACK_SIZE_OFFSET(a0)
|
||||
add t0, t0, t1 /* top = base + size (stack grows down) */
|
||||
|
||||
.Lstack_ready:
|
||||
andi t0, t0, -16 /* 16-byte alignment (RISC-V psABI requirement) */
|
||||
mv sp, t0
|
||||
mv s0, zero /* terminate frame-pointer chain */
|
||||
tail kernel_main_impl /* a0 (boot_info) already in place */
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* plic.c (riscv64) - Platform-Level Interrupt Controller driver
|
||||
*
|
||||
* Item 4.3.5b. Register layout (priority/pending/enable/threshold/claim-
|
||||
* complete) is the standard SiFive PLIC-1.0.0 layout QEMU implements --
|
||||
* architectural, not board-specific, same status as the GICD/GICC register
|
||||
* *offsets* item 0.6 cross-checked against the Linux arm-gic header.
|
||||
*
|
||||
* PLIC_BASE_QEMU_DEFAULT and PLIC_CONTEXT_S are QEMU-virt-specific and were
|
||||
* NOT hardcoded from memory: verified 2026-08-08 via
|
||||
* `qemu-system-riscv64 -machine virt,dumpdtb=...`, decoded with a script
|
||||
* using this tree's own fdt.c parsing logic (not dtc, which is not
|
||||
* installed). The real devicetree shows:
|
||||
* soc/plic@c000000: compatible = "sifive,plic-1.0.0","riscv,plic0",
|
||||
* reg = <0x0 0xc000000 0x0 0x600000>, riscv,ndev = 95
|
||||
* soc/plic@c000000: interrupts-extended = <&cpu0-intc 11 &cpu0-intc 9>
|
||||
* -- two contexts for hart 0: context 0 feeds cause 11 (MEIE, M-mode
|
||||
* external), context 1 feeds cause 9 (SEIE, S-mode external). Context
|
||||
* index is position in this list, confirmed rather than assumed.
|
||||
* soc/serial@10000000 (ns16550a): interrupts = <10>, interrupt-parent =
|
||||
* &plic -- PLIC source 10, matching console.c's existing NS16550_BASE.
|
||||
* Confirmed for this exact QEMU 10.2.1 build; not assumed stable across
|
||||
* versions, same caveat item 0.6 recorded for its own dumped values.
|
||||
*
|
||||
* DTB-discovered base address, as of FABRIC-3.md §V.3 item 3's fix,
|
||||
* 2026-09-05: the node-scoped ("reg" is not globally unique the way
|
||||
* "timebase-frequency" is) devicetree lookup this file's own comment
|
||||
* previously flagged as "a bigger addition than this item's stated scope"
|
||||
* was subsequently built anyway -- `fdt_find_node_by_compatible()` +
|
||||
* `fdt_find_prop_in_node()` (`fdt.c`/`fdt.h`), added for the aarch64 GIC
|
||||
* base fix (FABRIC-3.md §IV.3 item 7) and reused here unchanged.
|
||||
* `plic_base_from_dtb()` below tries `"sifive,plic-1.0.0"` first; on
|
||||
* failure (no DTB, e.g. this system's QEMU/UEFI firmware -- same
|
||||
* `fdt_valid()` failure mode apic.c's GIC discovery already documents) it
|
||||
* falls back to the QEMU constant above, unchanged from this function's
|
||||
* previous unconditional behaviour. Not yet confirmed against the Milk-V
|
||||
* Mars's real DTB -- the compatible string is architectural (SiFive
|
||||
* PLIC-1.0.0 is the standard binding), not board-specific, but the actual
|
||||
* JH7110 `reg` value is unverified until real hardware arrives (2026-09-17).
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "starkernel/plic.h"
|
||||
#include "starkernel/fdt.h"
|
||||
#include "console.h"
|
||||
|
||||
#define PLIC_BASE_QEMU_DEFAULT 0x0C000000UL
|
||||
#define PLIC_CONTEXT_S 1u /* S-mode, hart 0 -- verified, see file header */
|
||||
|
||||
/* Runtime PLIC base address -- set once by plic_init(), defaulting to the
|
||||
* QEMU constant above until/unless plic_base_from_dtb() overrides it.
|
||||
* uintptr_t, not a #define, precisely because this is no longer a
|
||||
* compile-time constant on this path (same shape as aarch64 apic.c's
|
||||
* s_gicd_base/s_gicc_base). */
|
||||
static uintptr_t s_plic_base = PLIC_BASE_QEMU_DEFAULT;
|
||||
|
||||
#define PLIC_PRIORITY(irq) (s_plic_base + 4u * (irq))
|
||||
#define PLIC_ENABLE(ctx) (s_plic_base + 0x2000u + 0x80u * (ctx))
|
||||
#define PLIC_THRESHOLD(ctx) (s_plic_base + 0x200000u + 0x1000u * (ctx))
|
||||
#define PLIC_CLAIM(ctx) (s_plic_base + 0x200004u + 0x1000u * (ctx))
|
||||
|
||||
static inline volatile uint32_t *reg32(uintptr_t addr) {
|
||||
return (volatile uint32_t *)addr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read one big-endian 32-bit cell from a devicetree property blob.
|
||||
*
|
||||
* Duplicated from aarch64 apic.c's identical helper rather than shared --
|
||||
* same "a few lines is simpler than a new shared dependency" precedent
|
||||
* that file's own comment already set for this exact situation.
|
||||
*/
|
||||
static uint32_t be32_cell(const unsigned char *b) {
|
||||
return ((uint32_t) b[0] << 24) | ((uint32_t) b[1] << 16) |
|
||||
((uint32_t) b[2] << 8) | (uint32_t) b[3];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read one two-cell (64-bit) big-endian address from a `reg` entry.
|
||||
*
|
||||
* QEMU-virt's `plic` node's own dumped `reg = <0x0 0xc000000 0x0 0x600000>`
|
||||
* (see file header) is address(2 cells) + size(2 cells) = 4 cells total,
|
||||
* same shape as aarch64's `gicv2` node -- reads only the two address
|
||||
* cells, ignoring the size pair that follows.
|
||||
*/
|
||||
static uint64_t be64_addr_cell_pair(const unsigned char *b) {
|
||||
return ((uint64_t) be32_cell(b) << 32) | (uint64_t) be32_cell(b + 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Try to discover the real PLIC base address from the DTB.
|
||||
*
|
||||
* Looks up the `"sifive,plic-1.0.0"` node and reads its `reg` property's
|
||||
* first (address) cell pair. Returns 0 (leaving @p base_out untouched) if
|
||||
* @p dtb is not a valid FDT, no matching node exists, or `reg` is shorter
|
||||
* than one address-cell pair -- the caller keeps the QEMU constant default
|
||||
* in that case, exactly as before this function existed.
|
||||
*
|
||||
* @param dtb Candidate devicetree blob (@c BootInfo->dtb); NULL-safe.
|
||||
* @param base_out Receives the PLIC base address on success.
|
||||
* @return 1 on success, 0 if discovery failed for any reason.
|
||||
*/
|
||||
static int plic_base_from_dtb(const void *dtb, uintptr_t *base_out) {
|
||||
const void *node;
|
||||
const unsigned char *reg;
|
||||
uint32_t len;
|
||||
|
||||
if (!fdt_valid(dtb)) return 0;
|
||||
|
||||
node = fdt_find_node_by_compatible(dtb, "sifive,plic-1.0.0");
|
||||
if (!node) return 0;
|
||||
|
||||
reg = (const unsigned char *) fdt_find_prop_in_node(dtb, node, "reg", &len);
|
||||
if (!reg || len < 8u) return 0;
|
||||
|
||||
*base_out = (uintptr_t) be64_addr_cell_pair(reg);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int plic_init(const void *dtb) {
|
||||
if (plic_base_from_dtb(dtb, &s_plic_base)) {
|
||||
console_puts("PLIC: base discovered from DTB (0x");
|
||||
for (int s = 60; s >= 0; s -= 4)
|
||||
console_putc("0123456789abcdef"[(s_plic_base >> s) & 0xF]);
|
||||
console_println(")");
|
||||
} else {
|
||||
console_println("PLIC: no DTB PLIC node -- using QEMU virt-machine default (base=0x0c000000)");
|
||||
}
|
||||
|
||||
/* Threshold 0 = maximally permissive. Safe: nothing is enabled at any
|
||||
* source yet, so nothing can actually reach claim() until a caller
|
||||
* explicitly enables it. */
|
||||
*reg32(PLIC_THRESHOLD(PLIC_CONTEXT_S)) = 0;
|
||||
|
||||
__asm__ volatile ("csrs sie, %0" :: "r"(1UL << 9) : "memory"); /* SEIE */
|
||||
|
||||
console_println("PLIC: init (S-mode context 1, threshold=0)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plic_set_priority(uint32_t irq, uint32_t priority) {
|
||||
*reg32(PLIC_PRIORITY(irq)) = priority;
|
||||
}
|
||||
|
||||
void plic_enable(uint32_t irq) {
|
||||
volatile uint32_t *reg = reg32(PLIC_ENABLE(PLIC_CONTEXT_S) + 4u * (irq / 32u));
|
||||
*reg |= (1u << (irq % 32u));
|
||||
}
|
||||
|
||||
void plic_disable(uint32_t irq) {
|
||||
volatile uint32_t *reg = reg32(PLIC_ENABLE(PLIC_CONTEXT_S) + 4u * (irq / 32u));
|
||||
*reg &= ~(1u << (irq % 32u));
|
||||
}
|
||||
|
||||
uint32_t plic_claim(void) {
|
||||
return *reg32(PLIC_CLAIM(PLIC_CONTEXT_S));
|
||||
}
|
||||
|
||||
void plic_complete(uint32_t irq) {
|
||||
*reg32(PLIC_CLAIM(PLIC_CONTEXT_S)) = irq;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* switch.S (riscv64) - Cooperative VM context switch primitive.
|
||||
*
|
||||
* FABRIC-3.md §XXVIII (Stage 2, preemptive-context-switch save/restore
|
||||
* primitive, 2026-09-13). Deliberately NOT isr.S's interrupt trap frame
|
||||
* (Stage 0) -- that saves everything because an async trap has no call
|
||||
* site. sk_vm_switch_to() is an ordinary `call`, so the LP64D psABI
|
||||
* already makes every caller-saved register (ra excepted -- see below --
|
||||
* t0-t6, a0-a7, ft0-ft11, fa0-fa7) the caller's own problem across the
|
||||
* call. Only the callee-saved set needs explicit handling: s0-s11,
|
||||
* fs0-fs11, and ra itself (ra is not "callee-saved" in the usual sense,
|
||||
* but this function does not really return to its own caller -- it swaps
|
||||
* stacks and later resumes as if returning from whichever earlier call
|
||||
* parked this context, so ra must be captured explicitly here rather than
|
||||
* left to a normal prologue/epilogue).
|
||||
*
|
||||
* The FP half is saved only when sstatus.FS != Off -- same reasoning and
|
||||
* bit test as isr.S's trap frame, checked once per call and reused for
|
||||
* both the save and restore decision (nothing changes FS in between; it
|
||||
* is genuine single global CPU state, not part of what's "switched," so
|
||||
* this function reads it but never writes it).
|
||||
*
|
||||
* void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
|
||||
* a0 = save_sp_out, a1 = new_sp
|
||||
*
|
||||
* For a VM that has never run before, new_sp must point at a synthetic
|
||||
* 208-byte frame built by the C glue (switch.c) with ra set to the
|
||||
* trampoline entry address and everything else zeroed, so the final
|
||||
* `ret` below (jalr via ra) jumps there.
|
||||
*/
|
||||
|
||||
.section .text
|
||||
.balign 4
|
||||
|
||||
.equ SW_S0, 0
|
||||
.equ SW_RA, 96
|
||||
.equ SW_FS0, 104
|
||||
.equ SW_SIZE, 208
|
||||
|
||||
.equ SSTATUS_FS_SHIFT, 13
|
||||
.equ SSTATUS_FS_MASK, 3
|
||||
|
||||
.hidden sk_vm_switch_to
|
||||
|
||||
.global sk_vm_switch_to
|
||||
sk_vm_switch_to:
|
||||
addi sp, sp, -SW_SIZE
|
||||
|
||||
sd s0, SW_S0+0(sp)
|
||||
sd s1, SW_S0+8(sp)
|
||||
sd s2, SW_S0+16(sp)
|
||||
sd s3, SW_S0+24(sp)
|
||||
sd s4, SW_S0+32(sp)
|
||||
sd s5, SW_S0+40(sp)
|
||||
sd s6, SW_S0+48(sp)
|
||||
sd s7, SW_S0+56(sp)
|
||||
sd s8, SW_S0+64(sp)
|
||||
sd s9, SW_S0+72(sp)
|
||||
sd s10, SW_S0+80(sp)
|
||||
sd s11, SW_S0+88(sp)
|
||||
sd ra, SW_RA(sp)
|
||||
|
||||
csrr t0, sstatus
|
||||
srli t1, t0, SSTATUS_FS_SHIFT
|
||||
andi t1, t1, SSTATUS_FS_MASK
|
||||
beqz t1, 1f
|
||||
|
||||
fsd fs0, SW_FS0+0(sp)
|
||||
fsd fs1, SW_FS0+8(sp)
|
||||
fsd fs2, SW_FS0+16(sp)
|
||||
fsd fs3, SW_FS0+24(sp)
|
||||
fsd fs4, SW_FS0+32(sp)
|
||||
fsd fs5, SW_FS0+40(sp)
|
||||
fsd fs6, SW_FS0+48(sp)
|
||||
fsd fs7, SW_FS0+56(sp)
|
||||
fsd fs8, SW_FS0+64(sp)
|
||||
fsd fs9, SW_FS0+72(sp)
|
||||
fsd fs10, SW_FS0+80(sp)
|
||||
fsd fs11, SW_FS0+88(sp)
|
||||
1:
|
||||
sd sp, 0(a0) /* *save_sp_out = sp (this context's parked SP) */
|
||||
mv sp, a1 /* sp = new_sp (switch stacks) */
|
||||
|
||||
ld s0, SW_S0+0(sp)
|
||||
ld s1, SW_S0+8(sp)
|
||||
ld s2, SW_S0+16(sp)
|
||||
ld s3, SW_S0+24(sp)
|
||||
ld s4, SW_S0+32(sp)
|
||||
ld s5, SW_S0+40(sp)
|
||||
ld s6, SW_S0+48(sp)
|
||||
ld s7, SW_S0+56(sp)
|
||||
ld s8, SW_S0+64(sp)
|
||||
ld s9, SW_S0+72(sp)
|
||||
ld s10, SW_S0+80(sp)
|
||||
ld s11, SW_S0+88(sp)
|
||||
ld ra, SW_RA(sp)
|
||||
|
||||
beqz t1, 2f
|
||||
|
||||
fld fs0, SW_FS0+0(sp)
|
||||
fld fs1, SW_FS0+8(sp)
|
||||
fld fs2, SW_FS0+16(sp)
|
||||
fld fs3, SW_FS0+24(sp)
|
||||
fld fs4, SW_FS0+32(sp)
|
||||
fld fs5, SW_FS0+40(sp)
|
||||
fld fs6, SW_FS0+48(sp)
|
||||
fld fs7, SW_FS0+56(sp)
|
||||
fld fs8, SW_FS0+64(sp)
|
||||
fld fs9, SW_FS0+72(sp)
|
||||
fld fs10, SW_FS0+80(sp)
|
||||
fld fs11, SW_FS0+88(sp)
|
||||
2:
|
||||
addi sp, sp, SW_SIZE
|
||||
ret
|
||||
|
||||
/*
|
||||
* uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
|
||||
*
|
||||
* Builds the synthetic frame above at the top of a fresh native stack, so
|
||||
* the first ever sk_vm_switch_to() into it lands at `entry` (ra) via its
|
||||
* final `ret`. Kept in this file (not C) so the layout can never drift
|
||||
* from sk_vm_switch_to() above -- one set of offsets, not two. FP slots
|
||||
* are zeroed via plain integer stores (sd, not fsd) -- this is just
|
||||
* writing zero bytes to memory, not an FP computation, so it needs no
|
||||
* FS-enabled check at all, unlike every other f-register touch in this
|
||||
* file.
|
||||
*/
|
||||
.hidden sk_vm_switch_prime
|
||||
|
||||
.global sk_vm_switch_prime
|
||||
sk_vm_switch_prime:
|
||||
addi t0, a0, -SW_SIZE
|
||||
|
||||
sd zero, SW_S0+0(t0)
|
||||
sd zero, SW_S0+8(t0)
|
||||
sd zero, SW_S0+16(t0)
|
||||
sd zero, SW_S0+24(t0)
|
||||
sd zero, SW_S0+32(t0)
|
||||
sd zero, SW_S0+40(t0)
|
||||
sd zero, SW_S0+48(t0)
|
||||
sd zero, SW_S0+56(t0)
|
||||
sd zero, SW_S0+64(t0)
|
||||
sd zero, SW_S0+72(t0)
|
||||
sd zero, SW_S0+80(t0)
|
||||
sd zero, SW_S0+88(t0)
|
||||
sd a1, SW_RA(t0)
|
||||
|
||||
sd zero, SW_FS0+0(t0)
|
||||
sd zero, SW_FS0+8(t0)
|
||||
sd zero, SW_FS0+16(t0)
|
||||
sd zero, SW_FS0+24(t0)
|
||||
sd zero, SW_FS0+32(t0)
|
||||
sd zero, SW_FS0+40(t0)
|
||||
sd zero, SW_FS0+48(t0)
|
||||
sd zero, SW_FS0+56(t0)
|
||||
sd zero, SW_FS0+64(t0)
|
||||
sd zero, SW_FS0+72(t0)
|
||||
sd zero, SW_FS0+80(t0)
|
||||
sd zero, SW_FS0+88(t0)
|
||||
|
||||
mv a0, t0
|
||||
ret
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
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 <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user