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:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+275
View File
@@ -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();
}