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>
263 lines
8.4 KiB
C
263 lines
8.4 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
|
||
*/
|
||
|
||
/**
|
||
* heartbeat.c - Shared M5 heartbeat / TIME-TRUST engine (punch-list item 0.8)
|
||
*
|
||
* One implementation of heartbeat_init()/heartbeat_ticks()/heartbeat_trust()/
|
||
* heartbeat_state(), shared by all three architectures. Each architecture's
|
||
* timer.c contributes only heartbeat_read_counter() — the one thing that is
|
||
* genuinely per-ISA (rdtsc / rdtime / CNTPCT_EL0).
|
||
*
|
||
* Top half / bottom half split (FABRIC-0.md §25.1 item 0.8, per the GAP-A1
|
||
* ruling in §16.4/§18.4): heartbeat_tick() is called from interrupt context
|
||
* and does nothing but read the counter, bump TIME-TICKS, and latch a
|
||
* pending sample. heartbeat_service() runs on the mainline (the REPL idle
|
||
* loop) and does the window/variance/trust work. Neither one feeds patron
|
||
* state — the engine stays on the virtual tick per §18.4, unchanged by this
|
||
* file.
|
||
*
|
||
* Adaptive re-arm period (FABRIC-0.md §26, ruled 2026-08-03): Loop #7
|
||
* (vm_runtime.c) computes an execution-derived stable/volatile signal and
|
||
* calls heartbeat_set_adaptive_period_ns() with it, rescaled to this file's
|
||
* kernel-appropriate base (10 ms, matching the 100 Hz rate item 0.1-0.7
|
||
* configured) rather than the hosted 10 µs HEARTBEAT_TICK_NS base. Each
|
||
* architecture's re-arm function reads heartbeat_next_period_ns() and
|
||
* converts it to raw counter units instead of using a fixed constant.
|
||
* Single writer (mainline, via vm_tick()'s Loop #7 site), single reader
|
||
* (the ISR's re-arm call) — no lock needed, per §21.1's finding that
|
||
* nothing here is actually concurrent on one hart.
|
||
*/
|
||
|
||
#include "starkernel/timer.h"
|
||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
|
||
|
||
#if defined(ARCH_AMD64)
|
||
#define HEARTBEAT_HAS_VARIANCE 1
|
||
#else
|
||
#define HEARTBEAT_HAS_VARIANCE 0
|
||
#endif
|
||
|
||
/* Kernel-side adaptive-period base: 10 ms, matching the 100 Hz hardware
|
||
* rate established throughout items 0.1-0.7. Deliberately NOT
|
||
* HEARTBEAT_TICK_NS (include/starforth_config.h) -- that constant is the
|
||
* hosted pthread-worker's 10 µs base and is three orders of magnitude too
|
||
* fast for a bare-metal ISR period (FABRIC-0.md §26.3). Loop #7's decision
|
||
* logic is reused unmodified; only the base it scales differs. */
|
||
#define HEARTBEAT_BASE_PERIOD_NS 10000000ULL
|
||
|
||
static TimeTrustState g_heartbeat;
|
||
|
||
/* Top/bottom half handoff. */
|
||
static volatile uint64_t g_pending_counter;
|
||
static volatile int g_pending_valid;
|
||
|
||
/* Adaptive re-arm period, clamped to the same [1/4x, 4x] band Loop #7
|
||
* itself enforces on tick_target_ns, so a caller cannot runaway the ISR
|
||
* rate even if it forwarded an unclamped value. */
|
||
static volatile uint64_t g_adaptive_period_ns = HEARTBEAT_BASE_PERIOD_NS;
|
||
|
||
#if HEARTBEAT_HAS_VARIANCE
|
||
|
||
/**
|
||
* @brief Push a signed delta value into the heartbeat rolling window.
|
||
*/
|
||
static void window_push(TimeWindow *w, int64_t delta)
|
||
{
|
||
w->deltas[w->pos] = delta;
|
||
w->pos = (w->pos + 1) % TIME_WINDOW_SIZE;
|
||
if (w->count < TIME_WINDOW_SIZE) {
|
||
w->count++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Compute the relative variance of heartbeat deltas as a Q48.16 value.
|
||
*/
|
||
static q48_16_t window_variance_q48(const TimeWindow *w, uint64_t expected_delta)
|
||
{
|
||
if (w->count < 2 || expected_delta == 0) {
|
||
return 0;
|
||
}
|
||
|
||
int64_t sum = 0;
|
||
for (uint32_t i = 0; i < w->count; i++) {
|
||
sum += w->deltas[i];
|
||
}
|
||
int64_t mean = sum / (int64_t)w->count;
|
||
|
||
uint64_t sum_sq = 0;
|
||
for (uint32_t i = 0; i < w->count; i++) {
|
||
int64_t diff = w->deltas[i] - mean;
|
||
if (diff > 0x7FFFFFFF) diff = 0x7FFFFFFF;
|
||
if (diff < -0x7FFFFFFF) diff = -0x7FFFFFFF;
|
||
sum_sq += (uint64_t)(diff * diff);
|
||
}
|
||
uint64_t var_tsc = sum_sq / w->count;
|
||
|
||
uint64_t exp_sq = expected_delta;
|
||
if (exp_sq > 0xFFFFFFFF) {
|
||
var_tsc >>= 16;
|
||
exp_sq >>= 8;
|
||
}
|
||
exp_sq = exp_sq * exp_sq;
|
||
if (exp_sq == 0) return 0;
|
||
|
||
if (var_tsc > 0x0000FFFFFFFFFFFFULL) {
|
||
var_tsc = 0x0000FFFFFFFFFFFFULL;
|
||
}
|
||
|
||
return (var_tsc << 16) / exp_sq;
|
||
}
|
||
|
||
/**
|
||
* @brief Derive TIME-TRUST from a Q48.16 relative variance value.
|
||
*/
|
||
static q48_16_t variance_to_trust(q48_16_t variance)
|
||
{
|
||
q48_16_t denom = q48_add(Q48_ONE, variance);
|
||
if (denom == 0) {
|
||
return Q48_ONE;
|
||
}
|
||
return q48_div(Q48_ONE, denom);
|
||
}
|
||
|
||
#endif /* HEARTBEAT_HAS_VARIANCE */
|
||
|
||
/**
|
||
* @brief Initialise the M5 heartbeat / TIME-TRUST subsystem.
|
||
*
|
||
* Shared across all three architectures -- the fallback and calibration
|
||
* formula never differed between them, only what heartbeat_service() does
|
||
* with the resulting window did.
|
||
*/
|
||
void heartbeat_init(uint64_t tsc_hz, uint64_t tick_hz)
|
||
{
|
||
g_heartbeat.ticks = 0;
|
||
g_heartbeat.last_tsc = 0;
|
||
g_heartbeat.total_samples = 0;
|
||
g_heartbeat.variance = 0;
|
||
g_heartbeat.trust = Q48_ONE;
|
||
|
||
g_heartbeat.window.pos = 0;
|
||
g_heartbeat.window.count = 0;
|
||
for (int i = 0; i < TIME_WINDOW_SIZE; i++) {
|
||
g_heartbeat.window.deltas[i] = 0;
|
||
}
|
||
|
||
g_heartbeat.expected_delta = (tick_hz > 0 && tsc_hz > 0)
|
||
? (tsc_hz / tick_hz) : 10000000ULL;
|
||
|
||
g_pending_valid = 0;
|
||
g_adaptive_period_ns = HEARTBEAT_BASE_PERIOD_NS;
|
||
}
|
||
|
||
void heartbeat_tick(void)
|
||
{
|
||
g_pending_counter = heartbeat_read_counter();
|
||
g_heartbeat.ticks++;
|
||
g_pending_valid = 1;
|
||
|
||
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): the one deliberate,
|
||
* documented amendment to this file's own top-half/bottom-half
|
||
* discipline above. sk_vm_switch_signal_tick() reaches into VM
|
||
* scheduling state from real ISR context -- something this file's
|
||
* header comment previously said nothing here did. It stays cheap
|
||
* (bounded iteration over a small slot table, no locking needed for
|
||
* the same reason nothing else here needs it: single hart, nothing
|
||
* concurrent with this ISR while it runs) and it never itself
|
||
* switches anything -- it only ever sets a flag, consumed later at a
|
||
* safe cooperative checkpoint on the mainline
|
||
* (execute_colon_word()). The GAP-A1 split itself is not overturned:
|
||
* everything else in this function is exactly as minimal as before. */
|
||
sk_vm_switch_signal_tick();
|
||
}
|
||
|
||
void heartbeat_service(void)
|
||
{
|
||
if (!g_pending_valid) {
|
||
return;
|
||
}
|
||
uint64_t now = g_pending_counter;
|
||
g_pending_valid = 0;
|
||
|
||
TimeTrustState *s = &g_heartbeat;
|
||
s->total_samples++;
|
||
|
||
if (s->total_samples == 1) {
|
||
s->last_tsc = now;
|
||
return;
|
||
}
|
||
|
||
uint64_t actual_delta = now - s->last_tsc;
|
||
s->last_tsc = now;
|
||
int64_t deviation = (int64_t)actual_delta - (int64_t)s->expected_delta;
|
||
|
||
#if HEARTBEAT_HAS_VARIANCE
|
||
window_push(&s->window, deviation);
|
||
s->variance = window_variance_q48(&s->window, s->expected_delta);
|
||
s->trust = variance_to_trust(s->variance);
|
||
#else
|
||
/* RISC-V `time` / AArch64 CNTPCT_EL0 are architecturally invariant
|
||
* counters -- no statistical quality estimate needed (matches the
|
||
* per-arch rationale this file replaces). */
|
||
(void)deviation;
|
||
s->trust = Q48_ONE;
|
||
#endif
|
||
}
|
||
|
||
uint64_t heartbeat_ticks(void)
|
||
{
|
||
return g_heartbeat.ticks;
|
||
}
|
||
|
||
time_trust_t heartbeat_trust(void)
|
||
{
|
||
return g_heartbeat.trust;
|
||
}
|
||
|
||
const TimeTrustState *heartbeat_state(void)
|
||
{
|
||
return &g_heartbeat;
|
||
}
|
||
|
||
/**
|
||
* @brief Set the adaptive re-arm period (FABRIC-0.md §26).
|
||
*
|
||
* Called from vm_runtime.c's Loop #7 site on the mainline execution path
|
||
* (never interrupt context) with a value already rescaled to this file's
|
||
* HEARTBEAT_BASE_PERIOD_NS. Clamped defensively to the same [1/4x, 4x]
|
||
* band Loop #7 itself enforces around its own base, so a caller forwarding
|
||
* an unclamped or wrongly-scaled value cannot run the ISR away.
|
||
*
|
||
* @param ns Desired period in nanoseconds for the next re-arm.
|
||
*/
|
||
void heartbeat_set_adaptive_period_ns(uint64_t ns)
|
||
{
|
||
uint64_t lo = HEARTBEAT_BASE_PERIOD_NS / 4;
|
||
uint64_t hi = HEARTBEAT_BASE_PERIOD_NS * 4;
|
||
if (ns < lo) ns = lo;
|
||
if (ns > hi) ns = hi;
|
||
g_adaptive_period_ns = ns;
|
||
}
|
||
|
||
/**
|
||
* @brief Read the period the next hardware re-arm should use.
|
||
*
|
||
* Called from interrupt context by each architecture's re-arm function
|
||
* (apic_timer_rearm(), riscv64_timer_rearm(), the AArch64 equivalent) in
|
||
* place of the fixed constant those functions used before item 0.8.
|
||
*
|
||
* @return Current adaptive period in nanoseconds.
|
||
*/
|
||
uint64_t heartbeat_next_period_ns(void)
|
||
{
|
||
return g_adaptive_period_ns;
|
||
}
|