Punch list §25 item 4.3.5b complete. sie.SEIE enabled, PLIC threshold/claim/complete wired into the trap handler. Verified with a UART-loopback synthetic interrupt (PLIC has no software set-pending register, unlike GICv2): claim_count=1, last_irq=10, IIR confirms genuine receive-data cause, byte matched exactly. Self-test code run once for evidence then fully reverted, per Captain Bob's ruling; only the permanent substrate remains, no source enabled by default. Three-arch acceptance boot clean, zero exceptions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
284 lines
9.0 KiB
C
284 lines
9.0 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.
|
||
*/
|
||
|
||
/**
|
||
* 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 <stdint.h>
|
||
|
||
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;
|
||
}
|