No. 50 / project of 147 on the ladder

S-mode trap routing

introduces — medeleg-driven trap routing to S, sret instruction, sstatus as a real subset view of mstatus

harden statelast run2026-05-03
areaunknownμm²
signoff
  • DRCNOT RUN
  • LVSNOT RUN
  • antennaNOT RUN

P50 wires medeleg from P48’s M-readable storage into actual trap routing. When the chip takes an exception in S-mode and the corresponding medeleg bit is set, the trap goes to S — uses stvec, writes sepc/scause/stval, captures cur_priv into mstatus.SPP. Adds sret so S-mode software can return.

Status: RTL pass. Delegation probe drives M→S→trap-to-S→ sret→S→trap-to-M→mret→M and verifies cause/SPP/MPP captures at every step. PASS at 5,102,703 clocks; FreeRTOS demo runs to halt cleanly afterwards.

What changed

enter_trap decides M vs S routing

delegate_to_s = (cur_priv != M)
              && ((!is_interrupt && medeleg[cause_idx]) ||
                  ( is_interrupt && mideleg[cause_idx]));

If delegating, write sepc/scause/stval, capture cur_priv into mstatus.SPP, save SIE→SPIE, set cur_priv=S, jump to stvec. Else the existing M-mode path.

sret instruction

0x10200073. Restores cur_priv from mstatus.SPP, restores SIE from SPIE, sets SPP back to U=0 (per spec — least-priv supported on sret), jumps to csr_sepc. Behavioral dual of mret.

sstatus is a subset view of mstatus

The P47 standalone storage register is gone. Reads return mstatus & SSTATUS_MASK (SIE bit 1 | SPIE bit 5 | SPP bit 8). Writes update only those bits in mstatus. Single architectural register, M-only and S-visible projections.

What’s tested

trap_delegation_probe (error codes 110-115):

  1. Save mtvec/stvec/mstatus/medeleg, install probe-local M and S trap handlers.
  2. medeleg[9] = 1, mret with MPP=S to a label.
  3. ecall from S — delegated. Chip routes to stvec, captures cur_priv=S into sstatus.SPP.
  4. S handler captures scause (=9) + sstatus, sets SPP=1, sret keeps us in S.
  5. Clear medeleg, ecall from S — not delegated. Chip routes to mtvec, captures cur_priv=S into mstatus.MPP.
  6. M handler captures, mret back to M.

End-to-end the cause codes, captured priv values, and handler hit-counts all match expectations.

What still doesn’t delegate

Interrupts. The routing decision consults mideleg correctly, but our interrupt fast paths (timer/external) haven’t been exercised from S-mode and sip/sie are still P47 standalone storage (not subset views of mip/mie). Next rung.

M-only CSR access from S isn’t privilege-checked either. The spec says accessing mstatus/medeleg/mtvec/etc. from S should illegal-instr trap; our chip allows it. Separable rung.

Files

  • src/top.sv - delegation routing + sret + sstatus subset
  • app/main.c - trap_delegation_probe

Harden

NOT RUN. Estimated +60 cells over P49 (sret decode, delegation mux on trap entry, sstatus subset projection logic).

What just happened?

The chip can now take a trap in one priv mode and run the handler in another. That’s the first piece of real privilege machinery — every prior rung was M-only-everywhere.

Linux uses this heavily: page faults, ecall-from-U, supervisor timer interrupts all need to land in S without an M-mode bounce. We have the exception path; the interrupt path and the privilege-checked CSR access path are next.