Files
LithosAnanake/kernel/src/heartbeat.c
T
rajamesandJunie a8b70e88d3 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>
2026-10-01 15:40:09 -04:00

263 lines
8.4 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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;
}