/* 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. */ /** * timer.c - Timer policy + TSC calibration (amd64) * * The kernel will always run with the best time source available, but will only * make determinism claims when the hardware contract is provably satisfied. * * Policy split: * - VM path: avoid HPET MMIO. Prefer invariant TSC for ABSOLUTE time. Derive TSC Hz * from CPUID leaves (0x15/0x16) when available; otherwise fall back to ACPI PM Timer * (port I/O, stable under QEMU/KVM) to obtain TSC Hz. If invariant TSC is absent, * kernel continues in RELATIVE time mode (PM Timer-based), unless strict mode is enabled. * - Bare metal path: calibrate TSC using HPET as primary reference, with optional PIT * cross-check. Fail hard on non-convergence. * * Failure mode: PANIC/HALT when invariants are not met. * * Strict VM policy: * -DTIMER_VM_STRICT_INVARIANT_TSC=1 enforces invariant TSC in VM mode (fail-hard). */ #include #include "timer.h" #include "console.h" #include "vmm.h" /* HPET registers */ #define HPET_GEN_CAP_ID 0x000 #define HPET_GEN_CONFIG 0x010 #define HPET_MAIN_COUNTER 0x0F0 #define HPET_PHYS_BASE 0xFED00000ull #define HPET_VIRT_BASE 0xFED00000ull /* identity-mapped in this setup */ /* PIT (bare metal cross-check only; optional) */ #define PIT_CHANNEL0_DATA 0x40 #define PIT_COMMAND 0x43 #define PIT_FREQ_HZ 1193182u /* PM Timer (VM-friendly reference; 3.579545 MHz; 24-bit typical). * The I/O port is machine-type-dependent (0x408 on legacy PIIX4/i440fx, * 0x608 on Q35's ICH9 LPC) and is looked up from the ACPI FADT at runtime * by fadt_find_pm_tmr_port() in timer_init(); this is only the last-resort * fallback if FADT parsing fails. Hardcoding a single port here silently * reads an unmapped port (returns all-ones) on any machine type that * doesn't match, freezing every RELATIVE-mode timestamp at a constant * value -- found via VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev h. */ #define PMTIMER_IO_PORT_DEFAULT 0x408 static uint16_t pmtimer_port = PMTIMER_IO_PORT_DEFAULT; #define PMTIMER_FREQ_HZ 3579545ull #define PMTIMER_WIDTH_BITS 24 #define PMTIMER_MASK ((1ull << PMTIMER_WIDTH_BITS) - 1ull) /* Convergence parameters (bare metal path) */ #define WARMUP_WINDOWS 2 #define SAMPLE_WINDOWS 8 #define WINDOW_TICKS_HPET 10000ull /* ~1ms at 10MHz nominal */ #define WINDOW_TICKS_PIT 4000u /* ~3.3ms nominal */ #define CV_EPSILON_PPM 1000ull /* 0.1% */ #define CV_STABLE_COUNT 6 #define DRIFT_EPSILON_PPM 2000ull #ifndef TIMER_REQUIRE_PIT #define TIMER_REQUIRE_PIT 0 #endif #ifndef TIMER_VM_STRICT_INVARIANT_TSC #define TIMER_VM_STRICT_INVARIANT_TSC 0 #endif static volatile uint64_t *hpet_regs = 0; static uint64_t hpet_freq_hz = 0; static uint64_t tsc_hz_locked = 0; static uint64_t pit_tsc_hz_mean = 0; static uint32_t windows_used = 0; static timer_calibration_record_t calib_record = {0}; static int pit_available = 0; /* VM RELATIVE time base (PM Timer anchored) */ static uint32_t vm_pm_start = 0; static uint64_t vm_ns_base = 0; /* ------------ tiny utils (serial-friendly) ------------ */ /** * @brief Compute @c (a × b) / c using 128-bit intermediate precision. * * Uses the x86-64 @c MUL instruction to produce a 128-bit product in * @c RDX:RAX, then @c DIV to produce a 64-bit quotient. This avoids the * overflow that would occur with a 64-bit-only intermediate when @p a and * @p b are both large (e.g., nanosecond conversion of multi-GHz tick counts). * No libgcc dependency — pure inline assembly. * * FABRIC.md item 4.5d, 2026-08-11: this asm previously declared @c RDX as a * plain output (@c "=d"(hi)), which tells GCC only "I want to read RDX's * value after this block" — nothing told it that @c mulq writes RDX *before* * @c divq needs to read a *different* value (the divisor @c c) out of it. * Nothing stopped the register allocator from placing @c c itself in RDX, * which @c mulq then overwrote with the product's high 64 bits before * @c divq ever read it. At @c -O0 this happened not to manifest; at @c -O2, * GCC's more aggressive allocation actually did it, and for a product like * `1000 * 1000000000` (fits entirely in the low 64 bits, so mulq's * high-word output is 0), that corrupted the intended divisor to 0 -- a * genuine #DE (divide error) confirmed via QEMU `-d int` tracing, cascading * to a double fault and then a triple fault (silently exited by * `-no-reboot`, which is why this looked like an infinite hang rather than * a crash at first). A `unsigned __int128` rewrite was tried first but * needs libgcc's `__udivti3` for the general 128÷64 DIVISION case, * undefined in this freestanding, `-nostdlib` build — not viable for that * reason specifically. (Confirmed narrower during the Ed25519 crypto work, * 2026-08-22: `__int128` multiply/add/shift-by-constant DO compile cleanly * with zero undefined symbols on all three target toolchains — it's only * division, needing a multi-instruction libgcc routine the compiler won't * inline, that's unavailable here. Don't read this comment as "avoid * __int128 entirely" — src/starkernel/crypto/fe25519.c uses it safely for * exactly that reason.) Fixed instead the way the * Linux kernel's own `mul_u64_u64_div_u64` does it: declare `rdx` as a pure * *clobber*, not an output. A clobber tells GCC the register is used * internally by the whole asm block and must never be allocated to any * input or output operand for its duration — exactly the guarantee the * previous constraint list was missing. * * Undefined behaviour if @c a × b / c overflows @c uint64_t (i.e., the * true quotient exceeds @c UINT64_MAX). Callers must ensure this cannot * happen with their inputs. * * @param a Multiplicand. * @param b Multiplier. * @param c Divisor (must be non-zero; division by zero causes a #DE fault). * @return Exact quotient @c (a * b) / c, truncated toward zero. */ static inline uint64_t muldiv64(uint64_t a, uint64_t b, uint64_t c) { uint64_t result; __asm__ volatile ( "mulq %2\n\t" /* RDX:RAX = a * b */ "divq %3\n\t" /* RAX = RDX:RAX / c, RDX = remainder (discarded) */ : "=a"(result) : "a"(a), "rm"(b), "rm"(c) : "rdx", "cc" ); return result; } /** * @brief Print a @c uint64_t value as decimal digits to the kernel console. * * Uses a local reverse-digit buffer and @c console_putc(); no libc or * heap allocation required. Prints @c "0" for a zero value. Used throughout * the timer subsystem to emit calibration results without depending on * @c printf(). * * @param val Value to print in decimal. */ static void print_dec(uint64_t val) { char buf[32]; int i = 0; if (val == 0) { console_putc('0'); return; } while (val > 0 && i < (int)sizeof(buf)) { buf[i++] = (char)('0' + (val % 10)); val /= 10; } while (i-- > 0) { console_putc(buf[i]); } } /** * @brief Read the x86-64 Time Stamp Counter without serialisation (RDTSC). * * Executes the @c RDTSC instruction and combines @c EDX:EAX into a 64-bit * result. May be reordered relative to surrounding instructions by the * out-of-order execution unit; use @c rdtscp_aux() where ordering matters. * * @return Current TSC value in CPU cycles. */ static inline uint64_t rdtsc(void) { uint32_t lo, hi; __asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi)); return ((uint64_t)hi << 32) | lo; } /** * @brief Read TSC and processor ID with weak serialisation (RDTSCP). * * @c RDTSCP is weakly serialising: all prior loads are complete before the * read, but it does not wait for prior stores. Optionally captures the * @c IA32_TSC_AUX MSR value (processor ID / NUMA node) in @p aux. Falls * back to @c rdtsc() at call sites that check @c cpu_has_rdtscp() first. * * @param aux If non-NULL, receives the @c ECX value from @c RDTSCP * (processor/socket ID programmed by the OS or hypervisor). * @return Current TSC value in CPU cycles. */ static inline uint64_t rdtscp_aux(uint32_t *aux) { uint32_t lo, hi, a; __asm__ volatile ("rdtscp" : "=a"(lo), "=d"(hi), "=c"(a) ::); if (aux) *aux = a; return ((uint64_t)hi << 32) | lo; } /** * @brief Execute the x86-64 @c CPUID instruction and capture all output registers. * * Passes @p leaf in @c EAX and @p subleaf in @c ECX before executing @c CPUID. * Any of the output pointers may be @c NULL; the corresponding register value * is then discarded. * * @param leaf CPUID function leaf (EAX input). * @param subleaf CPUID sub-leaf (ECX input, for structured extended leaves). * @param a If non-NULL, receives @c EAX on return. * @param b If non-NULL, receives @c EBX on return. * @param c If non-NULL, receives @c ECX on return. * @param d If non-NULL, receives @c EDX on return. */ static inline void cpuid(uint32_t leaf, uint32_t subleaf, uint32_t *a, uint32_t *b, uint32_t *c, uint32_t *d) { uint32_t eax, ebx, ecx, edx; __asm__ volatile ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(leaf), "c"(subleaf)); if (a) *a = eax; if (b) *b = ebx; if (c) *c = ecx; if (d) *d = edx; } /** * @brief Write a byte to an x86 I/O port (OUT instruction). * * @param port I/O port address. * @param val Byte value to write. */ static inline void outb(uint16_t port, uint8_t val) { __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port)); } /** * @brief Read a byte from an x86 I/O port (IN instruction). * * @param port I/O port address. * @return Byte value read from the port. */ static inline uint8_t inb(uint16_t port) { uint8_t ret; __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port)); return ret; } /** * @brief Read a 32-bit dword from an x86 I/O port (INL instruction). * * Used to read the ACPI PM Timer register which is a 24-bit counter * accessed as a 32-bit I/O port. * * @param port I/O port address. * @return 32-bit value read from the port. */ static inline uint32_t inl(uint16_t port) { uint32_t ret; __asm__ volatile ("inl %1, %0" : "=a"(ret) : "Nd"(port)); return ret; } /** * @brief Read a 64-bit HPET register at @p offset bytes from the HPET base. * * Accesses @c hpet_regs[] (the identity-mapped MMIO region) using a * naturally-aligned 64-bit read. @p offset must be a multiple of 8. * * @param offset Byte offset of the target register (e.g., @c HPET_MAIN_COUNTER = 0x0F0). * @return 64-bit register value. */ static inline uint64_t hpet_read(uint32_t offset) { return hpet_regs[offset / 8]; } /** * @brief Write a 64-bit value to an HPET register at @p offset bytes from base. * * @param offset Byte offset of the target register (must be a multiple of 8). * @param val 64-bit value to write. */ static inline void hpet_write(uint32_t offset, uint64_t val) { hpet_regs[offset / 8] = val; } /** * @brief Emit a fatal timer error message and halt the processor permanently. * * Prints @p msg followed by @c "Timer subsystem fatal. Halting." to the * kernel console, then enters an infinite @c HLT loop. Never returns. * Called when a hard timer invariant cannot be satisfied (e.g., HPET * counter not advancing, TSC calibration failed to converge). * * @param msg Null-terminated error description to print before halting. */ static void timer_fatal(const char *msg) { console_println(msg); console_println("Timer subsystem fatal. Halting."); while (1) { __asm__ volatile ("hlt"); } } /* ------------ CPU capability / environment detection ------------ */ /** * @brief Test whether the processor supports Invariant TSC (non-stop TSC). * * Reads @c CPUID.80000007H:EDX[8]. Invariant TSC means the TSC counts at a * constant rate regardless of CPU frequency scaling or sleep states — a * requirement for using RDTSC as a reliable nanosecond source. * * @return 1 if invariant TSC is supported, 0 otherwise. */ static int cpu_has_invariant_tsc(void) { /* CPUID.80000007H:EDX[8] = Invariant TSC */ uint32_t a, b, c, d; cpuid(0x80000007u, 0, &a, &b, &c, &d); return (d & (1u << 8)) ? 1 : 0; } /** * @brief Test whether the processor supports the RDTSCP instruction. * * Reads @c CPUID.80000001H:EDX[27]. RDTSCP provides weak serialisation and * simultaneously reads both the TSC and the @c IA32_TSC_AUX MSR (which the * OS programs to hold a per-CPU/socket identifier). Used in @c timer_now_ns() * to prefer RDTSCP over the non-serialising RDTSC when available. * * @return 1 if RDTSCP is supported, 0 otherwise. */ static int cpu_has_rdtscp(void) { /* CPUID.80000001H:EDX[27] = RDTSCP */ uint32_t a, b, c, d; cpuid(0x80000001u, 0, &a, &b, &c, &d); return (d & (1u << 27)) ? 1 : 0; } /** * @brief Detect whether the kernel is running under a hypervisor. * * Reads @c CPUID.1:ECX[31] (Hypervisor Present bit). When set, the kernel * switches to VM mode: HPET MMIO calibration is disabled (VM-exits would * poison timing), and TSC frequency is derived from CPUID leaves 0x15/0x16 * or the ACPI PM Timer instead. * * @return 1 if a hypervisor is present (including QEMU/TCG), 0 on bare metal. */ static int running_under_hypervisor(void) { /* CPUID.1:ECX[31] = Hypervisor present */ uint32_t a, b, c, d; cpuid(1u, 0, &a, &b, &c, &d); return (c & (1u << 31)) ? 1 : 0; } /* ------------ PM Timer port discovery (ACPI FADT) ------------ */ /* Minimal ACPI structures needed to locate PM_TMR_BLK. Mirrors the * RSDP/XSDT walk in pci.c's parse_mcfg(), independently duplicated here * (kernel has no shared ACPI header) rather than touching already-working * PCI enumeration code. */ typedef struct { uint8_t signature[8]; uint8_t checksum; uint8_t oem_id[6]; uint8_t revision; uint32_t rsdt_address; uint32_t length; uint64_t xsdt_address; uint8_t extended_checksum; uint8_t reserved[3]; } __attribute__((packed)) TimerRsdp2; typedef struct { uint8_t signature[4]; uint32_t length; uint8_t revision; uint8_t checksum; uint8_t oem_id[6]; uint8_t oem_table_id[8]; uint32_t oem_revision; uint32_t creator_id; uint32_t creator_revision; } __attribute__((packed)) TimerAcpiHeader; /* Generic Address Structure (ACPI 2.0+). address_space_id: 0=SystemMemory, * 1=SystemIO. */ typedef struct { uint8_t address_space_id; uint8_t register_bit_width; uint8_t register_bit_offset; uint8_t access_size; uint64_t address; } __attribute__((packed)) TimerAcpiGas; static int timer_sig4_eq(const uint8_t *p, const char *s) { return p[0] == (uint8_t)s[0] && p[1] == (uint8_t)s[1] && p[2] == (uint8_t)s[2] && p[3] == (uint8_t)s[3]; } static int timer_sig8_eq(const uint8_t *p, const char *s) { int i; for (i = 0; i < 8; i++) if (p[i] != (uint8_t)s[i]) return 0; return 1; } /** * @brief Look up the ACPI PM_TMR_BLK I/O port from the FADT ("FACP" table). * * Walks RSDP -> XSDT -> FADT exactly like pci.c's MCFG lookup. Prefers the * ACPI 2.0+ X_PM_TMR_BLK Generic Address Structure (offset 208) when it * names a valid SystemIO address; falls back to the legacy 32-bit * PM_TMR_BLK field (offset 76) otherwise. This port is machine-type * dependent -- QEMU's i440fx puts it at 0x408, Q35's ICH9 LPC at 0x608 -- * so it must be discovered rather than assumed. * * @param rsdp BootInfo->acpi_table pointer from the UEFI configuration * table (ACPI 2.0 GUID), or NULL. * @return The discovered I/O port, or 0 if the FADT/XSDT/RSDP could not be * parsed (caller should keep its hardcoded fallback in that case). */ static uint16_t fadt_find_pm_tmr_port(void *rsdp) { if (!rsdp) return 0; TimerRsdp2 *r = (TimerRsdp2 *)rsdp; if (!timer_sig8_eq(r->signature, "RSD PTR ")) return 0; if (r->revision < 2 || r->xsdt_address == 0) return 0; TimerAcpiHeader *xsdt = (TimerAcpiHeader *)(uintptr_t)r->xsdt_address; if (!timer_sig4_eq(xsdt->signature, "XSDT")) return 0; uint32_t hdr_len = xsdt->length; if (hdr_len <= 36u) return 0; uint32_t n_entries = (hdr_len - 36u) / 8u; uint64_t *entries = (uint64_t *)((uint8_t *)xsdt + 36u); uint32_t i; for (i = 0; i < n_entries; i++) { TimerAcpiHeader *sdt = (TimerAcpiHeader *)(uintptr_t)entries[i]; if (!timer_sig4_eq(sdt->signature, "FACP")) continue; uint8_t *fadt = (uint8_t *)sdt; uint32_t fadt_len = sdt->length; /* Prefer X_PM_TMR_BLK (GAS at offset 208) if present and SystemIO. */ if (fadt_len >= 220u) { TimerAcpiGas *x_pm_tmr = (TimerAcpiGas *)(fadt + 208); if (x_pm_tmr->address_space_id == 1 /* SystemIO */ && x_pm_tmr->address != 0 && x_pm_tmr->address <= 0xFFFFu) { return (uint16_t)x_pm_tmr->address; } } /* Fall back to the legacy 32-bit PM_TMR_BLK field (offset 76). */ if (fadt_len >= 80u) { uint32_t legacy_port; /* Unaligned read: FADT is a packed byte blob, offset 76 has no * alignment guarantee. */ legacy_port = (uint32_t)fadt[76] | ((uint32_t)fadt[77] << 8) | ((uint32_t)fadt[78] << 16) | ((uint32_t)fadt[79] << 24); if (legacy_port != 0 && legacy_port <= 0xFFFFu) { return (uint16_t)legacy_port; } } return 0; } return 0; } /* ------------ PM Timer (VM-friendly reference) ------------ */ /** * @brief Read the raw ACPI PM Timer counter from I/O port @c pmtimer_port. * * The PM Timer runs at 3.579545 MHz and is a 24-bit counter (bits [23:0]) * in most systems. The returned 32-bit value may contain non-zero bits * above bit 23 on 32-bit PM Timer implementations; callers must mask with * @c PMTIMER_MASK before use. * * @return Raw 32-bit PM Timer register value. */ static inline uint32_t pmtimer_read(void) { return inl(pmtimer_port); } /** * @brief Compute elapsed PM Timer ticks from @p start to @p cur, handling wrap-around. * * Masks both values to @c PMTIMER_WIDTH_BITS (24 bits) before comparison. * When the counter wraps — i.e., @p cur < @p start after masking — the * function returns the distance through the wrap: @c (MASK - start + 1 + cur). * * @param start PM Timer value at the beginning of the interval (unmasked). * @param cur PM Timer value at the end of the interval (unmasked). * @return Number of PM Timer ticks elapsed, accounting for a single wrap. */ static uint64_t pmtimer_delta(uint32_t start, uint32_t cur) { uint32_t s = start & (uint32_t)PMTIMER_MASK; uint32_t c = cur & (uint32_t)PMTIMER_MASK; if (c >= s) return (uint64_t)(c - s); return (uint64_t)((PMTIMER_MASK - s) + 1u + c); } /** * @brief Calibrate TSC frequency using the ACPI PM Timer as reference. * * Waits for 1000 PM Timer ticks (~279 µs at 3.58 MHz), measures the TSC * delta over the same interval, then converts to Hz using @c muldiv64(). * Includes a 5-million-iteration timeout to avoid hanging under TCG or * broken PM Timer implementations. * * Used by the VM path when CPUID leaves 0x15/0x16 do not provide a TSC * frequency. The PM Timer is port I/O-based and does not cause VM-exits on * most hypervisors (unlike HPET MMIO), making it suitable for VM calibration. * * @return TSC frequency in Hz, or 0 if calibration timed out or produced an * invalid result. */ static uint64_t calibrate_tsc_with_pmtimer(void) { /* Use a small delta to avoid long waits under TCG */ const uint32_t target_ticks = 1000u; /* ~279us at 3.58MHz */ const uint64_t timeout_iter = 5000000ull; uint32_t start = pmtimer_read() & (uint32_t)PMTIMER_MASK; uint64_t start_tsc = rdtsc(); uint64_t iters = 0; while (1) { uint32_t cur = pmtimer_read() & (uint32_t)PMTIMER_MASK; uint64_t delta = pmtimer_delta(start, cur); if (delta >= target_ticks) { uint64_t end_tsc = rdtsc(); uint64_t elapsed_ticks = delta; uint64_t elapsed_ns = muldiv64(elapsed_ticks, 1000000000ull, PMTIMER_FREQ_HZ); if (elapsed_ns == 0) return 0; /* FABRIC.md item 4.5d, 2026-08-11: this file's own comments already * flag TSC non-monotonicity as a real risk under TCG ("invariant * TSC not present under hypervisor... no determinism guarantees"). * If end_tsc < start_tsc, this subtraction wraps to a huge unsigned * value; muldiv64()'s own contract is explicit that an overflowing * quotient is caller-UB, and DIVQ raises #DE on overflow -- exactly * the divide-error / double-fault / triple-fault cascade found by * QEMU '-d int' tracing an -O2 boot stall that turned out not to be * a hang at all. Treat non-monotonic TSC as a calibration failure, * the same as the elapsed_ns==0 case just above, instead of feeding * an underflowed value into the division. */ if (end_tsc < start_tsc) return 0; uint64_t delta_tsc = end_tsc - start_tsc; return muldiv64(delta_tsc, 1000000000ull, elapsed_ns); } if (++iters > timeout_iter) { return 0; } } } /** * @brief Initialise the VM relative-time base anchored to the PM Timer. * * Records the current PM Timer counter value in @c vm_pm_start and resets * @c vm_ns_base to zero. Subsequent calls to @c timer_now_ns_vm_relative() * compute elapsed nanoseconds from this anchor point. * * Called during @c init_timer_vm_mode() after the calibration phase, so the * first call to @c timer_now_ns() returns a small positive value rather than * a large absolute-time offset. */ static void vm_timebase_init_from_pmtimer(void) { vm_pm_start = pmtimer_read() & (uint32_t)PMTIMER_MASK; vm_ns_base = 0; } /* ------------ HPET helpers (bare metal path) ------------ */ /** * @brief Enable the HPET main counter by setting the ENABLE_CNF bit. * * Reads the @c HPET_GEN_CONFIG register, sets bit 0 (ENABLE_CNF), and * writes it back. Must be called before the main counter is used for * calibration. The HPET must be identity-mapped at @c HPET_VIRT_BASE * before this function is called. */ static void enable_hpet(void) { uint64_t cfg = hpet_read(HPET_GEN_CONFIG); cfg |= 1ull; hpet_write(HPET_GEN_CONFIG, cfg); } /** * @brief Verify the HPET main counter is advancing (quick sanity check). * * Reads the @c HPET_MAIN_COUNTER repeatedly up to 200,000 times. If the * counter value changes at least once, the check passes silently. If the * counter never changes — indicating a hardware fault, frozen HPET, or * mapping error — calls @c timer_fatal() which halts the processor. * * Only used on the bare-metal path; skipped in VM mode. */ static void sanity_check_hpet_running_quick(void) { uint64_t start = hpet_read(HPET_MAIN_COUNTER); for (int i = 0; i < 200000; ++i) { if (hpet_read(HPET_MAIN_COUNTER) != start) { return; } } timer_fatal("HPET counter not advancing."); } /* ------------ PIT helpers (bare metal only) ------------ */ /** * @brief Programme the 8253/8254 PIT Channel 0 with a 16-bit reload value. * * Issues the mode command byte (0x34 = channel 0, lo/hi access, mode 2 * rate generator) followed by the low and high bytes of @p reload to the * PIT data port. After this call the PIT downcounts at @c PIT_FREQ_HZ * (1193182 Hz) and reloads automatically. * * @param reload 16-bit initial counter value (higher = longer period). */ static void pit_program_reload(uint16_t reload) { outb(PIT_COMMAND, 0x34); outb(PIT_CHANNEL0_DATA, (uint8_t)(reload & 0xFF)); outb(PIT_CHANNEL0_DATA, (uint8_t)((reload >> 8) & 0xFF)); } /** * @brief Read the current 16-bit counter value from PIT Channel 0. * * Sends a latch command (0x00) to the PIT command port to atomically snap * the current count, then reads the low and high bytes from the Channel 0 * data port in two @c INB instructions. Returns the counter as a 16-bit * value; note the PIT counter counts down from the reload value. * * @return Current PIT Channel 0 counter value (decreasing, wraps at 0). */ static uint16_t pit_read_counter16(void) { outb(PIT_COMMAND, 0x00); uint8_t lo = inb(PIT_CHANNEL0_DATA); uint8_t hi = inb(PIT_CHANNEL0_DATA); return (uint16_t)((hi << 8) | lo); } /** * @brief Probe whether the PIT is available and counting. * * Programs the PIT with reload value 0xFFFF and polls up to 20,000 times * for a counter change. Returns 1 if the counter advances (PIT running), * 0 if it never changes (PIT absent, frozen, or emulation disabled). Used * on the bare-metal path to decide whether to enable the PIT cross-check * for TSC convergence. * * @return 1 if the PIT is running, 0 if it is unavailable. */ static int pit_probe_running(void) { pit_program_reload(0xFFFF); uint16_t last = pit_read_counter16(); for (uint32_t i = 0; i < 20000u; ++i) { uint16_t cur = pit_read_counter16(); if (cur != last) return 1; } return 0; } /* ------------ stats / convergence (bare metal) ------------ */ /** * @brief Compute mean and standard deviation of an array of @c uint64_t samples. * * Uses 64-bit integer arithmetic throughout. For TSC-frequency samples * (~3 GHz) with ≤16 windows, both the sum and variance accumulator remain * within @c uint64_t range so long as deviations are below ~1 MHz (which is * well within the @c CV_EPSILON_PPM = 1000 ppm convergence bound). * * Standard deviation is approximated via integer Newton-Raphson square root * (10 iterations) over the variance. * * Sets @c *mean = @c *stddev = 0 when @p count is 0. * * @param samples Array of @p count frequency samples in Hz. * @param count Number of valid samples. * @param mean Output: arithmetic mean of @p samples. * @param stddev Output: population standard deviation of @p samples. */ static void update_stats(const uint64_t *samples, uint32_t count, uint64_t *mean, uint64_t *stddev) { if (count == 0) { *mean = 0; *stddev = 0; return; } /* For small sample counts (<=16) of Hz values (~3GHz), 64-bit sum is safe */ uint64_t sum = 0; for (uint32_t i = 0; i < count; ++i) sum += samples[i]; uint64_t m = sum / count; /* Variance: sum of (sample - mean)^2 / count * Each (sample - mean) is at most ~1MHz for converged data. * (1e6)^2 * 16 = 16e12, fits in 64 bits. */ uint64_t var_acc = 0; for (uint32_t i = 0; i < count; ++i) { int64_t d = (int64_t)samples[i] - (int64_t)m; var_acc += (uint64_t)(d * d); } uint64_t variance = var_acc / count; *mean = m; if (variance == 0) { *stddev = 0; return; } /* Integer square root via Newton-Raphson */ uint64_t x = variance; uint64_t r = x; for (int i = 0; i < 10; ++i) { r = (r + x / r) / 2; } *stddev = r; } /** * @brief Measure TSC frequency over a fixed HPET tick window. * * Spins until the HPET main counter advances by @p target_ticks ticks, * measuring the TSC delta over the same interval. Converts the result to Hz * via @c muldiv64(). Returns 0 if the elapsed nanoseconds are zero (which * indicates an invalid HPET frequency or overflow). * * Used exclusively on the bare-metal path; the tight spin loop would cause * excessive VM-exits if called under a hypervisor. * * @param target_ticks HPET ticks to wait (~1 ms at 10 MHz HPET). * @return Estimated TSC frequency in Hz, or 0 on error. */ static uint64_t calibrate_window_hpet(uint64_t target_ticks) { uint64_t start_cnt = hpet_read(HPET_MAIN_COUNTER); uint64_t start_tsc = rdtsc(); while ((hpet_read(HPET_MAIN_COUNTER) - start_cnt) < target_ticks) { /* tight loop; bare metal only */ } uint64_t end_tsc = rdtsc(); uint64_t elapsed_ticks = hpet_read(HPET_MAIN_COUNTER) - start_cnt; uint64_t elapsed_ns = muldiv64(elapsed_ticks, 1000000000ull, hpet_freq_hz); if (elapsed_ns == 0) return 0; uint64_t delta_tsc = end_tsc - start_tsc; return muldiv64(delta_tsc, 1000000000ull, elapsed_ns); } /** * @brief Measure TSC frequency over a fixed PIT tick window. * * Programs Channel 0 with reload 0xFFFF and polls until @p target_ticks * PIT ticks have elapsed (up to 200,000 poll iterations). Returns the TSC * Hz measured over the interval, or 0 if @c pit_available is false or the * poll timed out. * * @param target_ticks Number of PIT ticks to measure over (~3.3 ms at 1.19 MHz). * @return Estimated TSC frequency in Hz, or 0 if PIT is unavailable or timed out. */ static uint64_t calibrate_window_pit(uint32_t target_ticks) { if (!pit_available) return 0; pit_program_reload(0xFFFF); uint16_t start_cnt = pit_read_counter16(); uint64_t start_tsc = rdtsc(); for (uint32_t it = 0; it < 200000u; ++it) { uint16_t cur = pit_read_counter16(); uint16_t delta = (uint16_t)((start_cnt - cur) & 0xFFFF); if (delta >= (uint16_t)target_ticks) { uint64_t end_tsc = rdtsc(); uint64_t elapsed_ns = muldiv64((uint64_t)delta, 1000000000ull, PIT_FREQ_HZ); if (elapsed_ns == 0) return 0; uint64_t delta_tsc = end_tsc - start_tsc; return muldiv64(delta_tsc, 1000000000ull, elapsed_ns); } } return 0; } /** * @brief Converge TSC frequency on bare metal using HPET + optional PIT cross-check. * * Runs @c WARMUP_WINDOWS (2) warm-up windows then up to @c SAMPLE_WINDOWS (8) * measurement windows, alternating HPET and PIT samples. Declares convergence * when @c CV_STABLE_COUNT (6) consecutive windows all have CV below * @c CV_EPSILON_PPM (1000 ppm). If both HPET and PIT are active the inter-source * drift must also be < @c CV_EPSILON_PPM before the stable counter increments. * * Fills @c calib_record on success. Calls @c timer_fatal() — never returns — on * any hard error (HPET window failure, PIT required but unavailable). * * @param locked_hz Output: converged TSC frequency in Hz. * @param pit_mean_out Output: mean PIT-derived TSC Hz (0 if PIT disabled). * @return 0 on successful convergence, -1 if the sample limit was exhausted. */ static int converge_tsc_bare_metal(uint64_t *locked_hz, uint64_t *pit_mean_out) { uint64_t hpet_samples[SAMPLE_WINDOWS]; uint64_t pit_samples[SAMPLE_WINDOWS]; uint32_t hpet_count = 0; uint32_t pit_count = 0; console_println("Timer: warm-up..."); for (int i = 0; i < WARMUP_WINDOWS; ++i) { uint64_t h = calibrate_window_hpet(WINDOW_TICKS_HPET); if (h == 0) timer_fatal("Timer warm-up failed (HPET window)."); if (pit_available) { uint64_t p = calibrate_window_pit(WINDOW_TICKS_PIT); if (p == 0) { if (TIMER_REQUIRE_PIT) timer_fatal("Timer warm-up failed (PIT window)."); pit_available = 0; console_println("Timer: PIT disabled (warm-up stall)."); } } console_putc('.'); } console_println(""); console_println("Timer: convergence..."); uint32_t stable_streak = 0; for (int i = 0; i < SAMPLE_WINDOWS; ++i) { uint64_t h = calibrate_window_hpet(WINDOW_TICKS_HPET); if (h == 0) timer_fatal("Timer window failed (HPET)."); hpet_samples[hpet_count++] = h; if (pit_available) { uint64_t p = calibrate_window_pit(WINDOW_TICKS_PIT); if (p == 0) { if (TIMER_REQUIRE_PIT) timer_fatal("Timer window failed (PIT)."); pit_available = 0; console_println("Timer: PIT disabled (convergence stall)."); } else { pit_samples[pit_count++] = p; } } if (hpet_count < 4) { console_putc('.'); continue; } uint64_t mean_h = 0, std_h = 0; update_stats(hpet_samples, hpet_count, &mean_h, &std_h); uint64_t cv_h_ppm = (mean_h == 0) ? UINT64_MAX : muldiv64(std_h, 1000000ull, mean_h); if (!pit_available) { if (cv_h_ppm < CV_EPSILON_PPM) stable_streak++; else stable_streak = 0; if (stable_streak >= CV_STABLE_COUNT) { windows_used = i + 1; *locked_hz = mean_h; *pit_mean_out = 0; calib_record.hpet_hz = hpet_freq_hz; calib_record.tsc_hz_mean = mean_h; calib_record.pit_hz_mean = 0; calib_record.cv_hpet_ppm = cv_h_ppm; calib_record.cv_pit_ppm = 0; calib_record.diff_ppm = 0; calib_record.windows_used = windows_used; calib_record.converged = 1; calib_record.vm_mode = 0; calib_record.trust = TIMER_TRUST_ABSOLUTE; console_puts("Timer converged (HPET-only): TSC="); print_dec(mean_h); console_puts(" Hz, HPET CV="); print_dec(cv_h_ppm); console_println(" ppm"); return 0; } console_putc('.'); continue; } if (pit_count < 4) { console_putc('.'); continue; } uint64_t mean_p = 0, std_p = 0; update_stats(pit_samples, pit_count, &mean_p, &std_p); uint64_t cv_p_ppm = (mean_p == 0) ? UINT64_MAX : muldiv64(std_p, 1000000ull, mean_p); uint64_t min_hz = (mean_h < mean_p) ? mean_h : mean_p; uint64_t max_hz = (mean_h > mean_p) ? mean_h : mean_p; uint64_t diff_ppm = (min_hz == 0) ? UINT64_MAX : muldiv64(max_hz - min_hz, 1000000ull, min_hz); if (cv_h_ppm < CV_EPSILON_PPM && cv_p_ppm < CV_EPSILON_PPM && diff_ppm < CV_EPSILON_PPM) stable_streak++; else stable_streak = 0; if (stable_streak >= CV_STABLE_COUNT) { windows_used = i + 1; *locked_hz = mean_h; *pit_mean_out = mean_p; calib_record.hpet_hz = hpet_freq_hz; calib_record.tsc_hz_mean = mean_h; calib_record.pit_hz_mean = mean_p; calib_record.cv_hpet_ppm = cv_h_ppm; calib_record.cv_pit_ppm = cv_p_ppm; calib_record.diff_ppm = diff_ppm; calib_record.windows_used = windows_used; calib_record.converged = 1; calib_record.vm_mode = 0; calib_record.trust = TIMER_TRUST_ABSOLUTE; console_puts("Timer converged: TSC="); print_dec(mean_h); console_puts(" Hz, HPET CV="); print_dec(cv_h_ppm); console_puts(" ppm, PIT CV="); print_dec(cv_p_ppm); console_puts(" ppm, diff="); print_dec(diff_ppm); console_println(" ppm"); return 0; } console_putc('.'); } return -1; } /* ------------ VM path ------------ */ /** * @brief Derive TSC frequency from CPUID leaves 0x15 and 0x16. * * Checks leaf 0x15 (TSC/Crystal Ratio): * - If EAX, EBX, and ECX are all non-zero: @c hz = @c ECX * @c EBX / @c EAX. * * Falls back to leaf 0x16 (base frequency in MHz) if leaf 0x15 gives 0. * * Both leaves are available on Broadwell and later Intel CPUs; older CPUs * and AMD processors may return 0. Under QEMU/KVM, leaf 0x15 is usually * populated; under QEMU/TCG it often is not. * * @return TSC frequency in Hz if derivable, 0 otherwise. */ static uint64_t derive_tsc_hz_from_cpuid(void) { uint32_t a, b, c, d; /* CPUID.15H: TSC/Crystal ratio */ cpuid(0x15u, 0, &a, &b, &c, &d); if (a != 0 && b != 0 && c != 0) { /* hz = (c * b) / a using muldiv64 to avoid overflow */ uint64_t hz = muldiv64((uint64_t)c, (uint64_t)b, (uint64_t)a); if (hz > 0) return hz; } /* CPUID.16H: base freq in MHz (best-effort) */ cpuid(0x16u, 0, &a, &b, &c, &d); if (a != 0) { uint64_t mhz = (uint64_t)(a & 0xFFFFu); if (mhz > 0) return mhz * 1000000ull; } return 0; } /** * @brief Initialise the timer subsystem in VM (hypervisor-present) mode. * * Skips HPET MMIO entirely (VM-exits would corrupt timing measurements). * Instead: * 1. Checks for invariant TSC. If absent and @c TIMER_VM_STRICT_INVARIANT_TSC * is set, calls @c timer_fatal(). Otherwise logs a warning and uses * @c TIMER_TRUST_RELATIVE mode. * 2. Attempts to derive TSC Hz from CPUID leaves 0x15/0x16. * 3. Falls back to @c calibrate_tsc_with_pmtimer() if CPUID yields 0. * 4. If TSC Hz is still 0, logs a warning and forces @c TIMER_TRUST_RELATIVE * (PM Timer only; no TSC→ns conversion). * 5. Calls @c vm_timebase_init_from_pmtimer() to anchor relative time. * 6. Fills @c calib_record with @c vm_mode = 1. */ static void init_timer_vm_mode(void) { console_println("Timer: VM mode detected (hypervisor present)."); console_println("Timer: HPET calibration disabled (VM-exit MMIO would poison timing)."); int inv = cpu_has_invariant_tsc(); if (!inv) { if (TIMER_VM_STRICT_INVARIANT_TSC) { timer_fatal("Timer: invariant TSC required under hypervisor (strict)."); } console_println("Timer: WARNING: invariant TSC not present under hypervisor."); console_println("Timer: continuing in RELATIVE mode (no determinism guarantees)."); calib_record.trust = TIMER_TRUST_RELATIVE; } else { calib_record.trust = TIMER_TRUST_ABSOLUTE; } if (!cpu_has_rdtscp()) { console_println("Timer: RDTSCP not present; using RDTSC (less serialized)."); } /* Try to lock TSC Hz anyway (useful even in RELATIVE mode) */ tsc_hz_locked = derive_tsc_hz_from_cpuid(); if (tsc_hz_locked == 0) { console_println("Timer: CPUID frequency unavailable; trying PM Timer..."); tsc_hz_locked = calibrate_tsc_with_pmtimer(); } if (tsc_hz_locked == 0) { /* Without a frequency, TSC->ns is meaningless. In VM RELATIVE mode we can still provide PM Timer-based ns. */ console_println("Timer: WARNING: could not derive TSC frequency; PM Timer will be used for RELATIVE ns."); calib_record.trust = TIMER_TRUST_RELATIVE; } /* Initialize VM PM Timer timebase for RELATIVE ns */ vm_timebase_init_from_pmtimer(); calib_record.hpet_hz = 0; calib_record.tsc_hz_mean = tsc_hz_locked; calib_record.pit_hz_mean = 0; calib_record.cv_hpet_ppm = 0; calib_record.cv_pit_ppm = 0; calib_record.diff_ppm = 0; calib_record.windows_used = 0; calib_record.converged = 1; calib_record.vm_mode = 1; console_puts("Timer: trust="); print_dec((uint64_t)calib_record.trust); console_puts(" (0=NONE,1=REL,2=ABS), TSC="); print_dec(tsc_hz_locked); console_println(" Hz"); } /* ------------ public API ------------ */ /** * @brief Initialise the kernel timer subsystem (M5). * * Detects the execution environment (bare metal vs. hypervisor) and * follows the appropriate calibration path: * * - **VM / hypervisor path** (CPUID.1:ECX[31] set): calls * @c init_timer_vm_mode() — HPET MMIO disabled, TSC from CPUID/PM Timer. * - **Bare metal path**: maps HPET MMIO, enables the counter, optionally * probes the PIT, then calls @c converge_tsc_bare_metal() until TSC Hz * converges within @c CV_EPSILON_PPM ppm. Calls @c timer_fatal() on any * hard failure. * * On success, @c tsc_hz_locked holds the calibrated TSC frequency and * @c calib_record is fully populated. @c timer_now_ns() is safe to call * after this function returns. * * @param boot_info BootInfo pointer from the UEFI loader; @c acpi_table is * used to discover the ACPI PM_TMR_BLK port at runtime * (see @c fadt_find_pm_tmr_port()) rather than assuming a * fixed machine type's legacy address. * @return 0 on success; calls @c timer_fatal() (never returns) on hard failure. */ int timer_init(BootInfo *boot_info) { console_println("Timer: init start"); /* default record */ calib_record.trust = TIMER_TRUST_NONE; calib_record.converged = 0; calib_record.vm_mode = 0; if (boot_info) { uint16_t discovered = fadt_find_pm_tmr_port(boot_info->acpi_table); if (discovered != 0) { pmtimer_port = discovered; console_puts("Timer: PM_TMR_BLK discovered from FADT at port "); print_dec((uint64_t)pmtimer_port); console_println(""); } else { console_puts("Timer: WARNING: FADT PM_TMR_BLK lookup failed; using fallback port "); print_dec((uint64_t)pmtimer_port); console_println(""); } } if (running_under_hypervisor()) { init_timer_vm_mode(); return 0; } /* Bare metal path */ if (vmm_map_range(HPET_VIRT_BASE, HPET_PHYS_BASE, 0x1000, VMM_FLAG_WRITABLE) != 0) { timer_fatal("Failed to map HPET MMIO."); } hpet_regs = (volatile uint64_t *)(uintptr_t)HPET_VIRT_BASE; uint64_t cap = hpet_read(HPET_GEN_CAP_ID); uint64_t period_fs = cap >> 32; if (period_fs == 0) { timer_fatal("HPET period invalid."); } hpet_freq_hz = 1000000000000000ull / period_fs; enable_hpet(); sanity_check_hpet_running_quick(); pit_available = pit_probe_running() ? 1 : 0; if (!pit_available && TIMER_REQUIRE_PIT) { timer_fatal("PIT required but not available."); } if (converge_tsc_bare_metal(&tsc_hz_locked, &pit_tsc_hz_mean) != 0) { timer_fatal("TSC did not converge."); } console_puts("Timer: HPET freq = "); print_dec(hpet_freq_hz); console_println(" Hz"); console_puts("Timer: TSC locked = "); print_dec(tsc_hz_locked); console_println(" Hz"); if (pit_available) { console_puts("Timer: PIT mean = "); print_dec(pit_tsc_hz_mean); console_println(" Hz"); } else { console_println("Timer: PIT cross-check skipped."); } return 0; } /** * @brief Return the locked TSC frequency in Hz. * * Returns the value established by @c timer_init(). Zero means calibration * has not completed or failed to derive a TSC frequency (PM Timer relative * mode only). * * @return TSC frequency in Hz, or 0 if unknown. */ uint64_t timer_tsc_hz(void) { return tsc_hz_locked; } /** * @brief Return nanoseconds since boot using the ACPI PM Timer (VM relative mode). * * Reads the current PM Timer counter, computes ticks elapsed since * @c vm_pm_start (set by @c vm_timebase_init_from_pmtimer()), converts to * nanoseconds via @c muldiv64(), and adds @c vm_ns_base. Used when the * calibration record shows @c vm_mode = 1 and @c trust < TIMER_TRUST_ABSOLUTE. * * @return Nanoseconds elapsed since @c vm_timebase_init_from_pmtimer() was called. */ static uint64_t timer_now_ns_vm_relative(void) { /* Monotonic-ish RELATIVE ns based on ACPI PM Timer (port I/O). */ uint32_t cur = pmtimer_read() & (uint32_t)PMTIMER_MASK; uint64_t ticks = pmtimer_delta(vm_pm_start, cur); uint64_t ns = muldiv64(ticks, 1000000000ull, PMTIMER_FREQ_HZ); return vm_ns_base + ns; } /** * @brief Return the current monotonic time in nanoseconds. * * Selects the best available time source based on the calibration record: * * - @c TIMER_TRUST_NONE: returns 0 (no time base yet). * - VM mode + trust < @c TIMER_TRUST_ABSOLUTE: returns PM Timer relative ns. * - Absolute mode (@c tsc_hz_locked > 0): reads TSC (RDTSCP if supported, * else RDTSC) and converts to ns via @c muldiv64(). * * Called by the HAL (@c sk_hal_time_ns()) to feed the physics engine. * * @return Nanoseconds since boot, or 0 if the timer is not yet calibrated. */ uint64_t timer_now_ns(void) { /* If we don't have a time base yet, be honest. */ if (calib_record.trust == TIMER_TRUST_NONE) return 0; /* VM RELATIVE mode: return PM Timer derived ns */ if (calib_record.vm_mode && calib_record.trust < TIMER_TRUST_ABSOLUTE) { return timer_now_ns_vm_relative(); } /* ABSOLUTE mode: use TSC -> ns */ if (tsc_hz_locked == 0) return 0; uint64_t t; if (cpu_has_rdtscp()) { uint32_t aux; t = rdtscp_aux(&aux); } else { t = rdtsc(); } return muldiv64(t, 1000000000ull, tsc_hz_locked); } /** * @brief Check TSC drift against the HPET reference on bare metal. * * Takes a single HPET calibration window and compares the measured TSC Hz * to @c tsc_hz_locked. If the drift exceeds @c DRIFT_EPSILON_PPM (2000 ppm), * calls @c timer_fatal() — never returns. Returns -1 if either reading is * invalid. * * Skipped in VM mode (@c timer_check_drift_now() returns 0 immediately). * * @return 0 if drift is within bound, -1 on measurement failure. */ static int timer_drift_check_bare_metal(void) { uint64_t current = calibrate_window_hpet(WINDOW_TICKS_HPET); if (current == 0 || tsc_hz_locked == 0) return -1; uint64_t min_hz = (current < tsc_hz_locked) ? current : tsc_hz_locked; uint64_t max_hz = (current > tsc_hz_locked) ? current : tsc_hz_locked; uint64_t diff_ppm = (min_hz == 0) ? UINT64_MAX : muldiv64(max_hz - min_hz, 1000000ull, min_hz); if (diff_ppm > DRIFT_EPSILON_PPM) { timer_fatal("Timer drift exceeded runtime bound."); } return 0; } /** * @brief Perform a runtime TSC drift check (no-op in VM mode). * * In bare-metal mode delegates to @c timer_drift_check_bare_metal(). In VM * mode returns 0 immediately: VM drift checking requires a VM-friendly * reference (such as pvclock) that is not yet implemented. * * @return 0 if drift is acceptable or in VM mode, -1 on bare-metal * measurement failure. */ int timer_check_drift_now(void) { if (calib_record.vm_mode) { /* VM policy: drift checks require a VM-friendly reference (e.g., pvclock) */ return 0; } return timer_drift_check_bare_metal(); } /** * @brief Return a pointer to the current timer calibration record. * * Provides read-only access to the @c calib_record populated by * @c timer_init(). Callers can inspect fields such as @c trust, * @c tsc_hz_mean, @c cv_hpet_ppm, and @c converged for diagnostic output * or to determine whether absolute timing guarantees are available. * * @return Pointer to the static @c timer_calibration_record_t (never NULL). */ const timer_calibration_record_t *timer_calibration_record(void) { return &calib_record; } /* ============================================================================ * M5 Heartbeat Subsystem * ============================================================================ * * Implements TIME-TICKS and TIME-TRUST: * - TIME-TICKS: Monotonic heartbeat count * - TIME-TRUST: Continuous Q48.16 confidence derived from timing variance * * On each tick: * 1. Sample TSC * 2. Compute delta from last tick * 3. Record deviation in rolling window * 4. Compute variance from window * 5. Derive TIME-TRUST from variance */ /** * @brief Read the raw counter the amd64 heartbeat is paced against. * * Item 0.8 (FABRIC.md §25.1): the shared heartbeat.c owns * heartbeat_init()/heartbeat_tick()/heartbeat_service()/heartbeat_ticks()/ * heartbeat_trust()/heartbeat_state() and the variance/trust math that used * to live in this file. This is the one piece that stays per-architecture * -- the same @c rdtsc() the rest of this file's TSC calibration already * uses, not a separate/different source. * * @return Current TSC value. */ uint64_t heartbeat_read_counter(void) { return rdtsc(); }