P40 keeps the P39 RTL unchanged and rewrites the M-mode runtime so the trap path is solid enough for the FreeRTOS port at P41 to inherit without surprise.
Status: RTL pass. Directed test passes (
5808clocks - P39 plus the new ecall + IRQ-save probes). No P40 harden run yet.
Why an audit, not a feature
The P39 runtime trap glue saved only t0/t1 and didn’t recognise
ecall at all. Calling ecall from FreeRTOS-style code on it would
have:
- Trapped to
trap_vector, - Failed to match
MTIorMEI, - Fallen through to
p37_failand halted withx5 = 31.
That is the worst kind of bug for software bring-up - “kernel just stops, no message” instead of “port doesn’t compile” or “test asserts.” P40 closes that gap before P41 has the chance to discover it the painful way.
What changed
| change | location |
|---|---|
| 128-byte trap frame on entry | runtime/start.S |
mcause dispatch (ECALL_M / MTI / MEI / fail) | runtime/start.S |
c_trap_handler() for ecall | runtime/main.c |
| Trap-frame ABI header | runtime/trap.h |
irq_save() / irq_restore() inline asm helpers | runtime/trap.h |
_Static_assert catching layout drift between asm and C | runtime/trap.h |
ecall_probe() directed test | runtime/main.c |
irq_save_restore_probe() directed test | runtime/main.c |
The frame layout is fixed-size and uniform across every cause. We pay 128 bytes per trap regardless of what triggered it; the win is that FreeRTOS context-switching doesn’t need to special-case trap-cause shape.
What c_trap_handler proves
The directed test’s ecall_probe() does:
uint32_t result = do_ecall(0x1234u); // a0 = 0x1234, then ecall
// expect: result == ECALL_COOKIE (set by handler)
// p40_ecall_count incremented by 1
The path is:
ecallinstruction raises ECALL_M trap.start.Ssaves the full 128-byte frame.c_trap_handlerreadsframe->mcause, seesMCAUSE_ECALL_M, bumpsp40_ecall_count, writes0xC0DEC0DEintoframe->a0, and bumpsframe->mepcby 4.start.Swrites the (updated) frame’smepcback to the CSR, restores all GPRs from the frame (including the mutateda0), thenmret.- C caller observes
a0 == 0xC0DEC0DEand the counter incremented.
A second back-to-back ecall verifies re-entrance in the trivial sense. Both pass.
What irq_save / irq_restore proves
Four checks, all on the same routine:
- With
MIE = 0:irq_save()returns0, leavesMIE = 0. - With
MIE = 1set explicitly:irq_save()returns0x8, leavesMIE = 0(atomic viacsrrci). irq_restore(0x8)re-armsMIE.irq_restore(0x0)does not flipMIEif it’s already0.
That is the bracket FreeRTOS uses to enter and leave critical sections. P40 verifies the primitives behave exactly as expected before P41 starts using them under a real scheduler.
What stays the same
src/top.sv- identical to P39 except formimpid = 0x40.- ISA - RV32IM + Zicsr + Zifencei + Zicntr.
- Memory map - same external-RAM-at-zero, same MMIO at 0x10000000.
- Arch-test plugin - same DUT contract, copied verbatim into
projects/40_mmode_runtime_audit/arch_test/. Sweep wired up but not yet re-run; expected to match P39’sPASS=56.
What just happened?
P40 is software-side packaging on top of P39’s hardware. No new instructions, no new traps, no new MMIO. What changed is the shape of the runtime’s contract with the OS that will eventually run on top of it. The trap frame is uniform, the C handler can see and mutate full register state, and IRQ save/restore is a real bracket.
The next rung in the roadmap is P41, the actual
FreeRTOS port (compile-only). P40 is the foundation that lets P41
focus on port.c and portASM.S instead of inventing a trap frame
from scratch.