journal 2026-05-03

P40 audits the M-mode runtime

p40riscvruntimeecallfreertos

P39 added Zicntr counters - real ISA work, but small. P40 is a software-side rung: same RTL, but the runtime’s trap path gets overhauled so FreeRTOS can plug into it at P41 without surprise.

The smoking-gun motivation: the P37/P39 trap glue saved exactly two registers (t0, t1) and didn’t recognise ecall at all. Calling ecall from FreeRTOS-style code on that runtime would have fallen through mcause matching, hit the p37_fail label, and halted the chip with x5 = 31 - silently, no diagnostic. Worst kind of bug for software bring-up.

Three things landed:

  1. A real trap frame. runtime/start.S now saves all 30 GPRs + mepc + mcause into a fixed 128-byte stack frame. Same layout for every cause - no special cases. The C-side mirror in runtime/trap.h has a _Static_assert that catches drift between the assembly offsets and the struct.
  2. A C-side handler. c_trap_handler() in runtime/main.c receives the frame pointer, advances mepc by 4 for ecall, and can mutate any GPR slot in the frame. The directed test sends ecall from C, observes a0 come back as 0xC0DEC0DE (a magic cookie the handler wrote), and confirms the ecall counter incremented. Two back-to-back ecalls verify re-entrance.
  3. Real IRQ primitives. irq_save() clears mstatus.MIE atomically (via csrrci) and returns the previous bit; irq_restore() re-arms it iff the saved bit was set. The probe walks all four corner cases (was-clear/was-set × restore-clear/ restore-set). All pass.

The directed test now halts at 5808 clocks (P39 was 4002); the extra 1806 are the new ecall and IRQ probes plus the larger trap frame on each interrupt.

The point isn’t that P40 does anything observable from outside the chip. It does not. It’s that P41 (FreeRTOS port) inherits a runtime that has already proven the shapes work, instead of debugging them under a scheduler.

ISA hasn’t changed; the arch-test sweep isn’t re-run here but the target is wired up in case someone wants the regression smoke.