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,17 @@
|
||||
# src/starkernel/arch/aarch64/
|
||||
|
||||
AArch64 architecture support for LithosAnanke.
|
||||
|
||||
- `arch.c` — architecture init entry point.
|
||||
- `apic.c` — GIC (Generic Interrupt Controller) stub, named `apic.c` to
|
||||
match the shared per-arch file-naming convention across
|
||||
amd64/aarch64/riscv64. Satisfies the common `apic_init()`/`apic_eoi()`
|
||||
call sites without real GIC bring-up yet; deferred to a later milestone.
|
||||
- `timer.c` — architected timer setup (heartbeat tick source).
|
||||
- `interrupts.c` — exception vector table setup and interrupt dispatch.
|
||||
- `boot.S` — early boot assembly.
|
||||
- `isr.S` — interrupt service routine stubs.
|
||||
|
||||
No `kernel_entry.S` — aarch64 uses a different UEFI loader entry path
|
||||
(`clang`/`lld-link` COFF target, since GCC cannot produce an aarch64 PE
|
||||
image; see `Makefile.starkernel`).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
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 (aarch64) - Architecture abstractions for StarKernel on AArch64
|
||||
*/
|
||||
|
||||
#include "arch.h"
|
||||
#include "console.h"
|
||||
#include "starkernel/fdt.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* Installed by isr.S — installs VBAR_EL2 at EL2, VBAR_EL1 at EL1 (EL-aware
|
||||
* since before this comment was corrected, 2026-09-04 — see
|
||||
* interrupts.c's arch_interrupts_init() doc comment for the fuller
|
||||
* correction; this one-word staleness was caught in passing while fixing
|
||||
* that one). */
|
||||
extern void aarch64_install_vectors(void);
|
||||
|
||||
/* -1 = not yet read. Cached rather than re-read on every call: CurrentEL
|
||||
* cannot change during the kernel's lifetime post-ExitBootServices, and a
|
||||
* cached value gives every consumer (vector install, GIC timer register
|
||||
* selection) the same answer without repeating the MRS. */
|
||||
static int s_current_el = -1;
|
||||
|
||||
/**
|
||||
* @brief Return the AArch64 exception level the kernel is running at.
|
||||
*
|
||||
* Reads @c CurrentEL (bits [3:2] hold the EL number, [1:0] are RES0) on
|
||||
* first call and caches the result; every later call returns the cached
|
||||
* value. Safe to call at any point in boot, including before
|
||||
* @c console_init() — the read itself has no dependency, only the
|
||||
* printing of it does (see @c arch_interrupts_init() in @c interrupts.c,
|
||||
* the earliest point at which both this and a working console are
|
||||
* available).
|
||||
*
|
||||
* EDK2 on QEMU's aarch64 @c virt machine has been observed to leave the
|
||||
* kernel at either EL1 or EL2 depending on firmware build; nothing in this
|
||||
* tree may assume one over the other (FABRIC-0.md §25.7.1 GAP-B3). Every
|
||||
* EL-dependent choice — @c VBAR_EL1 vs @c VBAR_EL2, the @c ELR_ELx /
|
||||
* @c SPSR_ELx saved-state pair, and @c CNTP_*_EL0 vs @c CNTHP_*_EL2 — must
|
||||
* read this accessor rather than hardcode a level.
|
||||
*
|
||||
* @return 1 or 2. Never 0 or 3: a UEFI application that survived far enough
|
||||
* to reach this call cannot be running at EL0 (unprivileged) or EL3
|
||||
* (secure monitor — QEMU's virt firmware runs the kernel at EL1/EL2).
|
||||
*/
|
||||
int aarch64_current_el(void)
|
||||
{
|
||||
if (s_current_el < 0) {
|
||||
uint64_t current_el;
|
||||
__asm__ volatile ("mrs %0, CurrentEL" : "=r"(current_el));
|
||||
s_current_el = (int)((current_el >> 2) & 0x3);
|
||||
}
|
||||
return s_current_el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform the earliest AArch64 architecture initialisation (no-op).
|
||||
*
|
||||
* On x86-64 this function loads the GDT; AArch64 has no equivalent because
|
||||
* privilege separation is provided by the exception-level architecture
|
||||
* (EL0/EL1/EL2/EL3) rather than segment descriptors. The exception vector
|
||||
* table is installed later in @c arch_interrupts_init() by calling
|
||||
* @c aarch64_install_vectors() which writes @c VBAR_EL1.
|
||||
*
|
||||
* This stub exists to satisfy the common @c arch_early_init() call site in
|
||||
* @c kernel_main() so that the same milestone sequence compiles across all
|
||||
* three supported ISAs (amd64 / aarch64 / riscv64).
|
||||
*/
|
||||
void arch_early_init(void)
|
||||
{
|
||||
/* No GDT on AArch64; privilege separation via exception levels.
|
||||
* Interrupt vectors installed later in arch_interrupts_init(). */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enable IRQ delivery on the current core (AArch64 DAIFCLR).
|
||||
*
|
||||
* Issues @c MSR DAIFCLR, #2 which clears the @c I bit (IRQ mask) in the
|
||||
* @c DAIF (Debug / Abort / IRQ / FIQ) flags of the current exception level.
|
||||
* After this instruction the core will take IRQ exceptions from the GIC.
|
||||
* The @c memory clobber prevents the compiler from reordering stores across
|
||||
* the barrier.
|
||||
*
|
||||
* Must be called after @c arch_interrupts_init() has installed the vector
|
||||
* table in @c VBAR_EL1 and the GIC timer driver has been configured.
|
||||
*/
|
||||
void arch_enable_interrupts(void)
|
||||
{
|
||||
__asm__ volatile ("msr daifclr, #2" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Disable IRQ delivery on the current core (AArch64 DAIFSET).
|
||||
*
|
||||
* Issues @c MSR DAIFSET, #2 which sets the @c I bit (IRQ mask) in the
|
||||
* @c DAIF flags, masking IRQ exceptions. FIQ, SError, and Debug exceptions
|
||||
* are unaffected. Used around critical sections in the single-threaded
|
||||
* kernel where interrupt delivery would corrupt shared state.
|
||||
*
|
||||
* The @c memory clobber acts as a compiler-level barrier; for a full
|
||||
* hardware memory barrier use @c DSB before calling this function.
|
||||
*/
|
||||
void arch_disable_interrupts(void)
|
||||
{
|
||||
__asm__ volatile ("msr daifset, #2" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Halt the core until the next event (AArch64 WFI).
|
||||
*
|
||||
* Issues the @c WFI (Wait For Interrupt) instruction. The core enters a
|
||||
* low-power standby state and resumes execution when an IRQ, FIQ, SError,
|
||||
* debug event, or reset is detected. In the kernel panic path this is
|
||||
* called inside an infinite loop with interrupts disabled (@c DAIFSET)
|
||||
* to stop the processor permanently. In the idle path it reduces power
|
||||
* consumption between heartbeat ticks.
|
||||
*
|
||||
* The @c memory clobber prevents the compiler from sinking stores across
|
||||
* the @c WFI.
|
||||
*/
|
||||
void arch_halt(void)
|
||||
{
|
||||
__asm__ volatile ("wfi" ::: "memory");
|
||||
}
|
||||
|
||||
/* PSCI conduit selection (FABRIC-3.md §IV.3 item 7 fix, 2026-09-04).
|
||||
* Defaults to 0 (HVC) so a boot with no DTB -- this system's QEMU/UEFI
|
||||
* firmware, same `fdt_valid()` failure mode already documented in
|
||||
* apic.c's file header for GIC discovery -- keeps arch_cold_reset()'s
|
||||
* existing, working HVC behaviour unchanged. Set to 1 (SMC) by
|
||||
* aarch64_psci_conduit_init() below when the real DTB's own `/psci` node
|
||||
* says so. */
|
||||
static int s_psci_use_smc = 0;
|
||||
|
||||
/**
|
||||
* @brief Discover the real PSCI conduit ("smc" vs "hvc") from the DTB.
|
||||
*
|
||||
* Looks up the devicetree's PSCI firmware node and reads its `method`
|
||||
* property. BCM2712's own `/psci` node declares
|
||||
* `compatible = "arm,psci-1.0", "arm,psci-0.2", "arm,psci"` and
|
||||
* `method = "smc"` -- confirmed directly against `bcm2712.dtsi`, not
|
||||
* assumed. `fdt_find_node_by_compatible()` matches any entry in a node's
|
||||
* compatible list, so searching for just the first (most specific) string
|
||||
* is sufficient; the other two exist for older PSCI-spec-version
|
||||
* consumers, not because this node might be tagged differently.
|
||||
*
|
||||
* Leaves @c s_psci_use_smc at its default (0, HVC) if @p dtb is not a
|
||||
* valid FDT, no PSCI node is found, or `method` is anything other than
|
||||
* exactly `"smc"` -- including the explicit `"hvc"` case, and the
|
||||
* "property absent" case, both of which should keep the existing HVC
|
||||
* behaviour rather than guess.
|
||||
*
|
||||
* Called once from @c apic_init() (`arch/aarch64/apic.c`), the one point
|
||||
* in boot that already has @c boot_info->dtb in hand at the right time
|
||||
* for this kind of discovery (same rationale as that file's own GIC-base
|
||||
* lookup) -- collocated here in @c arch.c, not there, because
|
||||
* @c arch_cold_reset() is this function's actual consumer and has no
|
||||
* @c boot_info of its own to probe with by the time it runs (called from
|
||||
* deep in VM execution, via `BYE`). Same extern-in-place convention this
|
||||
* file and apic.c already use for @c aarch64_current_el().
|
||||
*
|
||||
* @param dtb Candidate devicetree blob (@c BootInfo->dtb); NULL-safe.
|
||||
*/
|
||||
void aarch64_psci_conduit_init(const void *dtb)
|
||||
{
|
||||
const void *node;
|
||||
const char *method;
|
||||
uint32_t len;
|
||||
|
||||
if (!fdt_valid(dtb)) {
|
||||
console_println("PSCI: no DTB -- using HVC (QEMU virt-machine default)");
|
||||
return;
|
||||
}
|
||||
|
||||
node = fdt_find_node_by_compatible(dtb, "arm,psci-1.0");
|
||||
if (!node) {
|
||||
console_println("PSCI: no DTB PSCI node -- using HVC (QEMU virt-machine default)");
|
||||
return;
|
||||
}
|
||||
|
||||
method = (const char *) fdt_find_prop_in_node(dtb, node, "method", &len);
|
||||
if (method && len >= 3 && method[0] == 's' && method[1] == 'm' && method[2] == 'c') {
|
||||
s_psci_use_smc = 1;
|
||||
console_println("PSCI: conduit discovered from DTB (method=\"smc\")");
|
||||
} else {
|
||||
console_println("PSCI: DTB method != \"smc\" -- using HVC");
|
||||
}
|
||||
}
|
||||
|
||||
void arch_cold_reset(void)
|
||||
{
|
||||
arch_disable_interrupts();
|
||||
/* PSCI SYSTEM_RESET (function 0x84000009). SYSTEM_RESET takes no
|
||||
* arguments and has no SMC64 variant defined by the PSCI spec -- only
|
||||
* the SMC32 encoding is valid (fixed 2026-08-18, was 0xC4000009).
|
||||
*
|
||||
* Conduit is @c s_psci_use_smc, set by @c aarch64_psci_conduit_init()
|
||||
* above from the real DTB when one is present (real Pi 5 hardware's
|
||||
* own `/psci` node declares `method = "smc"`, confirmed against
|
||||
* `bcm2712.dtsi`).
|
||||
*
|
||||
* FABRIC-1.md Section I, 2026-08-18: live gdb tracing (using the real
|
||||
* UEFI-relocated runtime address, not the standalone kernel.elf's
|
||||
* link-time address -- see that section for why those differ) proved
|
||||
* the SMC call itself traps on *this system's* QEMU aarch64 boot
|
||||
* (AAVMF UEFI firmware, no genuine EL3/TrustZone secure monitor, no
|
||||
* DTB forwarded to the guest either): PC does not fall through to the
|
||||
* wfi loop below, it jumps straight into this kernel's own exception
|
||||
* vector. PSCI there is served via HVC (hypervisor call, EL2)
|
||||
* instead -- exactly the @c s_psci_use_smc == 0 default case. The
|
||||
* function ID and calling convention are unchanged between conduits,
|
||||
* only the trapping instruction differs. */
|
||||
if (s_psci_use_smc) {
|
||||
__asm__ volatile (
|
||||
"mov x0, #0x84000000\n"
|
||||
"movk x0, #0x0009\n"
|
||||
"smc #0\n"
|
||||
::: "x0", "memory"
|
||||
);
|
||||
} else {
|
||||
__asm__ volatile (
|
||||
"mov x0, #0x84000000\n"
|
||||
"movk x0, #0x0009\n"
|
||||
"hvc #0\n"
|
||||
::: "x0", "memory"
|
||||
);
|
||||
}
|
||||
for (;;) __asm__ volatile ("wfi" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the AArch64 system counter (CNTPCT_EL0).
|
||||
*
|
||||
* Issues @c MRS @c %0, @c cntpct_el0 to read the 64-bit physical system
|
||||
* counter. On AArch64 the system counter is the architectural equivalent
|
||||
* of the x86-64 TSC: a monotonically incrementing clock whose frequency
|
||||
* is reported by @c CNTFRQ_EL0 and set by firmware at boot.
|
||||
*
|
||||
* Unlike @c RDTSC on x86-64, @c CNTPCT_EL0 is fully synchronised across
|
||||
* cores (no per-core skew), invariant across power-state changes, and
|
||||
* does not require serialisation for most measurement use cases. It is
|
||||
* used as the time base for @c timer_now_ns() and @c heartbeat_tick().
|
||||
*
|
||||
* @return Current 64-bit system counter value in counter ticks
|
||||
* (frequency given by @c CNTFRQ_EL0, typically 10–62.5 MHz).
|
||||
*/
|
||||
uint64_t arch_read_timestamp(void)
|
||||
{
|
||||
uint64_t val;
|
||||
__asm__ volatile ("mrs %0, cntpct_el0" : "=r"(val));
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialise the AArch64 MMU (stub — deferred to a later milestone).
|
||||
*
|
||||
* Full page-table setup (TTBR0/TTBR1, MAIR, TCR, SCTLR.M) is deferred to a
|
||||
* later milestone. This stub exists to satisfy the common @c arch_mmu_init()
|
||||
* call site shared across all three ISAs. The VMM subsystem (@c vmm.c)
|
||||
* handles page-table management independently of this hook.
|
||||
*/
|
||||
void arch_mmu_init(void)
|
||||
{
|
||||
/* Stub: MMU bring-up deferred for later milestone */
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* boot.S (aarch64) - Entry stub and linker-script symbol substitutes.
|
||||
*
|
||||
* With lld-link (PE/COFF), GNU ld linker-script symbols are unavailable.
|
||||
* Define __text_start, __text_end, __kernel_start, __kernel_end here 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).
|
||||
*
|
||||
* The .reloc section (PE base-relocation table) is generated automatically
|
||||
* by lld-link from the COFF relocations in the object files.
|
||||
*/
|
||||
.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,223 @@
|
||||
/*
|
||||
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 (aarch64) - Exception vector setup and common handler
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "arch.h"
|
||||
#include "console.h"
|
||||
#include "starkernel/timer.h"
|
||||
|
||||
volatile const char *g_sk_fault_word = (void *)0;
|
||||
|
||||
/* Defined in isr.S */
|
||||
extern void aarch64_install_vectors(void);
|
||||
|
||||
/* Defined in arch.c. Extern-declared here rather than in the shared arch.h:
|
||||
* "exception level" has no amd64/riscv64 equivalent, so it does not belong
|
||||
* in the cross-ISA header — same convention as riscv64_timer_rearm() in
|
||||
* arch/riscv64/interrupts.c. */
|
||||
extern int aarch64_current_el(void);
|
||||
|
||||
/* Defined in apic.c (items 0.6/0.7). Same extern-in-place convention: GICv2
|
||||
* and generic-timer register-level details belong to the file that owns
|
||||
* the MMIO base addresses / EL-aware register selection, not the shared
|
||||
* apic.h. */
|
||||
extern uint32_t apic_read_iar(void);
|
||||
extern uint32_t apic_timer_ppi(void);
|
||||
extern void apic_eoi_intid(uint32_t intid);
|
||||
extern void apic_timer_rearm(void);
|
||||
|
||||
/* Defined in virtio_input.c (item 4.3.5e). g_virtio_input_gic_intid is 0
|
||||
* (not a valid SPI INTID -- SPIs start at 32) until
|
||||
* virtio_input_find_keyboard() finds a real device and computes its routed
|
||||
* INTID, so this branch is inert on any boot where the device isn't
|
||||
* present. Same extern-in-place convention as the rest of this file. */
|
||||
extern uint32_t g_virtio_input_gic_intid;
|
||||
extern void virtio_input_isr(void);
|
||||
|
||||
/* INTID 1023 = "spurious" (GICv2 spec): the CPU interface has nothing
|
||||
* pending, typically because another interrupt at the same or higher
|
||||
* priority raced this one to acknowledgement. Architecturally defined,
|
||||
* not this board's invention, so it is named rather than left as a magic
|
||||
* number in the comparison below. */
|
||||
#define GIC_INTID_SPURIOUS 1023u
|
||||
|
||||
/**
|
||||
* @brief Dispatch an IRQ taken at current-EL with SPx (punch-list item 0.7).
|
||||
*
|
||||
* Called from @c irq_spx_trampoline in @c isr.S, which has already saved the
|
||||
* full caller-saved integer and FP/SIMD register sets plus @c ELR_ELx /
|
||||
* @c SPSR_ELx, and will restore all of it and @c ERET after this function
|
||||
* returns — unlike @c aarch64_exception_handler(), this path is designed to
|
||||
* return normally.
|
||||
*
|
||||
* Reads @c GICC_IAR once (the GICv2 acknowledgement step — this also moves
|
||||
* the interrupt to the active state), dispatches on the returned INTID, and
|
||||
* always completes with the matching @c GICC_EOIR write, spurious or not:
|
||||
* a spurious read must still not be treated as "nothing to clean up," and
|
||||
* an unrecognised real INTID still needs the CPU interface unblocked for
|
||||
* the next one.
|
||||
*
|
||||
* For the timer PPI (@c apic_timer_ppi() — 30 at EL1, 26 at EL2, decided
|
||||
* once in @c apic_init()): @c apic_timer_rearm() runs **first**, before
|
||||
* @c heartbeat_tick() — the ARM Generic Timer does not auto-reload, so
|
||||
* skipping this leaves the interrupt condition permanently latched (see
|
||||
* @c apic_timer_rearm()'s own doc comment). Ordering the re-arm ahead of
|
||||
* the rest of the dispatch means a fault in @c heartbeat_tick() cannot
|
||||
* also cost the next tick — same discipline as @c riscv64_timer_rearm()
|
||||
* (item 0.3).
|
||||
*
|
||||
* virtio-input (item 4.3.5e, @c g_virtio_input_gic_intid): dispatched
|
||||
* **before** the @c GICC_EOIR write below, not after — the ISR itself is
|
||||
* what deasserts the device's (level-triggered) interrupt line by reading
|
||||
* its ISR-status capability; EOI'ing first would let the GIC immediately
|
||||
* re-signal a still-asserted line. Same claim-dispatch-complete ordering
|
||||
* @c riscv64_interrupt_handler() uses for its own PLIC-routed devices.
|
||||
*/
|
||||
void aarch64_irq_handler(void)
|
||||
{
|
||||
uint32_t intid = apic_read_iar();
|
||||
|
||||
if (intid == apic_timer_ppi()) {
|
||||
apic_timer_rearm();
|
||||
heartbeat_tick();
|
||||
}
|
||||
|
||||
if (intid == g_virtio_input_gic_intid) {
|
||||
virtio_input_isr();
|
||||
}
|
||||
|
||||
if (intid != GIC_INTID_SPURIOUS) {
|
||||
apic_eoi_intid(intid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) then emits it via
|
||||
* @c console_puts(). Used by @c aarch64_exception_handler() to print
|
||||
* @c ESR_EL1, @c ELR_EL1, @c FAR_EL1, and @c SPSR_EL1 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 AArch64 common exception handler — print diagnostic and spin.
|
||||
*
|
||||
* Called from the @c isr.S exception vector table stubs for all exception
|
||||
* types (synchronous, IRQ, FIQ, SError) that are not handled by a specific
|
||||
* driver. Reads the four key EL1 exception system registers directly:
|
||||
*
|
||||
* - @c ESR_EL1 — Exception Syndrome Register: encodes the exception class
|
||||
* (EC, bits [31:26]) and instruction-specific syndrome (ISS, bits [24:0]).
|
||||
* The EC identifies the cause (e.g., 0x21 = data abort from EL1, 0x25 =
|
||||
* data abort from EL0, 0x15 = SVC64).
|
||||
* - @c ELR_EL1 — Exception Link Register: the PC of the instruction that
|
||||
* caused the exception (or the instruction after, for IRQ/FIQ/SError).
|
||||
* - @c FAR_EL1 — Fault Address Register: the virtual address that triggered
|
||||
* a Data Abort or Instruction Abort; undefined for other exception types.
|
||||
* - @c SPSR_EL1 — Saved Program Status Register: the PSTATE at the time of
|
||||
* exception, including the mode field and interrupt-mask bits.
|
||||
*
|
||||
* Prints all four values to the kernel console then enters an infinite
|
||||
* @c WFE loop to halt the affected core permanently. All exceptions that
|
||||
* reach this handler are fatal — there is no recovery path.
|
||||
*
|
||||
* The global @c g_sk_fault_word is set by the VM FORTH dispatcher just
|
||||
* before calling each word's function pointer; it names the word executing
|
||||
* at the time of the fault, aiding post-mortem analysis from the serial log.
|
||||
* (Currently logged only on the amd64 #GP path; future AArch64 diagnostic
|
||||
* expansion can include it here.)
|
||||
*/
|
||||
void aarch64_exception_handler(void)
|
||||
{
|
||||
uint64_t esr, elr, far_val, spsr;
|
||||
__asm__ volatile ("mrs %0, esr_el1" : "=r"(esr));
|
||||
__asm__ volatile ("mrs %0, elr_el1" : "=r"(elr));
|
||||
__asm__ volatile ("mrs %0, far_el1" : "=r"(far_val));
|
||||
__asm__ volatile ("mrs %0, spsr_el1" : "=r"(spsr));
|
||||
|
||||
console_println("*** EXCEPTION (aarch64) ***");
|
||||
console_puts(" ESR_EL1 = "); print_hex(esr); console_puts("\n");
|
||||
console_puts(" ELR_EL1 = "); print_hex(elr); console_puts("\n");
|
||||
console_puts(" FAR_EL1 = "); print_hex(far_val); console_puts("\n");
|
||||
console_puts(" SPSR_EL1= "); print_hex(spsr); console_puts("\n");
|
||||
|
||||
for (;;) {
|
||||
__asm__ volatile ("wfe" ::: "memory");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Install the AArch64 exception vector table (M4 milestone).
|
||||
*
|
||||
* First reads and reports the exception level via @c aarch64_current_el()
|
||||
* (punch-list item 0.4) — this is the earliest point in boot with both a
|
||||
* working console (up since M1) and a genuine need for the answer (vector
|
||||
* installation is the first EL-dependent operation). The read itself is
|
||||
* cheap and safe far earlier than this; only the console print is gated on
|
||||
* this call site.
|
||||
*
|
||||
* Then calls @c aarch64_install_vectors() (defined in @c isr.S) which
|
||||
* writes the address of the vector table base into @c VBAR_EL1 or
|
||||
* @c VBAR_EL2 (Vector Base Address Register), whichever matches the
|
||||
* exception level @c aarch64_current_el() just reported. After this
|
||||
* instruction all exceptions at that level (synchronous, IRQ, FIQ,
|
||||
* SError) dispatch through the 512-byte-aligned vector table assembled
|
||||
* in @c isr.S.
|
||||
*
|
||||
* **Correction, 2026-09-04 (FABRIC-3.md §IV.3 item 7's code audit):**
|
||||
* this doc comment and the runtime console message below previously
|
||||
* claimed EL2 was "a known gap... @c VBAR_EL1 is written unconditionally
|
||||
* regardless of the detected level," citing punch-list item 0.5 as the
|
||||
* still-open work to fix it. Checked against @c isr.S's own
|
||||
* @c aarch64_install_vectors() (the actual implementation, not this
|
||||
* comment) and found that claim stale: the assembly already branches on
|
||||
* @c aarch64_current_el() and writes @c vbar_el2 at EL2 /
|
||||
* @c vbar_el1 at EL1, threading the same answer through
|
||||
* @c el2_mode_flag for the IRQ trampoline's @c ELR_ELx/@c SPSR_ELx
|
||||
* selection too (see @c isr.S's own "EL selection" header comment).
|
||||
* There never was a gap here to close — item 0.5's EL2 vector work was
|
||||
* already done by the time this comment was written describing it as
|
||||
* open; only the comment was wrong, not the code.
|
||||
*
|
||||
* On AArch64 there is no IDT and no PIC to disable; the GIC replaces
|
||||
* both. The @c arch_interrupts_init() name is kept identical across all
|
||||
* ISAs so that @c kernel_main() can call the same symbol regardless of
|
||||
* the target architecture.
|
||||
*
|
||||
* Must be called after the kernel stack is established (so that the
|
||||
* correct-EL SP is valid) and before @c arch_enable_interrupts().
|
||||
*/
|
||||
void arch_interrupts_init(void)
|
||||
{
|
||||
int el = aarch64_current_el();
|
||||
console_puts("AArch64: running at EL");
|
||||
console_putc((char)('0' + el));
|
||||
console_println(el == 2
|
||||
? " (VBAR_EL2/CNTHP_*_EL2 -- both already EL-aware, see isr.S/apic.c)"
|
||||
: "");
|
||||
|
||||
aarch64_install_vectors();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* native_rpi5_entry.S (aarch64) - Raspberry Pi 5 native (non-UEFI) entry
|
||||
* stub (FABRIC-3.md §IV.3 item 1, 2026-09-04).
|
||||
*
|
||||
* **Never run on real hardware** -- the Pi 5 isn't in hand until
|
||||
* 2026-09-17 (FABRIC-3.md §II). Compile-only-verified.
|
||||
*
|
||||
* Entered directly by Pi 5 firmware at the classic bare-metal load address
|
||||
* 0x80000 (item 6, `config.txt`'s `os_check=0`, is still open -- that's
|
||||
* what actually gets a real image loaded there; this file is just the
|
||||
* code that would run once it is). No UEFI, no ACPI -- none of
|
||||
* `boot/uefi_loader.c`'s PE-loader shape applies on this path. Per
|
||||
* FABRIC-3.md §IV.1's own researched entry protocol:
|
||||
*
|
||||
* x0 = 32-bit DTB pointer, upper 32 bits of the 64-bit register
|
||||
* UNSPECIFIED -- must mask before use.
|
||||
* x1-x3 = reserved / zero, unused here.
|
||||
*
|
||||
* This stub's own job: mask and capture that DTB pointer into
|
||||
* `g_rpi5_dtb_ptr` (declared in `include/starkernel/rpi5_native_entry.h`),
|
||||
* establish a dedicated stack -- this path has no EDK2 boot stack to
|
||||
* inherit, there is no EDK2 at all here -- then tail-call
|
||||
* `rpi5_native_boot()` (`rpi5_native_boot.c`, §IV.3 item 2), which builds
|
||||
* `BootInfo` from the devicetree and calls the existing `kernel_main()`.
|
||||
* `rpi5_native_boot()` never returns; the `wfe`/`b` loop after the `bl`
|
||||
* exists only so this file's own control flow has somewhere defined to
|
||||
* go if that contract is ever violated.
|
||||
*
|
||||
* Build-system note: `kernel/Makefile`'s `KERNEL_ASM` wildcards every
|
||||
* `*.S` file in this directory into the kernel ELF build (not the PE/COFF
|
||||
* loader build -- that list is explicit, boot.S/isr.S/switch.S only), so
|
||||
* this file links into the ARCH=aarch64 QEMU/UEFI kernel ELF as dead
|
||||
* code, and is the entry point of the separately-linked Pi 5 native
|
||||
* image (`make boot_image TARGET=RASPI`):
|
||||
* `kernel/linker/starkernel-native-rpi5.ld` places `.text.entry` first
|
||||
* at 0x80000 and the image is flattened to `kernel_2712.img`.
|
||||
*
|
||||
* Pi 5 firmware loads only the file's bytes; `.bss` (NOLOAD) is whatever
|
||||
* was in RAM. It is zeroed here, before anything in it -- including
|
||||
* `g_rpi5_dtb_ptr` and this stub's own stack -- is used. x0 (the DTB
|
||||
* pointer) is preserved across the loop; only x4-x6 are touched.
|
||||
*/
|
||||
|
||||
.section .bss
|
||||
.align 8
|
||||
.global g_rpi5_dtb_ptr
|
||||
g_rpi5_dtb_ptr:
|
||||
.space 8 /* uint64_t -- masked DTB pointer */
|
||||
|
||||
.align 4
|
||||
g_rpi5_native_stack:
|
||||
.space 0x200000 /* 2 MiB, same convention as the
|
||||
* amd64/riscv64 kernel_entry.S
|
||||
* BSS stacks */
|
||||
g_rpi5_native_stack_top:
|
||||
|
||||
.section .text.entry, "ax"
|
||||
.global rpi5_native_start
|
||||
rpi5_native_start:
|
||||
/* Mask x0 to its documented 32-bit DTB-pointer range (§IV.1: the
|
||||
* upper 32 bits of the 64-bit register are unspecified by the
|
||||
* firmware's own entry protocol). */
|
||||
mov x1, #0xffffffff
|
||||
and x0, x0, x1
|
||||
|
||||
/* Zero .bss: [__bss_start, __bss_end), both 16-byte aligned by the
|
||||
* native linker script. */
|
||||
adrp x4, __bss_start
|
||||
add x4, x4, :lo12:__bss_start
|
||||
adrp x5, __bss_end
|
||||
add x5, x5, :lo12:__bss_end
|
||||
.Lbss_zero:
|
||||
cmp x4, x5
|
||||
b.hs .Lbss_done
|
||||
stp xzr, xzr, [x4], #16
|
||||
b .Lbss_zero
|
||||
.Lbss_done:
|
||||
|
||||
adrp x2, g_rpi5_dtb_ptr
|
||||
add x2, x2, :lo12:g_rpi5_dtb_ptr
|
||||
str x0, [x2]
|
||||
|
||||
/* Establish this path's own dedicated stack. */
|
||||
adrp x0, g_rpi5_native_stack_top
|
||||
add x0, x0, :lo12:g_rpi5_native_stack_top
|
||||
mov sp, x0
|
||||
|
||||
/* x0 was clobbered above for the stack-top address -- reload the
|
||||
* masked DTB pointer from g_rpi5_dtb_ptr before calling in. */
|
||||
adrp x0, g_rpi5_dtb_ptr
|
||||
add x0, x0, :lo12:g_rpi5_dtb_ptr
|
||||
ldr x0, [x0]
|
||||
bl rpi5_native_boot
|
||||
|
||||
.Lhalt:
|
||||
wfe
|
||||
b .Lhalt
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* rpi5_dtb.c - Raspberry Pi 5 (BCM2712) devicetree-based peripheral
|
||||
* discovery (FABRIC-3.md §IV.3, 2026-09-04).
|
||||
*
|
||||
* Confirmed directly against `bcm2712.dtsi` (raspberrypi/linux,
|
||||
* rpi-6.12.y) before writing this, not assumed:
|
||||
*
|
||||
* soc: soc@107c000000 {
|
||||
* compatible = "simple-bus";
|
||||
* ranges = <0x0 0x10 0x0 0x80000000>;
|
||||
* #address-cells = <1>;
|
||||
* #size-cells = <1>;
|
||||
* ...
|
||||
* uart10: serial@7d001000 {
|
||||
* compatible = "arm,pl011", "arm,primecell";
|
||||
* reg = <0x7d001000 0x200>;
|
||||
* };
|
||||
* mailbox: mailbox@7c013880 {
|
||||
* compatible = "brcm,bcm2835-mbox";
|
||||
* reg = <0x7c013880 0x40>;
|
||||
* };
|
||||
* };
|
||||
*
|
||||
* Every peripheral this file cares about lives under this one "soc"
|
||||
* node, which applies exactly one translation to every child `reg`
|
||||
* value: child address 0 maps to parent (CPU-physical) address
|
||||
* 0x10_0000_0000 (`ranges`'s own `<0x0 0x10 0x0 ...>` — child cell 0,
|
||||
* parent cells `0x10 0x0`, i.e. `0x10 << 32`). The child side's own
|
||||
* `#address-cells = <1>` confirms each `reg` value's first cell is the
|
||||
* whole child address (32-bit, no cell-splitting needed) before adding
|
||||
* the offset. `fdt.c`'s reader deliberately does not parse `ranges`
|
||||
* generally (it is "not a general devicetree library," per its own
|
||||
* header comment) — this one, fixed, SoC-wide offset is applied here
|
||||
* by name instead, since it is a single known hardware fact, not a
|
||||
* general mechanism this codebase needs elsewhere yet.
|
||||
*/
|
||||
|
||||
#include "starkernel/rpi5_dtb.h"
|
||||
#include "starkernel/fdt.h"
|
||||
|
||||
/* soc@107c000000's own `ranges` offset -- see this file's own doc
|
||||
* comment above for where it was confirmed. */
|
||||
#define BCM2712_SOC_RANGES_OFFSET 0x1000000000ULL
|
||||
|
||||
/**
|
||||
* @brief Read a devicetree `reg` property's first cell as big-endian.
|
||||
*
|
||||
* `fdt.c` keeps its own `be32()` helper file-local (freestanding, no
|
||||
* shared byte-swap utility to reuse) -- duplicated here rather than
|
||||
* exposing it, same "a few lines is simpler than a new shared
|
||||
* dependency" precedent `tools/pkcs8_ed25519.c` already set in this
|
||||
* codebase.
|
||||
*/
|
||||
static uint32_t reg_first_cell_be32(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 Find @p compatible's node, read its `reg` base address, and
|
||||
* apply the one fixed BCM2712 `soc`-node translation offset.
|
||||
*/
|
||||
static uint64_t rpi5_peripheral_base(const void* dtb, const char* compatible)
|
||||
{
|
||||
const void* node;
|
||||
const void* val;
|
||||
uint32_t len;
|
||||
|
||||
node = fdt_find_node_by_compatible(dtb, compatible);
|
||||
if (!node) return 0;
|
||||
|
||||
val = fdt_find_prop_in_node(dtb, node, "reg", &len);
|
||||
if (!val || len < 4) return 0;
|
||||
|
||||
return BCM2712_SOC_RANGES_OFFSET + reg_first_cell_be32((const unsigned char*) val);
|
||||
}
|
||||
|
||||
uint64_t rpi5_uart_base(const void* dtb)
|
||||
{
|
||||
return rpi5_peripheral_base(dtb, "arm,pl011");
|
||||
}
|
||||
|
||||
uint64_t rpi5_mailbox_base(const void* dtb)
|
||||
{
|
||||
return rpi5_peripheral_base(dtb, "brcm,bcm2835-mbox");
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James. All rights reserved.
|
||||
Licensed under the StarForth License, Version 1.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* rpi5_mailbox.c - VideoCore mailbox property-interface framebuffer
|
||||
* setup (FABRIC-3.md §IV.3 item 3, 2026-09-04).
|
||||
*
|
||||
* **Never run on real hardware** — the Pi 5 isn't in hand until
|
||||
* 2026-09-17 (FABRIC-3.md §II). Everything below is built from real,
|
||||
* cited sources rather than assumed, but is compile-only-verified.
|
||||
*
|
||||
* Register layout (offsets from `rpi5_mailbox_base()`, i.e. the
|
||||
* `mailbox` devicetree node's own translated base address —
|
||||
* `rpi5_dtb.c` already confirmed `0x107C013880` on real Pi 5
|
||||
* hardware, matching a Pi-5-specific bare-metal reference
|
||||
* (main.lv/writeup/raspberry5_baremetal_framebuffer.md) independently
|
||||
* confirming the same `0x107C000000` SoC base + `0x13880` mailbox
|
||||
* offset this codebase's own devicetree-driven lookup already
|
||||
* produces — two independent sources agreeing is why this offset is
|
||||
* trusted, not just cited once):
|
||||
*
|
||||
* +0x00 MBOX0_READ (receive: VC -> ARM)
|
||||
* +0x18 MBOX0_STATUS (bit30 MBOX_EMPTY: nothing to read)
|
||||
* +0x20 MBOX1_WRITE (send: ARM -> VC)
|
||||
* +0x38 MBOX1_STATUS (bit31 MBOX_FULL: cannot write yet)
|
||||
*
|
||||
* (`+0x20/+0x38` per the same Pi-5-specific reference above, which
|
||||
* treats MBOX0/MBOX1 as two structurally identical 0x20-byte blocks
|
||||
* back to back — a real difference from older-chip tutorials, which
|
||||
* often describe a single shared status register at `+0x18` for both
|
||||
* directions. Trusted here because it's the one source actually
|
||||
* confirmed against Pi 5 hardware, not an older SoC.)
|
||||
*
|
||||
* Message encoding (official wiki:
|
||||
* github.com/raspberrypi/firmware/wiki/Mailbox-property-interface,
|
||||
* channel and buffer format; property tag IDs/lengths from the same
|
||||
* page): write `(buffer_addr & ~0xF) | channel` to MBOX1_WRITE; read
|
||||
* back the same encoded value from MBOX0_READ once available, confirming
|
||||
* it before trusting the buffer (the response overwrites the request
|
||||
* in place — same shared buffer, not a separate response buffer). No
|
||||
* bus-address translation applied to the buffer pointer itself
|
||||
* (confirmed against the same Pi-5-specific reference: it writes the
|
||||
* plain masked pointer, no added GPU-bus-alias bit, unlike the
|
||||
* classic 32-bit-Pi convention some older tutorials describe) —
|
||||
* genuinely different per-SoC-generation behavior, not an oversight.
|
||||
*
|
||||
* Buffer/tag format (official wiki, same page): u32 total size, u32
|
||||
* request(0)/response(0x80000000|len) code, tags..., u32 end-tag(0).
|
||||
* Each tag: u32 id, u32 value-buffer size, u32 req-size/resp-flag,
|
||||
* value bytes padded to 4-byte alignment. Buffer itself must be
|
||||
* 16-byte aligned (only the upper 28 bits of its address travel
|
||||
* through the mailbox).
|
||||
*/
|
||||
|
||||
#include "starkernel/rpi5_mailbox.h"
|
||||
#include "starkernel/rpi5_dtb.h"
|
||||
|
||||
#define MBOX_OFF_READ 0x00u
|
||||
#define MBOX_OFF_STATUS0 0x18u
|
||||
#define MBOX_OFF_WRITE 0x20u
|
||||
#define MBOX_OFF_STATUS1 0x38u
|
||||
|
||||
#define MBOX_FULL 0x80000000u
|
||||
#define MBOX_EMPTY 0x40000000u
|
||||
|
||||
#define MBOX_CH_PROPERTY 8u
|
||||
|
||||
#define MBOX_CODE_REQUEST 0x00000000u
|
||||
#define MBOX_CODE_RESPONSE_OK 0x80000000u
|
||||
|
||||
#define TAG_SET_PHYS_SIZE 0x00048003u
|
||||
#define TAG_SET_VIRT_SIZE 0x00048004u
|
||||
#define TAG_SET_DEPTH 0x00048005u
|
||||
#define TAG_SET_PIXEL_ORDER 0x00048006u
|
||||
#define TAG_SET_VIRTUAL_OFFS 0x00048009u
|
||||
#define TAG_ALLOCATE_BUFFER 0x00040001u
|
||||
#define TAG_GET_PITCH 0x00040008u
|
||||
#define TAG_END 0x00000000u
|
||||
|
||||
#define PIXEL_ORDER_RGB 1u /* per the wiki's own tag doc: 0=BGR, 1=RGB */
|
||||
|
||||
/* 16-byte-aligned static request/response buffer -- no allocator
|
||||
* exists this early in boot (this driver runs before kmalloc's own
|
||||
* init, same as every other pre-M6 boot step), and one buffer is
|
||||
* enough: this driver only ever has one request in flight, never
|
||||
* concurrent. Exact word count for the 7-tag sequence below, counted
|
||||
* word-by-word (not estimated): 2 [header] + 5 [phys size] + 5 [virt
|
||||
* size] + 4 [depth] + 4 [pixel order] + 5 [virtual offset] + 5
|
||||
* [allocate] + 4 [get pitch] + 1 [end tag] = 35 words (140 bytes) --
|
||||
* 40 gives real margin without the array itself getting large enough
|
||||
* to matter. (An earlier version of this comment claimed 32 words was
|
||||
* "generous headroom" for a 38-word estimate against a 32-word array
|
||||
* -- that was wrong on its own terms before even being checked against
|
||||
* the real 35-word count; recomputed properly this time, not
|
||||
* re-estimated.) */
|
||||
static uint32_t g_mbox_buf[40] __attribute__((aligned(16)));
|
||||
|
||||
static inline void mmio_write32(uint64_t addr, uint32_t val)
|
||||
{
|
||||
*(volatile uint32_t*) addr = val;
|
||||
}
|
||||
|
||||
static inline uint32_t mmio_read32(uint64_t addr)
|
||||
{
|
||||
return *(volatile uint32_t*) addr;
|
||||
}
|
||||
|
||||
/* Appends one tag (id, value-buffer size in bytes, request size in
|
||||
* bytes, then `nvalues` u32 value words) to g_mbox_buf starting at
|
||||
* word index *idx, advancing *idx past it (including alignment
|
||||
* padding -- every value here is a whole number of u32 words already,
|
||||
* so no padding math is actually needed, but the wiki's own format
|
||||
* allows non-multiple-of-4 value sizes in general; this driver's own
|
||||
* tags never do). */
|
||||
static void append_tag(uint32_t* idx, uint32_t tag_id, uint32_t value_bytes,
|
||||
const uint32_t* values, uint32_t nvalues)
|
||||
{
|
||||
uint32_t i;
|
||||
g_mbox_buf[(*idx)++] = tag_id;
|
||||
g_mbox_buf[(*idx)++] = value_bytes;
|
||||
/* Request-size word: set equal to value_bytes (the value-buffer's
|
||||
* own size, which for TAG_ALLOCATE_BUFFER is 8 -- the response's
|
||||
* size, since the buffer must be big enough for whichever is
|
||||
* larger -- not the 4-byte true request payload). Common practice
|
||||
* across multiple reference implementations surveyed while writing
|
||||
* this, not a single spec-quoted number; firmware is not observed
|
||||
* to validate an exact match against the true request length.
|
||||
* Unverified against real hardware either way. */
|
||||
g_mbox_buf[(*idx)++] = value_bytes;
|
||||
for (i = 0; i < nvalues; i++) g_mbox_buf[(*idx)++] = values[i];
|
||||
}
|
||||
|
||||
int rpi5_mailbox_get_framebuffer(const void* dtb, uint32_t width, uint32_t height,
|
||||
uint32_t bpp, Rpi5FramebufferInfo* out)
|
||||
{
|
||||
uint64_t base;
|
||||
uint32_t idx;
|
||||
uint32_t send_val;
|
||||
uint32_t v2[2];
|
||||
|
||||
if (!out) return -1;
|
||||
|
||||
base = rpi5_mailbox_base(dtb);
|
||||
if (!base) return -2; /* mailbox node not found/malformed in the DTB */
|
||||
|
||||
idx = 2; /* words 0/1 are the header, filled in after tags are built */
|
||||
|
||||
v2[0] = width;
|
||||
v2[1] = height;
|
||||
append_tag(&idx, TAG_SET_PHYS_SIZE, 8, v2, 2);
|
||||
append_tag(&idx, TAG_SET_VIRT_SIZE, 8, v2, 2);
|
||||
|
||||
v2[0] = bpp;
|
||||
append_tag(&idx, TAG_SET_DEPTH, 4, v2, 1);
|
||||
|
||||
v2[0] = PIXEL_ORDER_RGB;
|
||||
append_tag(&idx, TAG_SET_PIXEL_ORDER, 4, v2, 1);
|
||||
|
||||
v2[0] = 0; v2[1] = 0; /* virtual offset x,y -- always (0,0), no scrolling support */
|
||||
append_tag(&idx, TAG_SET_VIRTUAL_OFFS, 8, v2, 2);
|
||||
|
||||
v2[0] = 16; /* alignment request, bytes */
|
||||
v2[1] = 0; /* response slot (base addr); request only sends v2[0] */
|
||||
append_tag(&idx, TAG_ALLOCATE_BUFFER, 8, v2, 2);
|
||||
|
||||
v2[0] = 0;
|
||||
append_tag(&idx, TAG_GET_PITCH, 4, v2, 1);
|
||||
|
||||
g_mbox_buf[idx++] = TAG_END;
|
||||
|
||||
g_mbox_buf[0] = idx * 4u; /* total buffer size, bytes */
|
||||
g_mbox_buf[1] = MBOX_CODE_REQUEST;
|
||||
|
||||
send_val = ((uint32_t) (uintptr_t) g_mbox_buf & ~0xFu) | MBOX_CH_PROPERTY;
|
||||
|
||||
while ((mmio_read32(base + MBOX_OFF_STATUS1) & MBOX_FULL) != 0)
|
||||
{ /* spin until MBOX1 (send) has room */
|
||||
}
|
||||
mmio_write32(base + MBOX_OFF_WRITE, send_val);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
while ((mmio_read32(base + MBOX_OFF_STATUS0) & MBOX_EMPTY) != 0)
|
||||
{ /* spin until MBOX0 (receive) has a message */
|
||||
}
|
||||
if (mmio_read32(base + MBOX_OFF_READ) == send_val) break;
|
||||
/* A message arrived, but not the one we sent (another channel's
|
||||
* traffic) -- discard and keep waiting, per the wiki's own
|
||||
* documented channel-matching convention. */
|
||||
}
|
||||
|
||||
if (g_mbox_buf[1] != MBOX_CODE_RESPONSE_OK) return -3; /* VC rejected the request */
|
||||
|
||||
/* Re-walk the tags to pull out the allocate-buffer and get-pitch
|
||||
* responses -- same fixed order they were sent in, so fixed word
|
||||
* offsets are safe to compute directly rather than re-parsing
|
||||
* generically (this driver only ever sends this one sequence). */
|
||||
{
|
||||
uint32_t alloc_tag_value_off = 2 /* header */
|
||||
+ (3 + 2) /* phys size */
|
||||
+ (3 + 2) /* virt size */
|
||||
+ (3 + 1) /* depth */
|
||||
+ (3 + 1) /* pixel order */
|
||||
+ (3 + 2) /* virtual offset */
|
||||
+ 3; /* allocate-buffer tag header, then its value words */
|
||||
uint32_t pitch_tag_value_off = alloc_tag_value_off + 2 /* alloc response: base+size */
|
||||
+ 3; /* get-pitch tag header, then its value word */
|
||||
|
||||
uint32_t fb_addr = g_mbox_buf[alloc_tag_value_off];
|
||||
uint32_t fb_size = g_mbox_buf[alloc_tag_value_off + 1];
|
||||
uint32_t pitch_bytes = g_mbox_buf[pitch_tag_value_off];
|
||||
|
||||
if (fb_addr == 0 || fb_size == 0 || pitch_bytes == 0) return -4;
|
||||
|
||||
/* Allocate-buffer's own response address is a VC bus address,
|
||||
* not necessarily the plain ARM-physical one -- masking the
|
||||
* top bits to recover the ARM-physical alias is the documented
|
||||
* convention on earlier Pi chips; kept here defensively even
|
||||
* though the Pi-5-specific reference this file cites for the
|
||||
* *send* side found no bus-alias bit in play there. Genuinely
|
||||
* unverified which of these is correct for Pi 5's own
|
||||
* allocate-buffer response specifically -- flagged, not
|
||||
* guessed past this comment. */
|
||||
out->base = (void*) (uintptr_t) (fb_addr & 0x3FFFFFFFu);
|
||||
out->size = fb_size;
|
||||
out->width = width;
|
||||
out->height = height;
|
||||
out->pixels_per_scanline = pitch_bytes / (bpp / 8u);
|
||||
out->pixel_format = 0; /* PixelRedGreenBlueReserved8BitPerColor --
|
||||
* matches PIXEL_ORDER_RGB requested above */
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* switch.S (aarch64) - 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 interrupt has no call
|
||||
* site. sk_vm_switch_to() is an ordinary `bl`, so AAPCS64 already makes
|
||||
* every caller-saved register (x0-x18, v0-v7, v16-v31) the caller's own
|
||||
* problem across the call. Only the callee-saved set needs explicit
|
||||
* handling: x19-x28, x29 (fp), x30 (lr), and d8-d15 (only the low 64
|
||||
* bits of v8-v15 are callee-saved per AAPCS64).
|
||||
*
|
||||
* void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
|
||||
* x0 = save_sp_out, x1 = new_sp
|
||||
*
|
||||
* For a VM that has never run before, new_sp must point at a synthetic
|
||||
* 160-byte frame built by the C glue (switch.c) to this exact layout,
|
||||
* with x30 (lr) set to the trampoline entry address and everything else
|
||||
* zeroed, so the final `ret` below (which branches to lr) jumps there.
|
||||
*/
|
||||
|
||||
.section .text
|
||||
.balign 4
|
||||
|
||||
.equ SW_X19, 0
|
||||
.equ SW_X21, 16
|
||||
.equ SW_X23, 32
|
||||
.equ SW_X25, 48
|
||||
.equ SW_X27, 64
|
||||
.equ SW_X29, 80 /* x29 (fp), x30 (lr) */
|
||||
.equ SW_D8, 96
|
||||
.equ SW_D10, 112
|
||||
.equ SW_D12, 128
|
||||
.equ SW_D14, 144
|
||||
.equ SW_SIZE, 160
|
||||
|
||||
/* .hidden is an ELF-only visibility directive with no COFF equivalent;
|
||||
* this file is also assembled for the aarch64 loader, which clang targets
|
||||
* at aarch64-pc-windows-msvc (PE/COFF) -- guard on _WIN32 (defined only by
|
||||
* that target), matching isr.S's own precedent exactly. */
|
||||
#ifndef _WIN32
|
||||
.hidden sk_vm_switch_to
|
||||
#endif
|
||||
|
||||
.global sk_vm_switch_to
|
||||
sk_vm_switch_to:
|
||||
sub sp, sp, #SW_SIZE
|
||||
|
||||
stp x19, x20, [sp, #SW_X19]
|
||||
stp x21, x22, [sp, #SW_X21]
|
||||
stp x23, x24, [sp, #SW_X23]
|
||||
stp x25, x26, [sp, #SW_X25]
|
||||
stp x27, x28, [sp, #SW_X27]
|
||||
stp x29, x30, [sp, #SW_X29]
|
||||
stp d8, d9, [sp, #SW_D8]
|
||||
stp d10, d11, [sp, #SW_D10]
|
||||
stp d12, d13, [sp, #SW_D12]
|
||||
stp d14, d15, [sp, #SW_D14]
|
||||
|
||||
mov x2, sp
|
||||
str x2, [x0] /* *save_sp_out = sp (this context's parked SP) */
|
||||
|
||||
mov sp, x1 /* sp = new_sp (switch stacks) */
|
||||
|
||||
ldp x19, x20, [sp, #SW_X19]
|
||||
ldp x21, x22, [sp, #SW_X21]
|
||||
ldp x23, x24, [sp, #SW_X23]
|
||||
ldp x25, x26, [sp, #SW_X25]
|
||||
ldp x27, x28, [sp, #SW_X27]
|
||||
ldp x29, x30, [sp, #SW_X29]
|
||||
ldp d8, d9, [sp, #SW_D8]
|
||||
ldp d10, d11, [sp, #SW_D10]
|
||||
ldp d12, d13, [sp, #SW_D12]
|
||||
ldp d14, d15, [sp, #SW_D14]
|
||||
|
||||
add 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` (x30/lr) 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.
|
||||
*/
|
||||
#ifndef _WIN32
|
||||
.hidden sk_vm_switch_prime
|
||||
#endif
|
||||
|
||||
.global sk_vm_switch_prime
|
||||
sk_vm_switch_prime:
|
||||
sub x2, x0, #SW_SIZE
|
||||
stp xzr, xzr, [x2, #SW_X19]
|
||||
stp xzr, xzr, [x2, #SW_X21]
|
||||
stp xzr, xzr, [x2, #SW_X23]
|
||||
stp xzr, xzr, [x2, #SW_X25]
|
||||
stp xzr, xzr, [x2, #SW_X27]
|
||||
stp xzr, x1, [x2, #SW_X29] /* x29=0, x30(lr)=entry */
|
||||
stp xzr, xzr, [x2, #SW_D8]
|
||||
stp xzr, xzr, [x2, #SW_D10]
|
||||
stp xzr, xzr, [x2, #SW_D12]
|
||||
stp xzr, xzr, [x2, #SW_D14]
|
||||
mov x0, x2
|
||||
ret
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
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 (aarch64) - Timer subsystem using ARM Generic Timer (CNTPCT_EL0)
|
||||
*
|
||||
* Uses the 64-bit system counter available as CNTPCT_EL0. The counter
|
||||
* frequency is read from CNTFRQ_EL0 (set by firmware).
|
||||
*/
|
||||
|
||||
#include "timer.h"
|
||||
#include "console.h"
|
||||
#include "q48_16.h"
|
||||
#include "uefi.h"
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ─── ARM generic-timer helpers ─────────────────────────────────────── */
|
||||
|
||||
/* FABRIC-2.md §I.5, 2026-09-04: real hypervisor-vs-hardware detection.
|
||||
* s_cal.vm_mode was hardcoded to 1 unconditionally below (comment:
|
||||
* "QEMU SBSA always uses virtualised Generic Timer") -- true for the
|
||||
* *timing policy* this file cares about, but wrong to reuse as a
|
||||
* general "are we in QEMU" signal elsewhere (contrib-capsule trust-tier
|
||||
* enforcement, §I.5), which is exactly what happened before this was
|
||||
* caught: it would report "always QEMU" on real hardware too.
|
||||
*
|
||||
* ARM has no single CPUID-equivalent hypervisor-present bit the way
|
||||
* amd64's CPUID.1:ECX[31] is (arch/amd64/timer.c's own
|
||||
* running_under_hypervisor()). Checks the ACPI RSDP's OEM ID instead --
|
||||
* "BOCHS " is the SeaBIOS-heritage signature QEMU's ACPI table
|
||||
* generation has used since long before this project, on every machine
|
||||
* type it emulates, aarch64 virt included. Real hardware vendors set
|
||||
* their own OEMID, never this string. Mirrors pci.c's own private Rsdp2
|
||||
* struct layout (signature[8], checksum, oem_id[6]) -- duplicated
|
||||
* rather than shared, same "a few TLV-walking lines is simpler than a
|
||||
* new header" precedent tools/pkcs8_ed25519.c already set. */
|
||||
static int running_under_hypervisor(BootInfo *boot_info) {
|
||||
if (!boot_info || !boot_info->acpi_table) return 0;
|
||||
const uint8_t *rsdp = (const uint8_t *) boot_info->acpi_table;
|
||||
if (memcmp(rsdp, "RSD PTR ", 8) != 0) return 0;
|
||||
return memcmp(rsdp + 9, "BOCHS ", 6) == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the AArch64 Physical System Counter with ISB serialisation.
|
||||
*
|
||||
* Issues an @c ISB (Instruction Synchronisation Barrier) before reading
|
||||
* @c CNTPCT_EL0 to ensure all preceding instructions have completed before
|
||||
* the counter value is sampled. This prevents the counter read from being
|
||||
* speculated ahead of a preceding store or flag update, which would produce
|
||||
* an erroneously early timestamp.
|
||||
*
|
||||
* The ISB is heavier than strictly necessary for most timing uses but
|
||||
* provides the same ordering guarantee as @c RDTSCP on x86-64. For
|
||||
* interval measurements where start and end reads are in the same path,
|
||||
* the overhead (1–3 cycles) is negligible.
|
||||
*
|
||||
* @return Current 64-bit @c CNTPCT_EL0 counter value in system-counter ticks.
|
||||
*/
|
||||
static inline uint64_t cntpct_read(void)
|
||||
{
|
||||
uint64_t val;
|
||||
__asm__ volatile ("isb; mrs %0, cntpct_el0" : "=r"(val));
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the AArch64 Counter Frequency Register (@c CNTFRQ_EL0).
|
||||
*
|
||||
* @c CNTFRQ_EL0 is a read-only EL0-accessible register whose value is
|
||||
* written by EL3/EL2 firmware at boot to advertise the system counter
|
||||
* frequency in Hz. It is the architectural way to discover @c CNTPCT_EL0's
|
||||
* tick rate without out-of-band knowledge of the board.
|
||||
*
|
||||
* QEMU's SBSA/virt machine models typically set this to 62,500,000 Hz
|
||||
* (62.5 MHz) for Cortex-A57 compatibility. Real hardware varies widely
|
||||
* (10 MHz on Raspberry Pi 4, 24 MHz on Cortex-A53 dev boards, etc.).
|
||||
* @c timer_init() falls back to 62,500,000 if the register reads zero.
|
||||
*
|
||||
* @return Counter frequency in Hz as set by firmware; 0 if firmware did
|
||||
* not initialise the register (uncommon but possible in bare-metal
|
||||
* bring-up without a proper firmware stack).
|
||||
*/
|
||||
static inline uint64_t cntfrq_read(void)
|
||||
{
|
||||
uint64_t val;
|
||||
__asm__ volatile ("mrs %0, cntfrq_el0" : "=r"(val));
|
||||
return val;
|
||||
}
|
||||
|
||||
/* ─── Module state ───────────────────────────────────────────────────── */
|
||||
|
||||
static uint64_t s_counter_hz = 0; /* CNTFRQ_EL0 (ticks/second) */
|
||||
static uint64_t s_ns_per_tick = 0; /* nanoseconds per counter tick (scaled) */
|
||||
static uint64_t s_base_count = 0; /* counter value at timer_init() */
|
||||
static uint64_t s_base_ns = 0; /* ns offset at timer_init() */
|
||||
|
||||
static timer_calibration_record_t s_cal;
|
||||
|
||||
/* ─── timer_init ─────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* @brief Initialise the AArch64 timer subsystem from @c CNTFRQ_EL0 (M5).
|
||||
*
|
||||
* Reads the system counter frequency from @c CNTFRQ_EL0. If firmware left
|
||||
* this register zero (a non-conforming but possible situation in bare-metal
|
||||
* bring-up) the function falls back to 62,500,000 Hz — the canonical
|
||||
* Cortex-A57 / QEMU SBSA frequency.
|
||||
*
|
||||
* Derived values stored in module state:
|
||||
* - @c s_counter_hz — raw frequency in Hz.
|
||||
* - @c s_base_count — @c CNTPCT_EL0 sample at init time; all subsequent
|
||||
* @c timer_now_ns() calls compute elapsed ticks as
|
||||
* @c (cntpct_read() - s_base_count).
|
||||
* - @c s_base_ns — 0 at init; the ns origin for @c timer_now_ns().
|
||||
* - @c s_ns_per_tick — Q16.16 fixed-point nanoseconds per counter tick,
|
||||
* computed as @c (1e9 << 16) / @c freq to avoid floating-point in the
|
||||
* freestanding build. @c timer_now_ns() right-shifts by 16 after the
|
||||
* multiply to recover the integer nanosecond count.
|
||||
*
|
||||
* The calibration record @c s_cal is populated with the firmware-reported
|
||||
* frequency, @c converged = 1, and @c trust = @c TIMER_TRUST_ABSOLUTE
|
||||
* because the ARM Generic Timer is invariant and synchronised by
|
||||
* architecture — no drift-detection loop is needed.
|
||||
*
|
||||
* @param boot_info Kernel @c BootInfo -- GIC base address will be used in a
|
||||
* later milestone for the interrupt controller; as of
|
||||
* 2026-09-04 also consulted here for @c acpi_table, so
|
||||
* @c s_cal.vm_mode can be a real hypervisor-vs-hardware
|
||||
* detection instead of the hardcoded-1 timing-policy
|
||||
* shortcut this field used to double as (see
|
||||
* running_under_hypervisor()'s own doc comment above).
|
||||
* @return 0 always.
|
||||
*/
|
||||
int timer_init(BootInfo *boot_info)
|
||||
{
|
||||
uint64_t freq = cntfrq_read();
|
||||
if (freq == 0) {
|
||||
/* Firmware did not set CNTFRQ; assume 62.5 MHz (Cortex-A57 default) */
|
||||
freq = 62500000ULL;
|
||||
}
|
||||
|
||||
s_counter_hz = freq;
|
||||
s_base_count = cntpct_read();
|
||||
s_base_ns = 0;
|
||||
|
||||
/* ns_per_tick = 1e9 / freq — store as (1e9 << 16) / freq for fixed-point */
|
||||
s_ns_per_tick = (1000000000ULL << 16) / freq;
|
||||
|
||||
s_cal.tsc_hz_mean = freq;
|
||||
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;
|
||||
s_cal.trust = TIMER_TRUST_ABSOLUTE;
|
||||
|
||||
console_println("Timer: AArch64 generic timer initialised.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ─── timer_tsc_hz ───────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Return the system counter frequency in Hz.
|
||||
*
|
||||
* Returns @c s_counter_hz as set from @c CNTFRQ_EL0 by @c timer_init().
|
||||
* If @c timer_init() has not been called yet (or firmware reported 0 and
|
||||
* the fallback was not applied), returns 62,500,000 Hz as a safe default.
|
||||
*
|
||||
* Used by @c apic_timer_init() to compute @c s_timer_period_tsc and by
|
||||
* the shim time backend to convert counter ticks to nanoseconds before
|
||||
* the full timer subsystem is available.
|
||||
*
|
||||
* @return System counter frequency in Hz; at least 62,500,000.
|
||||
*/
|
||||
uint64_t timer_tsc_hz(void)
|
||||
{
|
||||
return s_counter_hz ? s_counter_hz : 62500000ULL;
|
||||
}
|
||||
|
||||
/* ─── timer_now_ns ───────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Return a monotonic nanosecond timestamp.
|
||||
*
|
||||
* Reads @c CNTPCT_EL0, computes the 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 multiplication uses 64-bit arithmetic; at typical counter frequencies
|
||||
* (≤ 100 MHz) and reasonable uptime intervals (days to weeks) there is no
|
||||
* overflow risk in the delta-to-ns conversion.
|
||||
*
|
||||
* @return Monotonic nanosecond count since @c timer_init(), starting at 0.
|
||||
*/
|
||||
uint64_t timer_now_ns(void)
|
||||
{
|
||||
uint64_t delta = cntpct_read() - s_base_count;
|
||||
/* delta * (1e9 / freq) = (delta * ns_per_tick_q16) >> 16 */
|
||||
return s_base_ns + ((delta * s_ns_per_tick) >> 16);
|
||||
}
|
||||
|
||||
/* ─── timer_check_drift_now ──────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Check for timer drift (AArch64 stub — always returns 0).
|
||||
*
|
||||
* On x86-64 this function cross-checks the TSC against the HPET or PM
|
||||
* Timer to detect frequency drift. The AArch64 Generic Timer is
|
||||
* architecturally invariant and synchronised across all cores in the
|
||||
* system; it cannot drift against itself. This stub always returns 0
|
||||
* (no drift detected) to satisfy the common @c timer_check_drift_now()
|
||||
* call site.
|
||||
*
|
||||
* @return 0 always.
|
||||
*/
|
||||
int timer_check_drift_now(void)
|
||||
{
|
||||
return 0; /* Stub: ARM counter has no drift against itself */
|
||||
}
|
||||
|
||||
/* ─── timer_calibration_record ───────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Return a pointer to the timer calibration record.
|
||||
*
|
||||
* Returns @c &s_cal which is populated by @c timer_init() with:
|
||||
* - @c tsc_hz_mean = @c CNTFRQ_EL0 (or 62.5 MHz fallback)
|
||||
* - @c hpet_hz = 0 (no HPET on AArch64)
|
||||
* - @c pit_hz_mean = 0 (no PIT on AArch64)
|
||||
* - @c converged = 1 (Generic Timer is already calibrated by firmware)
|
||||
* - @c vm_mode = real hypervisor-vs-hardware detection as of 2026-09-04
|
||||
* (ACPI RSDP OEM ID check, running_under_hypervisor() above) -- the
|
||||
* Generic Timer itself works identically either way, so this field's
|
||||
* *timing* meaning here is purely informational, unlike amd64 where
|
||||
* vm_mode gates a real calibration-path choice
|
||||
* - @c trust = @c TIMER_TRUST_ABSOLUTE
|
||||
*
|
||||
* The record is exposed to @c kernel_main() and @c timer.h consumers for
|
||||
* boot-log reporting and for the heartbeat subsystem's trust initialisation.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/* ─── Heartbeat ──────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* @brief Read the raw counter the aarch64 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() and the per-arch @c g_heartbeat
|
||||
* state that used to live in this file. This is the one piece that stays
|
||||
* per-architecture -- the same @c CNTPCT_EL0 the rest of this file's
|
||||
* calibration already reads via @c cntpct_read().
|
||||
*
|
||||
* @return Current @c CNTPCT_EL0 value.
|
||||
*/
|
||||
uint64_t heartbeat_read_counter(void)
|
||||
{
|
||||
return cntpct_read();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# src/starkernel/arch/amd64/
|
||||
|
||||
AMD64 architecture support for LithosAnanke.
|
||||
|
||||
- `arch.c` — architecture init entry point.
|
||||
- `apic.c` — Local APIC interrupt controller driver.
|
||||
- `timer.c` — TSC/HPET/APIC timer setup (100 Hz heartbeat tick).
|
||||
- `interrupts.c` — IDT setup and interrupt dispatch.
|
||||
- `boot.S` — early boot assembly.
|
||||
- `kernel_entry.S` — kernel entry trampoline invoked from the loader.
|
||||
- `isr.S` — interrupt service routine stubs.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
This file is part of the StarForth project.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at:
|
||||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||||
|
||||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
express or implied, including but not limited to the warranties of
|
||||
merchantability, fitness for a particular purpose, and noninfringement.
|
||||
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
This file is part of the StarForth project.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at:
|
||||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||||
|
||||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
express or implied, including but not limited to the warranties of
|
||||
merchantability, fitness for a particular purpose, and noninfringement.
|
||||
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* arch.c (amd64) - Architecture abstractions for StarKernel on x86_64
|
||||
*/
|
||||
|
||||
#include "arch.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* GDT entries for 64-bit mode.
|
||||
* In long mode, the base and limit fields of code/data segments are ignored,
|
||||
* but the access and flags bytes must be correct.
|
||||
*/
|
||||
struct gdt_entry {
|
||||
uint16_t limit_low;
|
||||
uint16_t base_low;
|
||||
uint8_t base_mid;
|
||||
uint8_t access;
|
||||
uint8_t granularity; /* includes high nibble of limit */
|
||||
uint8_t base_high;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct gdtr {
|
||||
uint16_t limit;
|
||||
uint64_t base;
|
||||
} __attribute__((packed));
|
||||
|
||||
/*
|
||||
* GDT layout:
|
||||
* 0x00: Null descriptor
|
||||
* 0x08: 64-bit kernel code segment (selector used in IDT)
|
||||
* 0x10: Kernel data segment
|
||||
*/
|
||||
static struct gdt_entry gdt[3] __attribute__((aligned(16)));
|
||||
|
||||
/**
|
||||
* @brief Populate a GDT entry with limit 0xFFFF and a zero base.
|
||||
*
|
||||
* In 64-bit long mode the base and limit fields of code/data segment
|
||||
* descriptors are ignored by the processor; only the @p access and
|
||||
* @p granularity flag bytes are architecturally significant. This helper
|
||||
* writes the canonical long-mode descriptor layout: limit = 0xFFFF, all base
|
||||
* bytes = 0, then stores the provided access and granularity values.
|
||||
*
|
||||
* @param index Index into the @c gdt[] array (0 = null, 1 = code, 2 = data).
|
||||
* @param access Access byte (e.g., 0x9A for kernel code, 0x92 for data).
|
||||
* @param granularity Granularity/flags byte (e.g., 0x20 sets the L-bit for 64-bit code).
|
||||
*/
|
||||
static void gdt_set_entry(int index, uint8_t access, uint8_t granularity)
|
||||
{
|
||||
gdt[index].limit_low = 0xFFFF;
|
||||
gdt[index].base_low = 0;
|
||||
gdt[index].base_mid = 0;
|
||||
gdt[index].access = access;
|
||||
gdt[index].granularity = granularity;
|
||||
gdt[index].base_high = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialise and load the minimal 3-entry GDT required for long mode.
|
||||
*
|
||||
* Builds three GDT entries in the @c gdt[] array and loads the descriptor via
|
||||
* @c LGDT followed by a far-return sequence to reload @c CS to selector 0x08
|
||||
* and all other segment registers to 0x10:
|
||||
*
|
||||
* - Slot 0 (0x00): Null descriptor — all fields zero.
|
||||
* - Slot 1 (0x08): 64-bit kernel code (access=0x9A, granularity=0x20).
|
||||
* - Slot 2 (0x10): Kernel data (access=0x92, granularity=0x00).
|
||||
*
|
||||
* Must be called before @c arch_interrupts_init() because the IDT uses
|
||||
* selector 0x08 for all gate descriptors.
|
||||
*/
|
||||
static void gdt_init(void)
|
||||
{
|
||||
/* Null descriptor */
|
||||
gdt[0].limit_low = 0;
|
||||
gdt[0].base_low = 0;
|
||||
gdt[0].base_mid = 0;
|
||||
gdt[0].access = 0;
|
||||
gdt[0].granularity = 0;
|
||||
gdt[0].base_high = 0;
|
||||
|
||||
/*
|
||||
* 64-bit kernel code segment (selector 0x08)
|
||||
* Access: 0x9A = Present (1), DPL 0 (00), S=1 (code/data), Exec (1), Conforming (0), Readable (1), Accessed (0)
|
||||
* Granularity: 0x20 = Long mode (L=1), Default operand size (D=0, must be 0 for long mode)
|
||||
*/
|
||||
gdt_set_entry(1, 0x9A, 0x20);
|
||||
|
||||
/*
|
||||
* Kernel data segment (selector 0x10)
|
||||
* Access: 0x92 = Present (1), DPL 0 (00), S=1 (code/data), Exec (0), Direction (0), Writable (1), Accessed (0)
|
||||
* Granularity: 0x00 (no special flags for data segment)
|
||||
*/
|
||||
gdt_set_entry(2, 0x92, 0x00);
|
||||
|
||||
struct gdtr gdtr_val;
|
||||
gdtr_val.limit = sizeof(gdt) - 1;
|
||||
gdtr_val.base = (uint64_t)&gdt[0];
|
||||
|
||||
__asm__ volatile (
|
||||
"lgdt %0\n\t"
|
||||
"pushq $0x08\n\t" /* Push new CS */
|
||||
"leaq 1f(%%rip), %%rax\n\t" /* Load address of label 1 */
|
||||
"pushq %%rax\n\t" /* Push return address */
|
||||
"lretq\n\t" /* Far return to reload CS */
|
||||
"1:\n\t"
|
||||
"mov $0x10, %%ax\n\t" /* Load new DS/ES/SS/FS/GS */
|
||||
"mov %%ax, %%ds\n\t"
|
||||
"mov %%ax, %%es\n\t"
|
||||
"mov %%ax, %%ss\n\t"
|
||||
"mov %%ax, %%fs\n\t"
|
||||
"mov %%ax, %%gs\n\t"
|
||||
:
|
||||
: "m"(gdtr_val)
|
||||
: "rax", "memory"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform the earliest x86-64 architecture initialisation.
|
||||
*
|
||||
* Called immediately after the UEFI boot stub transfers control to the
|
||||
* kernel, before the console or interrupt controller are available.
|
||||
* Loads the minimal three-entry GDT via @c gdt_init() so that subsequent
|
||||
* code has valid segment selectors. IDT setup is deferred until after the
|
||||
* console is live (called separately via @c arch_interrupts_init()).
|
||||
*/
|
||||
void arch_early_init(void)
|
||||
{
|
||||
gdt_init(); /* Must be done before IDT, since IDT uses selector 0x08 */
|
||||
/* IDT setup is done later via arch_interrupts_init() after console is live */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enable hardware interrupts on the current CPU core (x86-64 STI).
|
||||
*
|
||||
* Issues the @c STI instruction to set the Interrupt Flag. Must be called
|
||||
* after the IDT is loaded and the APIC is configured, otherwise the first
|
||||
* interrupt will triple-fault.
|
||||
*/
|
||||
void arch_enable_interrupts(void)
|
||||
{
|
||||
__asm__ volatile ("sti");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Disable hardware interrupts on the current CPU core (x86-64 CLI).
|
||||
*
|
||||
* Issues the @c CLI instruction to clear the Interrupt Flag. Used around
|
||||
* critical sections in the single-threaded kernel where interrupt delivery
|
||||
* would corrupt shared state.
|
||||
*/
|
||||
void arch_disable_interrupts(void)
|
||||
{
|
||||
__asm__ volatile ("cli");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Halt the CPU until the next interrupt (x86-64 HLT).
|
||||
*
|
||||
* Issues the @c HLT instruction. In the kernel panic path this is called
|
||||
* in an infinite loop (interrupts disabled) to stop the processor permanently.
|
||||
* In normal use it is called once per idle iteration to reduce power consumption.
|
||||
*/
|
||||
void arch_halt(void)
|
||||
{
|
||||
__asm__ volatile ("hlt");
|
||||
}
|
||||
|
||||
void arch_cold_reset(void)
|
||||
{
|
||||
arch_disable_interrupts();
|
||||
/* Pulse keyboard controller reset line — standard bare-metal amd64 reset */
|
||||
__asm__ volatile ("outb %0, %1" : : "a"((uint8_t)0xFE), "Nd"((uint16_t)0x64));
|
||||
/* Should not reach here; spin as fallback */
|
||||
for (;;) __asm__ volatile ("cli; hlt");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the x86-64 Time Stamp Counter (RDTSC).
|
||||
*
|
||||
* Executes @c RDTSC and combines @c EDX:EAX into a 64-bit value. Does not
|
||||
* serialise instruction execution; use @c RDTSCP or a surrounding @c CPUID
|
||||
* where ordering guarantees are required.
|
||||
*
|
||||
* @return Current TSC value in CPU cycles.
|
||||
*/
|
||||
uint64_t arch_read_timestamp(void)
|
||||
{
|
||||
uint32_t lo, hi;
|
||||
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
|
||||
return ((uint64_t)hi << 32) | lo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialise the x86-64 MMU (stub — deferred to a later milestone).
|
||||
*
|
||||
* Full four-level page table setup is deferred; the VMM subsystem
|
||||
* (@c vmm.c) handles page table management independently. This function
|
||||
* exists as an architectural hook for future per-CPU MMU configuration
|
||||
* that must run after GDT/IDT setup but before the first memory access
|
||||
* through a non-identity-mapped virtual address.
|
||||
*/
|
||||
void arch_mmu_init(void)
|
||||
{
|
||||
/* Stub: paging will be wired up in later milestones */
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* boot.S (amd64) - Placeholder entry for future low-level bring-up.
|
||||
* The UEFI loader currently calls efi_main() directly; this stub exists
|
||||
* to reserve the symbol for later early-boot work.
|
||||
*/
|
||||
.section .text.entry
|
||||
.code64
|
||||
.global arch_bootstrap
|
||||
arch_bootstrap:
|
||||
ret
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
This file is part of the StarForth project.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at:
|
||||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||||
|
||||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
express or implied, including but not limited to the warranties of
|
||||
merchantability, fitness for a particular purpose, and noninfringement.
|
||||
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* i8042.c - PS/2 keyboard controller driver (amd64)
|
||||
*
|
||||
* Item 4.3.5 (FABRIC-0.md §27.5).
|
||||
*/
|
||||
|
||||
#ifndef __STARKERNEL__
|
||||
#error "i8042.c is kernel-only"
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include "starkernel/i8042.h"
|
||||
#include "console.h"
|
||||
|
||||
#define I8042_DATA_PORT 0x60
|
||||
#define I8042_STATUS_PORT 0x64
|
||||
#define I8042_CMD_PORT 0x64
|
||||
|
||||
#define I8042_STATUS_OUTPUT_FULL (1u << 0)
|
||||
#define I8042_STATUS_INPUT_FULL (1u << 1)
|
||||
|
||||
#define I8042_CMD_READ_CONFIG 0x20
|
||||
#define I8042_CMD_WRITE_CONFIG 0x60
|
||||
#define I8042_CONFIG_IRQ1_ENABLE (1u << 0)
|
||||
|
||||
#define I8042_DEV_CMD_ENABLE_SCANNING 0xF4
|
||||
#define I8042_DEV_ACK 0xFA
|
||||
|
||||
/* Bounded wait -- command handshakes during one-time init only, never in
|
||||
* the interrupt path or a per-keystroke loop. */
|
||||
#define I8042_INIT_WAIT_ITERS 100000
|
||||
|
||||
static inline void outb(uint16_t port, uint8_t val) {
|
||||
__asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
|
||||
}
|
||||
static inline uint8_t inb(uint16_t port) {
|
||||
uint8_t val;
|
||||
__asm__ volatile ("inb %1, %0" : "=a"(val) : "Nd"(port));
|
||||
return val;
|
||||
}
|
||||
|
||||
void i8042_init(void) {
|
||||
/* Drain any stale output byte left by firmware before enabling IRQ1 —
|
||||
* otherwise the first "interrupt" would really be this leftover byte
|
||||
* read via polling, not a real IRQ1 delivery. */
|
||||
while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
|
||||
(void)inb(I8042_DATA_PORT);
|
||||
}
|
||||
|
||||
outb(I8042_CMD_PORT, I8042_CMD_READ_CONFIG);
|
||||
uint8_t config = inb(I8042_DATA_PORT);
|
||||
config |= I8042_CONFIG_IRQ1_ENABLE;
|
||||
outb(I8042_CMD_PORT, I8042_CMD_WRITE_CONFIG);
|
||||
outb(I8042_DATA_PORT, config);
|
||||
|
||||
/* Tell the keyboard device itself to (re)enable scanning. OVMF's own
|
||||
* PS/2 driver commonly sends 0xF5 (disable scanning) to the device as
|
||||
* part of its own teardown before handoff, the same way it stops
|
||||
* driving the PIT/PIC -- the controller's IRQ1-enable bit above only
|
||||
* governs the controller, not whether the device generates scancodes
|
||||
* at all. Bounded waits: one-time init handshake, not steady-state
|
||||
* polling. */
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < I8042_INIT_WAIT_ITERS; i++) {
|
||||
if (!(inb(I8042_STATUS_PORT) & I8042_STATUS_INPUT_FULL)) break;
|
||||
}
|
||||
outb(I8042_DATA_PORT, I8042_DEV_CMD_ENABLE_SCANNING);
|
||||
for (i = 0; i < I8042_INIT_WAIT_ITERS; i++) {
|
||||
if (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
|
||||
uint8_t resp = inb(I8042_DATA_PORT);
|
||||
if (resp == I8042_DEV_ACK) {
|
||||
console_println("i8042: keyboard ACKed enable-scanning");
|
||||
} else {
|
||||
console_println("i8042: keyboard responded (not ACK) to enable-scanning");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console_println("i8042: IRQ1 enabled in controller config byte");
|
||||
}
|
||||
|
||||
void i8042_drain_stale(void) {
|
||||
/* Edge-triggered IRQ1 + OBF already set at unmask time == no rising
|
||||
* edge ever again (the textbook "fires once, then never" pattern).
|
||||
* i8042_init()'s drain happens at M4; VM bootstrap runs for real wall
|
||||
* time between then and unmask, so re-drain immediately before. */
|
||||
while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
|
||||
(void)inb(I8042_DATA_PORT);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ring buffer: single producer (the ISR), single reader (i8042_pop_scancode(),
|
||||
* called from ordinary non-interrupt context, e.g. a diagnostic FORTH word).
|
||||
* 32 entries is far more than one QEMU test keypress burst can fill. */
|
||||
#define RING_SIZE 32
|
||||
static volatile uint8_t ring[RING_SIZE];
|
||||
static volatile uint8_t ring_head = 0; /* next slot the ISR writes */
|
||||
static volatile uint8_t ring_tail = 0; /* next slot the reader takes */
|
||||
|
||||
/* Count of real keyboard IRQs serviced since boot -- a plain counter, no
|
||||
* I/O, is cheap enough to keep in the ISR itself and doubles as the
|
||||
* quickest way to confirm the interrupt path is alive without touching
|
||||
* the ring buffer at all. */
|
||||
volatile uint32_t g_i8042_isr_count = 0;
|
||||
|
||||
void i8042_handle_irq(void) {
|
||||
/* Must always read 0x60, even to discard — otherwise OBF never clears
|
||||
* and the controller stops delivering further interrupts. */
|
||||
uint8_t scancode = inb(I8042_DATA_PORT);
|
||||
g_i8042_isr_count++;
|
||||
|
||||
uint8_t next_head = (uint8_t)((ring_head + 1) % RING_SIZE);
|
||||
if (next_head != ring_tail) { /* drop the byte if the ring is full */
|
||||
ring[ring_head] = scancode;
|
||||
ring_head = next_head;
|
||||
}
|
||||
}
|
||||
|
||||
int i8042_pop_scancode(uint8_t *out) {
|
||||
if (ring_tail == ring_head) return 0; /* empty */
|
||||
*out = ring[ring_tail];
|
||||
ring_tail = (uint8_t)((ring_tail + 1) % RING_SIZE);
|
||||
return 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* isr.S - Interrupt/exception stubs for amd64
|
||||
*
|
||||
* Stack on entry to ISR stub (after CPU pushes):
|
||||
* [rsp+0] = error code (if any, or pushed 0)
|
||||
* [rsp+8] = vector (pushed by stub)
|
||||
* [rsp+16] = RIP
|
||||
* [rsp+24] = CS
|
||||
* [rsp+32] = RFLAGS
|
||||
* [rsp+40] = RSP (if privilege change)
|
||||
* [rsp+48] = SS (if privilege change)
|
||||
*
|
||||
* For IRQs (vector >= 32), we return via IRETQ.
|
||||
* For exceptions (vector < 32), we halt.
|
||||
*/
|
||||
|
||||
.intel_syntax noprefix
|
||||
.section .text
|
||||
|
||||
/* Mark symbols referenced from C as hidden so the C compiler uses direct
|
||||
* RIP-relative addressing (LEA) rather than GOT indirection. Without this,
|
||||
* -fPIC code collapses the GOT reference to a direct MOV that reads the
|
||||
* symbol's *bytes* instead of its *address*, corrupting the IDT setup. */
|
||||
.hidden isr_stub_table
|
||||
.hidden isr_stub0
|
||||
.hidden isr_stub32
|
||||
.hidden isr_stub33
|
||||
.hidden isr_stub_default
|
||||
.hidden isr_common_entry
|
||||
|
||||
.macro ISR_NOERR num
|
||||
.global isr_stub\num
|
||||
isr_stub\num:
|
||||
push 0 /* fake error code */
|
||||
push \num /* vector number */
|
||||
jmp isr_common_entry
|
||||
.endm
|
||||
|
||||
.macro ISR_ERR num
|
||||
.global isr_stub\num
|
||||
isr_stub\num:
|
||||
/* error code already on stack from CPU */
|
||||
push \num /* vector number */
|
||||
jmp isr_common_entry
|
||||
.endm
|
||||
|
||||
.global isr_common_entry
|
||||
isr_common_entry:
|
||||
/* Save all general-purpose registers */
|
||||
push rax
|
||||
push rbx
|
||||
push rcx
|
||||
push rdx
|
||||
push rsi
|
||||
push rdi
|
||||
push rbp
|
||||
push r8
|
||||
push r9
|
||||
push r10
|
||||
push r11
|
||||
push r12
|
||||
push r13
|
||||
push r14
|
||||
push r15
|
||||
|
||||
/* Stack layout now:
|
||||
* [rsp+0] = r15
|
||||
* [rsp+8] = r14
|
||||
* ...
|
||||
* [rsp+112] = rax
|
||||
* [rsp+120] = vector
|
||||
* [rsp+128] = error
|
||||
* [rsp+136] = rip
|
||||
* [rsp+144] = cs
|
||||
* [rsp+152] = rflags
|
||||
*/
|
||||
|
||||
/* FABRIC-3.md SXXVIII (Stage 0, preemptive-context-switch trap-frame
|
||||
* parity, 2026-09-13): FXSAVE/FXRSTOR added here to close a real,
|
||||
* pre-existing gap, not just a future-preemption nicety -- confirmed
|
||||
* live double-precision FP code reachable from ordinary interpreter
|
||||
* dispatch (vm_runtime.c's Loop #5/#6 physics inference, called from
|
||||
* vm_tick() during word execution, not just at boot). The GPR pushes
|
||||
* above only cover integer state; on x86-64 SysV all XMM registers are
|
||||
* caller-saved, meaning the compiler is free to leave a live value in
|
||||
* one between any two instructions of straight-line code -- an
|
||||
* asynchronous interrupt is not a call site the interrupted code's own
|
||||
* instructions know about, so nothing here previously guaranteed FP
|
||||
* state actually survived a timer tick landing mid-computation.
|
||||
*
|
||||
* rbp is repurposed below as a fixed anchor pointing at "rsp right after
|
||||
* the 15 GPR pushes" (rbp's own true pushed value already lives safely
|
||||
* on the stack above and is restored by the ordinary `pop rbp` later) --
|
||||
* FXSAVE requires a 16-byte-aligned address and rsp's alignment here is
|
||||
* unpredictable (the interrupt could have landed anywhere), so the
|
||||
* FXSAVE area is carved out and aligned separately, with rbp used to
|
||||
* keep addressing everything else at its original, fixed offsets
|
||||
* regardless of that adjustment. */
|
||||
mov rbp, rsp
|
||||
sub rsp, 528 /* 512-byte FXSAVE area + 16 slack for alignment */
|
||||
and rsp, -16
|
||||
fxsave [rsp]
|
||||
|
||||
/* Set up arguments for isr_common_handler(vector, error, rip, cs, rflags, cr2) */
|
||||
mov rdi, [rbp + 120] /* vector */
|
||||
mov rsi, [rbp + 128] /* error */
|
||||
mov rdx, [rbp + 136] /* rip */
|
||||
mov rcx, [rbp + 144] /* cs */
|
||||
mov r8, [rbp + 152] /* rflags */
|
||||
mov r9, cr2 /* cr2 */
|
||||
|
||||
/* Pass stack frame pointer (after pushes) as 7th argument */
|
||||
push rbp
|
||||
call isr_common_handler
|
||||
add rsp, 8
|
||||
|
||||
fxrstor [rsp]
|
||||
mov rsp, rbp /* collapse back to right after the GPR pushes */
|
||||
|
||||
/* Restore general-purpose registers */
|
||||
pop r15
|
||||
pop r14
|
||||
pop r13
|
||||
pop r12
|
||||
pop r11
|
||||
pop r10
|
||||
pop r9
|
||||
pop r8
|
||||
pop rbp
|
||||
pop rdi
|
||||
pop rsi
|
||||
pop rdx
|
||||
pop rcx
|
||||
pop rbx
|
||||
pop rax
|
||||
|
||||
/* Remove vector and error code from stack */
|
||||
add rsp, 16
|
||||
|
||||
/* Return from interrupt */
|
||||
iretq
|
||||
|
||||
/* Exception/interrupt stubs (0-31 explicit) */
|
||||
ISR_NOERR 0
|
||||
ISR_NOERR 1
|
||||
ISR_NOERR 2
|
||||
ISR_NOERR 3
|
||||
ISR_NOERR 4
|
||||
ISR_NOERR 5
|
||||
ISR_NOERR 6
|
||||
ISR_NOERR 7
|
||||
ISR_ERR 8
|
||||
ISR_NOERR 9
|
||||
ISR_ERR 10
|
||||
ISR_ERR 11
|
||||
ISR_ERR 12
|
||||
ISR_ERR 13
|
||||
ISR_ERR 14
|
||||
ISR_NOERR 15
|
||||
ISR_NOERR 16
|
||||
ISR_ERR 17
|
||||
ISR_NOERR 18
|
||||
ISR_NOERR 19
|
||||
ISR_NOERR 20
|
||||
ISR_ERR 21
|
||||
ISR_NOERR 22
|
||||
ISR_NOERR 23
|
||||
ISR_NOERR 24
|
||||
ISR_NOERR 25
|
||||
ISR_NOERR 26
|
||||
ISR_NOERR 27
|
||||
ISR_NOERR 28
|
||||
ISR_NOERR 29
|
||||
ISR_ERR 30
|
||||
ISR_NOERR 31
|
||||
|
||||
/* Stub for APIC Timer (vector 0x20 = 32) */
|
||||
/* Diagnostic: write raw 'I' to COM1 (0x3F8) when stub is entered.
|
||||
* If 'I' never appears in the serial log, the stub is never reached
|
||||
* (bad IDT entry or fault during CPU interrupt delivery). */
|
||||
.global isr_stub32
|
||||
isr_stub32:
|
||||
push 0
|
||||
push 0x20
|
||||
jmp isr_common_entry
|
||||
|
||||
/* Stub for i8042 keyboard IRQ1, routed via I/O APIC to vector 0x21 = 33
|
||||
* (item 4.3.5, FABRIC-0.md §27.5). Before this stub existed, vector 33 fell
|
||||
* through to isr_stub_default below, which unconditionally reports "255"
|
||||
* regardless of which IDT slot actually fired -- the CPU legitimately took
|
||||
* IDT[33] (confirmed via the LAPIC's own ISR register), but isr_common_handler
|
||||
* only ever saw vector 255 and treated a real keyboard IRQ as spurious,
|
||||
* silently skipping EOI and leaving vector 33 permanently stuck in-service. */
|
||||
.global isr_stub33
|
||||
isr_stub33:
|
||||
push 0
|
||||
push 33
|
||||
jmp isr_common_entry
|
||||
|
||||
/* Default stub for other vectors (34-255) */
|
||||
.global isr_stub_default
|
||||
isr_stub_default:
|
||||
push 0 /* fake error */
|
||||
push 255 /* unknown vector, will be logged */
|
||||
jmp isr_common_entry
|
||||
|
||||
/* Use .data instead of .rodata so UEFI applies relocations */
|
||||
.section .data
|
||||
.align 8
|
||||
.global isr_stub_table
|
||||
isr_stub_table:
|
||||
.quad isr_stub0, isr_stub1, isr_stub2, isr_stub3
|
||||
.quad isr_stub4, isr_stub5, isr_stub6, isr_stub7
|
||||
.quad isr_stub8, isr_stub9, isr_stub10, isr_stub11
|
||||
.quad isr_stub12, isr_stub13, isr_stub14, isr_stub15
|
||||
.quad isr_stub16, isr_stub17, isr_stub18, isr_stub19
|
||||
.quad isr_stub20, isr_stub21, isr_stub22, isr_stub23
|
||||
.quad isr_stub24, isr_stub25, isr_stub26, isr_stub27
|
||||
.quad isr_stub28, isr_stub29, isr_stub30, isr_stub31
|
||||
/* Vector 32 = APIC Timer */
|
||||
.quad isr_stub32
|
||||
/* Vector 33 = i8042 keyboard IRQ1 (item 4.3.5) */
|
||||
.quad isr_stub33
|
||||
/* Vectors 34-255 default to unknown stub */
|
||||
.rept 222
|
||||
.quad isr_stub_default
|
||||
.endr
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* kernel_entry.S — kernel stack setup and entry trampoline (amd64)
|
||||
*
|
||||
* UEFI hands control to kernel_main() running on a small UEFI-provided stack
|
||||
* (typically 128 KB–256 KB). The FORTH interpreter + DOE experiment loop can
|
||||
* easily exceed that depth. This trampoline allocates a 2 MiB BSS stack and
|
||||
* switches to it before calling the real implementation (kernel_main_impl).
|
||||
*
|
||||
* 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: SysV AMD64 — boot_info pointer arrives in %rdi and is
|
||||
* passed unchanged to kernel_main_impl via the tail-call.
|
||||
*/
|
||||
|
||||
.intel_syntax noprefix
|
||||
|
||||
#include "starkernel/boot_info_offsets.h"
|
||||
|
||||
/* 2 MiB kernel stack in BSS — fallback when no dynamic stack requested */
|
||||
.section .bss
|
||||
.align 16
|
||||
.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.
|
||||
*
|
||||
* rdi = BootInfo* (SysV AMD64 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, minus 8 for ABI)
|
||||
* 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:
|
||||
mov rax, [rdi + BOOT_INFO_KERNEL_STACK_BASE_OFFSET]
|
||||
test rax, rax
|
||||
jnz .Ldynamic_stack
|
||||
|
||||
/* BSS fallback */
|
||||
lea rax, [rip + g_kernel_stack_top]
|
||||
jmp .Lstack_ready
|
||||
|
||||
.Ldynamic_stack:
|
||||
mov rcx, [rdi + BOOT_INFO_KERNEL_STACK_SIZE_OFFSET]
|
||||
add rax, rcx /* top = base + size (stack grows down) */
|
||||
|
||||
.Lstack_ready:
|
||||
and rax, -16 /* 16-byte alignment */
|
||||
sub rax, 8 /* SysV ABI: RSP ≡ 8 mod 16 at C call boundary */
|
||||
mov rsp, rax
|
||||
xor rbp, rbp /* terminate frame chain */
|
||||
jmp kernel_main_impl /* rdi (boot_info) already in place */
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* switch.S (amd64) - Cooperative VM context switch primitive.
|
||||
*
|
||||
* FABRIC-3.md §XXVIII (Stage 2, preemptive-context-switch save/restore
|
||||
* primitive, 2026-09-13). This is deliberately NOT the same mechanism as
|
||||
* isr.S's interrupt trap frame (Stage 0) -- that saves every register
|
||||
* because an interrupt is not a call site the interrupted code knows
|
||||
* about. sk_vm_switch_to() IS an ordinary function call (SWITCH-TO's C
|
||||
* handler calls it directly), so the standard System V AMD64 ABI already
|
||||
* guarantees every caller-saved register (rax, rcx, rdx, rsi, rdi, r8-r11,
|
||||
* all XMM) is the caller's own problem to preserve across the call, exactly
|
||||
* like any other call. Only the callee-saved set -- rbx, rbp, r12-r15 --
|
||||
* needs explicit handling here, because this function does not really
|
||||
* "return" in the normal sense: it swaps to a completely different stack
|
||||
* and (from the newly-resumed context's point of view) appears to return
|
||||
* from whichever earlier call to this same function originally parked it.
|
||||
* SysV has no callee-saved XMM registers at all, so there is no FP state
|
||||
* to carry here.
|
||||
*
|
||||
* void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
|
||||
* rdi = save_sp_out, rsi = new_sp
|
||||
*
|
||||
* For a VM that has never run before, new_sp must point at a synthetic
|
||||
* frame built to this exact layout by the C glue (switch.c): six zeroed
|
||||
* quadwords (r15, r14, r13, r12, rbx, rbp -- values irrelevant, never a
|
||||
* real prior context) followed by the trampoline entry address at
|
||||
* new_sp+48, so the final `ret` below jumps there instead of into some
|
||||
* prior caller.
|
||||
*/
|
||||
|
||||
.intel_syntax noprefix
|
||||
.section .text
|
||||
|
||||
.hidden sk_vm_switch_to
|
||||
|
||||
.global sk_vm_switch_to
|
||||
sk_vm_switch_to:
|
||||
push rbp
|
||||
push rbx
|
||||
push r12
|
||||
push r13
|
||||
push r14
|
||||
push r15
|
||||
|
||||
mov [rdi], rsp /* *save_sp_out = rsp (this context's parked SP) */
|
||||
mov rsp, rsi /* rsp = new_sp (switch stacks) */
|
||||
|
||||
pop r15
|
||||
pop r14
|
||||
pop r13
|
||||
pop r12
|
||||
pop rbx
|
||||
pop rbp
|
||||
ret
|
||||
|
||||
/*
|
||||
* uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
|
||||
*
|
||||
* Builds the synthetic frame described above at the top of a fresh native
|
||||
* stack, so the first ever sk_vm_switch_to() into it lands at `entry`
|
||||
* instead of some real prior caller. Returns the resulting sp value to
|
||||
* pass as sk_vm_switch_to()'s new_sp. Kept in this file (not C) so the
|
||||
* layout can never drift from what sk_vm_switch_to() above actually
|
||||
* expects -- one set of offsets, not two copies to keep in sync.
|
||||
*/
|
||||
.hidden sk_vm_switch_prime
|
||||
|
||||
.global sk_vm_switch_prime
|
||||
sk_vm_switch_prime:
|
||||
mov rax, rdi
|
||||
sub rax, 56
|
||||
mov qword ptr [rax+0], 0 /* r15 */
|
||||
mov qword ptr [rax+8], 0 /* r14 */
|
||||
mov qword ptr [rax+16], 0 /* r13 */
|
||||
mov qword ptr [rax+24], 0 /* r12 */
|
||||
mov qword ptr [rax+32], 0 /* rbx */
|
||||
mov qword ptr [rax+40], 0 /* rbp */
|
||||
mov [rax+48], rsi /* return address = entry */
|
||||
ret
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user