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>
276 lines
12 KiB
C
276 lines
12 KiB
C
/*
|
||
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();
|
||
}
|