/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James. All rights reserved. Licensed under the StarForth License, Version 1.0. */ /** * apic.c (riscv64) - APIC stub. No Local APIC on RISC-V; interrupt * controller is PLIC (Platform-Level Interrupt Controller). apic_init() * calls into plic.c for the real PLIC bring-up (item 4.3.5b); apic_eoi() * stays a no-op since PLIC completion happens per-source in the trap * handler (claim/complete), not via a single shared EOI register. */ #include "apic.h" #include "uefi.h" #include "console.h" #include "timer.h" #include "starkernel/plic.h" #include static uint64_t s_timer_period_tsc = 0; static uint64_t s_time_hz = 0; /* `time` CSR frequency (item 0.8/§26) */ /* --------------------------------------------------------------------------- * SBI (Supervisor Binary Interface) * * RISC-V S-mode cannot program the timer directly: the CLINT's mtimecmp is an * M-mode register. The timer is armed by asking the SEE (OpenSBI, running in * M-mode beneath EDK2) via ECALL. * * Calling convention, SBI v0.2+ (SBI spec §3): a7 = EID, a6 = FID, * a0.. = arguments; returns a0 = error, a1 = value. * ------------------------------------------------------------------------- */ #define SBI_EXT_BASE 0x10UL #define SBI_BASE_FID_PROBE_EXT 3UL #define SBI_EXT_TIME 0x54494D45UL /* "TIME" */ #define SBI_TIME_FID_SET_TIMER 0UL #define SBI_SUCCESS 0L /* sie.STIE — supervisor timer interrupt enable (Privileged Spec §4.1.3) */ #define SIE_STIE (1UL << 5) typedef struct { long error; long value; } sbiret_t; /** @brief Issue an SBI ECALL with one argument. */ static sbiret_t sbi_call1(unsigned long eid, unsigned long fid, unsigned long arg0) { register unsigned long r_a0 __asm__("a0") = arg0; register unsigned long r_a1 __asm__("a1") = 0; register unsigned long r_a6 __asm__("a6") = fid; register unsigned long r_a7 __asm__("a7") = eid; sbiret_t ret; __asm__ volatile ( "ecall" : "+r"(r_a0), "+r"(r_a1) : "r"(r_a6), "r"(r_a7) : "memory"); ret.error = (long)r_a0; ret.value = (long)r_a1; return ret; } /** * @brief Read the RISC-V @c time CSR. * * Deadlines handed to @c sbi_set_timer() are absolute values on this counter. * @c arch/riscv64/timer.c keeps its own copy of this accessor; duplicating * four instructions is preferable to widening @c timer.h with an * architecture-specific accessor that only these two files can use. */ static inline uint64_t rdtime(void) { uint64_t val; __asm__ volatile ( "rdtime %0" : "=r"(val)); return val; } /* Set once in apic_timer_start(): 1 when the TIME extension probed present, * 0 when the timer could not be armed at all. */ static int s_sbi_time_ok = 0; /* Absolute `time` value of the next expected interrupt. Advanced by period * rather than recomputed from "now" so that a late tick does not push the * whole schedule out; see riscv64_timer_rearm(). */ static uint64_t s_next_deadline = 0; /** * @brief Arm the SBI timer for @p deadline. * @return 1 on success, 0 if the SEE rejected the call. */ static int sbi_set_timer(uint64_t deadline) { sbiret_t r = sbi_call1(SBI_EXT_TIME, SBI_TIME_FID_SET_TIMER, (unsigned long)deadline); return r.error == SBI_SUCCESS; } /** * @brief Re-arm the one-shot SBI timer and account the tick. * * **The SBI timer is one-shot by nature.** Servicing a timer interrupt without * programming the next deadline leaves the heartbeat stopped permanently, with * no error anywhere — the single most likely silent failure of this driver. * Every path out of a timer interrupt must reach this function. * * The step is @c heartbeat_next_period_ns() (item 0.8, §26) converted to * `time`-counter ticks via @c s_time_hz, not the fixed @c s_timer_period_tsc * used to seed the very first deadline in @c apic_timer_start() -- the * latter remains @c apic_timer_period_tsc()'s return value for * @c heartbeat_init()'s initial @c expected_delta, unchanged. * * Called from @c riscv64_interrupt_handler() in @c interrupts.c on * @c scause cause 5. */ void riscv64_timer_rearm(void) { uint64_t now; uint64_t step; if (!s_sbi_time_ok) return; step = (s_time_hz > 0) ? (heartbeat_next_period_ns() * s_time_hz) / 1000000000ULL : s_timer_period_tsc; if (step == 0) { step = 1; } s_next_deadline += step; /* If servicing ran long enough that the next deadline is already behind * us, resynchronise rather than burn through a backlog of instant * interrupts. */ now = rdtime(); if (s_next_deadline <= now) { s_next_deadline = now + step; } sbi_set_timer(s_next_deadline); } /* * @brief Initialise the interrupt controller: PLIC bring-up (item 4.3.5b). * * On x86-64 this function configures the Local APIC. On RISC-V the * Platform-Level Interrupt Controller (PLIC) handles external interrupt * routing -- @c plic_init() (arch/riscv64/plic.c) maps it, sets the S-mode * context's threshold, and enables @c sie.SEIE. No source is enabled here; * that is each future consumer's own job (4.3.5c for virtio-keyboard). * * @param boot_info Kernel boot information; unused (PLIC base is a verified * constant, not discovered from @c boot_info->dtb -- see * plic.c's file header for why). * @return 0 always. */ int apic_init(BootInfo *boot_info) { (void)boot_info; plic_init(); return 0; } /** * @brief Signal End of Interrupt to the interrupt controller (RISC-V stub). * * On x86-64 this writes to the Local APIC EOI register. On RISC-V EOI * is handled by reading the PLIC claim/complete register; that path is * not yet wired. This stub is a no-op that satisfies the common interface. */ void apic_eoi(void) { } /* * @brief Compute the expected `time`-counter ticks per heartbeat period. * * Unlike x86-64's APIC timer, this does not itself arm anything — RISC-V S-mode * cannot program the timer directly, so arming happens via SBI in * @c apic_timer_start() below. This function only computes * @c s_timer_period_tsc, the expected number of @c time-counter ticks between * heartbeats, which seeds both @c apic_timer_start()'s first deadline and * @c heartbeat_init()'s @c expected_delta. * * Falls back to 10,000,000 ticks if either frequency is zero (a safe * non-zero sentinel preventing division-by-zero in the heartbeat path). * * @param tsc_hz `time` counter frequency from @c timer_init() (Hz). * @param tick_hz Desired heartbeat rate (Hz); 0 → uses fallback. * @return 0 always. */ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz) { if (tick_hz > 0 && tsc_hz > 0) s_timer_period_tsc = tsc_hz / tick_hz; else s_timer_period_tsc = 10000000; s_time_hz = tsc_hz; return 0; } /** * @brief Start timer delivery via the SBI TIME extension. * * Probes for the TIME extension first. If the SEE does not provide it the * timer is **not** armed and the condition is reported loudly rather than * papered over with the legacy EID 0x00 call: a heartbeat that silently never * ticks is far worse to diagnose than one that says why at boot. * * On success: computes the first absolute deadline, arms it, and sets * @c sie.STIE. Global delivery is gated separately by @c sstatus.SIE, which * @c arch_enable_interrupts() sets. */ void apic_timer_start(void) { sbiret_t probe; probe = sbi_call1(SBI_EXT_BASE, SBI_BASE_FID_PROBE_EXT, SBI_EXT_TIME); if (probe.error != SBI_SUCCESS || probe.value == 0) { console_println("SBI: TIME extension ABSENT - timer NOT armed, " "heartbeat will not tick"); s_sbi_time_ok = 0; return; } s_sbi_time_ok = 1; s_next_deadline = rdtime() + s_timer_period_tsc; if (!sbi_set_timer(s_next_deadline)) { console_println("SBI: set_timer REJECTED - timer NOT armed"); s_sbi_time_ok = 0; return; } __asm__ volatile ( "csrs sie, %0" :: "r"(SIE_STIE) : "memory"); console_println("SBI: timer armed (TIME extension)"); } /** * @brief Stop timer delivery by masking @c sie.STIE. * * The SBI timer cannot be cancelled outright — masking the enable bit is the * supported way to stop delivery. Any deadline already programmed simply goes * unserviced. */ void apic_timer_stop(void) { __asm__ volatile ( "csrc sie, %0" :: "r"(SIE_STIE) : "memory"); s_sbi_time_ok = 0; } /** * @brief Return the expected `time`-counter ticks per heartbeat period. * * Returns @c s_timer_period_tsc, set by @c apic_timer_init() as * @c tsc_hz / @c tick_hz. Consumed by @c heartbeat_init() to seed the * rolling-window @c expected_delta, and by @c apic_timer_start() / * @c riscv64_timer_rearm() as the SBI deadline step. * * @return Expected `time`-counter ticks per heartbeat period; 10,000,000 if * frequencies were not available at init time. */ uint64_t apic_timer_period_tsc(void) { return s_timer_period_tsc; }