rivet-arch-xtensa 0.1.0

Rivet RTOS: Xtensa LX7 ISA port (ESP32-S3) — context switch, exception/interrupt entry, no board/MMIO knowledge
docs.rs failed to build rivet-arch-xtensa-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

Rivet RTOS — Xtensa LX7 ISA port (ESP32-S3, plan.md Phases 21/24).

Implements the Group A (rivet::port::arch) symbol contract for the xtensa-esp32s3-none-elf target: context switch, interrupt entry/exit, critical section. Contains no board/MMIO knowledge (UART, watchdog, SYSTIMER) — that is rivet-bsp-esp32s3's job.

Why this crate depends on xtensa-lx/xtensa-lx-rt

Unlike rivet-arch-riscv/rivet-arch-cortex-m (hand-written trap entry assembly, verified against a QEMU machine model), this port targets real hardware only — no Xtensa QEMU model is available in this environment. Xtensa's exception/interrupt architecture (multiple priority levels, hardware-assisted windowed-register spill/fill) is substantially harder to get right blind than RISC-V's single mtvec or Cortex-M's fixed vector table, with a real bricking risk and no fast simulator iteration loop. xtensa-lx/xtensa-lx-rt are the direct Xtensa analogs of the riscv/cortex-m crates the other two ports depend on for CSR/register access — except here the boot-glue layer (vector table, exception/interrupt entry assembly, Reset) is also sourced from there rather than hand-written, because that is exactly the layer where a mistake is most likely and least recoverable. Rivet's own code starts where xtensa-lx-rt hands control to a Rust interrupt handler with a full, already-correctly-saved Context.

Task switching: mutate the save frame, don't hand-write asm

xtensa_lx_rt::exception::Context is the CPU's full saved state (PC, PS, A0-A15, SAR, ...). #[interrupt(3)]'s handler receives &mut Context pointing at a frame xtensa-lx-rt's own assembly allocated 256 bytes below whatever stack was live at interrupt time (confirmed by reading SAVE_CONTEXT/RESTORE_CONTEXT in xtensa-lx-rt's source: A1 is saved/restored as the genuine pre-interrupt stack pointer). Whatever this handler leaves in that struct is what gets resumed on return — so a context switch is: copy the outgoing task's full state out, copy the incoming task's full state in. No hand-written save/restore assembly needed for the switch itself.

Because the CPU's windowed-register overflow/underflow spill handling is defined relative to the current stack (A1), not any global state, switching A1 to a different task's stack as part of restoring its Context is sufficient on its own — the hardware spills/fills each task's own window state to/from that task's own stack memory on demand, already isolated by construction. Context does not persist WINDOWBASE/WINDOWSTART — confirmed against xtensa-lx-rt's own struct definition, consistent with this being re-derivable hardware state rather than architectural task state.

A task's first-ever dispatch does not go through a real interrupt

A brand new task can be dispatched two different ways: via [__rivet_arch_start_first_task] (the very first task the scheduler ever runs, reached directly from boot) or, just as often, via the ordinary tick/reschedule interrupt path (every other task spawned before rivet::run(), since only one task can be "first"). Both cases fabricate the same shape of Context — see [fresh_task_context] — reached by jumping (never calling) into [rivet_ptask_trampoline_impl], whose first instruction is entry a1, 0. This is exactly how xtensa-lx-rt's own Reset reaches main (a plain jump into windowed code with A1 pre-pointed at a fresh stack), and the one thing a fabricated first dispatch does not need to get right — a valid return address / caller frame — is never exercised, because the trampoline never executes retw: task exit goes through an explicit call to rivet_task_exit_core, never a return.

Distinguishing "never dispatched" from "has a saved Context"

[__rivet_arch_init_task_stack] runs before the task has an id (rivet::preempt::spawn calls it before registering the task), so it cannot yet index into [CONTEXTS] (which is keyed by task id). Its returned sp therefore encodes (entry_fn, arg, stack_base, stack_len) directly — a bootstrap marker, not a Context index. sp values of the two kinds are reliably distinguishable at every point that needs to tell them apart: a task id is always < MAX_PTASKS (a handful, in practice); a bootstrap marker is always >= MAX_PTASKS by construction (see [encode_bootstrap]). The first time a task is actually interrupted (by either path), its Tcb.sp is permanently rewritten to its task id, and the bootstrap marker for it is never consulted again.