rivet-bsp-esp32c6 0.2.0

Rivet RTOS: ESP32-C6 board port (UART0 console, watchdog disable, SYSTIMER clock) — real hardware, no QEMU model
docs.rs failed to build rivet-bsp-esp32c6-0.2.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 board support: ESP32-C6 real hardware (plan.md Phase 26).

Status

Confirmed working on real hardware: boot (through espflash's ESP-IDF app image format — see link-esp32c6.ld's module docs), watchdog disable, the monotonic clock (SYSTIMER), the polling console, preemptive task spawning and dispatch, and — the hard part — genuine tick-driven preemption between two same-priority tasks (preempt_test.rs: reliable "ABABBABAB..." interleaving, both tasks reaching completion, repeatable across independent flash/boot cycles).

Four real, hardware-confirmed bugs found getting there, all root-caused (not guessed) via bisecting print statements or, once prints stopped showing up at all, via bisecting hardware behavior itself against esp-hal's reference source:

  1. This core's PMP doesn't behave like the privileged spec's reference shape rivet-arch-riscv::pmp was written against (proven correct on QEMU virt and MPS2-AN385) — probing its grain via the standard technique returned an implausible ~256 MiB minimum NAPOT region, not a real hardware property. Worked around, not fixed: hardware PMP guards are off entirely for this board (pmp-guard disabled on rivet-arch-riscv), falling back to the kernel's own software stack-watermark check alone — already proven sufficient by itself on the ESP32-S3, which has no PMP/MPU-equivalent hardware guard at all.
  2. mcycle (RV32's cycle-count CSR) hangs forever the instant it's read on this core — not the standard rollover-safe mcycleh double-read loop failing to converge (tried a 32-bit-only read too, still hung), the bare single-instruction csrr itself never completes, with no trap/panic ever printed. rivet-arch-riscv's no-mcycle feature skips defining __rivet_arch_cycle_count entirely; this crate provides it instead, derived from SYSTIMER (already used for now_us()).
  3. This chip's interrupt matrix gates CPU-level delivery through PLIC_MX (0x2000_1000), a genuinely different peripheral from INTPRI (0x600c_5000) despite similar-looking field names in the esp32c6 PAC — writing enable/priority/threshold to INTPRI was silently a no-op for delivery. See the "interrupt matrix" section below.
  4. Enabling one of this chip's custom per-line mie bits (12-31) while mtvec is in direct mode hard-locks the core immediately — no trap, no exception, nothing printable, every time. This chip's "plic" controller flavour requires vectored mtvec (confirmed against esp-hal's own _setup_interrupts, which always installs one before touching these lines) — rivet-arch-riscv's new vectored-trap feature. Getting the resulting image to actually flash uncovered a fifth, tooling-level issue: see rivet-arch-riscv's own vector-table doc comment for the espflash alignment-padding quirk that cost the most time of anything in this list.

Full S3-equivalent test suite now passing on real hardware: demo, preempt_test, sleep_test, mutex_test, stress_spawn, join_test, respawn_test, stress_max_ptasks, soak_smoke, report_test, deadline_test, watchdog_test (genuine LP_WDT reset — rst:0x10 LP_WDT_SYS — after the expected marker), fault_overflow/fault_isolate (via the software watermark check; PMP guards are off, see bug #1), irq_test (a real peripheral interrupt — UART0 tx_done — routed through rivet::irq, not a software-only stand-in; see the "generic peripheral IRQ" section below), embedded_hal_test (rivet-bsp-support's RivetDelay/ Serial are entirely board-agnostic, so this needed no new code at all). smp_test doesn't apply — single-core chip.

The interrupt matrix + PLIC_MX (plan.md Phase 26 follow-up)

Same two-part design as the ESP32-S3's own interrupt matrix (rivet-arch-xtensa's periph_irq/ipi modules), just RISC-V-side register names: INTERRUPT_CORE0.core_0_intr_map(source) routes a peripheral interrupt source (e.g. SYSTIMER_TARGET0 = 57, FROM_CPU_INTR0 = 22 — confirmed against this PAC version's own Interrupt enum) to one of 32 CPU interrupt lines; PLIC_MX (0x2000_1000 — a different peripheral from INTPRI, despite esp32c6's SVD using similar-looking names for both; confirmed by reading esp-hal's own interrupt/riscv/plic.rs driver, which esp-metadata's soc.toml says is the controller flavour this chip actually uses) enables/prioritizes each line via its mxint_enable/_pri/_thresh/_clear registers. INTPRI still has one real job: its cpu_intr_from_cpu register is the software self-trigger ("write 1 to raise IPI to self") used for the reschedule line — a genuinely separate mechanism from the delivery gate above. rivet-arch-riscv's trap handler doesn't know any of this — dispatch reaches this crate via its board-irq-hook feature, an escape hatch exactly for controllers that are real RISC-V ISA-adjacent hardware but not portable across vendors the way CLINT/PLIC are. Both PLIC_MX-enabled lines also need their mie bit set directly at the core (raw CSR access — bits 12-31 aren't in the riscv crate's typed API) — see bug #4 above for why that requires rivet-arch-riscv/vectored-trap.

CPU lines 16 (tick) and 17 (reschedule) were picked arbitrarily from the "platform-defined" range (avoiding 3/7/11, which carry standard RISC-V meaning even though this core has no CLINT wired to them) — not verified against any documented reservation, just chosen to be unlikely to collide with anything.

Generic peripheral IRQ (rivet::irq)

Lines 18-23 are a small pool reserved for whatever peripherals an app registers via rivet::irq::register/enable — a board-owned [AtomicU32; 6] (IRQ_LINE_SOURCE) hands out one on first use and remembers the mapping so a later enable for the same id reuses it. rivet::irq::register/dispatch both index a fixed [T; RIVET_MAX_IRQS] table in rivet itself (default 32) directly by the u32 the caller passes — this chip's own peripheral source numbers run well past that (UART0 = 43), so irq::UART0 etc. are small logical ids instead of raw source numbers, translated to the real source only where INTERRUPT_CORE0 routing actually needs it (LOGICAL_TO_SOURCE). rivet-arch-riscv's board-irq-hook feature grew three more extern hooks for this (__rivet_board_irq_{enable, disable,set_priority}), the same split as the tick/resched dispatch hook: the mechanism (rivet::irq's table, __rivet_arch_irq_*'s call-through) is arch-generic, which peripheral is which id and how it's wired to a CPU line is this board's own business.

Why no esp-hal dependency

Same reasoning as rivet-bsp-esp32s3: esp-hal's own interrupt/timer runtime would fight Rivet for ownership of the same hardware. Watchdog disable and the boot-time cache/clock state are hand-rolled directly against the esp32c6 PAC, cross-checked against esp-hal's own source for correctness (register names, unlock keys, bit meanings) without linking esp-hal itself.