No. 49 / project of 147 on the ladder

M↔S privilege mode tracking

introduces — real cur_priv register, mstatus.MPP wired through trap entry/mret, ECALL cause routes by priv mode, WARL coerce on MPP

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

P49 makes mstatus.MPP real. The chip now has a 2-bit cur_priv register, mret returns to the priv-when-trapped, and the ECALL cause code reflects priv-at-call.

Status: RTL pass. Probes (WARL + storage + full M→S→trap→M round trip with probe-local trap handler) all PASS at 5,102,225 clocks. FreeRTOS demo runs to halt cleanly afterwards.

What changed in the RTL

  • New cur_priv register, resets to M (2'b11).
  • enter_trap captures cur_priv into mstatus.MPP and sets cur_priv <= M.
  • mret restores cur_priv from mstatus.MPP, then sets MPP back to M (post-mret MPP defaults to least-priv-supported per spec; we pick M so a stuck-in-S bug fails loudly).
  • ECALL cause = M/S/U-mode based on cur_priv at the call.
  • WARL on mstatus.MPP: writes of U (2'b00) and reserved 2'b10 coerce to S (2'b01). M and S pass through. We don’t have U-mode hardware so this keeps a software typo from silently entering an unsupported state.

What still goes to M

Every trap. We don’t yet consult medeleg/mideleg for routing, so even from S-mode the trap goes to mtvec and the handler runs in M. That’s spec-correct behavior when medeleg=0/mideleg=0 (which they are at reset), and it lets the existing FreeRTOS trap handler keep working unchanged.

S-mode trap routing is the next big rung.

Probe scope

priv_tracking_probe (error codes 92-97):

  • mstatus.MPP storage round-trip for both M and S.
  • WARL coerce of U (2'b00) and reserved (2'b10) → S.
  • Independent of other mstatus bits (no smearing).
  • Restoring MPP=M before handing off to FreeRTOS.

priv_transition_probe (error codes 100-104) — full M→S→trap→M round trip:

  1. Save mtvec/mstatus, install probe-local naked handler.
  2. Set mepc to a forward label, write mstatus with MPP=S, run mret. Chip switches cur_priv to S.
  3. The label runs ecall. Trap fires; chip captures cur_priv=S into MPP, sets cur_priv=M, jumps to handler.
  4. Handler captures mcause/mstatus, sets MPP=M, advances mepc past the ecall, runs mret. Execution resumes after the ecall in M-mode.
  5. Probe verifies mcause==MCAUSE_ECALL_SMODE, captured MPP==S, and that a follow-up ecall yields MCAUSE_ECALL_MMODE. Restore mtvec/mstatus.

This exercises the full priv-transition path end to end on the chip. The remaining work (S-mode trap routing through stvec instead of mtvec) is the next rung.

One RTL fix surfaced

csr_mtvec_hi was 10 bits wide ([11:2]) — capping mtvec at 4 KiB. The FreeRTOS handler at 0x100 was always under that, so nothing noticed; our probe handler at 0x51f8 was not. P49 widens to the full 30-bit base ([31:2]).

Files

  • src/top.sv - priv register + MPP wiring + mret restore + ECALL cause
  • app/main.c - priv_tracking_probe

Harden

NOT RUN. Estimated +25 cells over P48 (2-bit priv reg, MPP mux, WARL coerce mux).

What just happened?

The chip can finally distinguish M from S in priv state. That’s the first real behavioral step into the privileged-architecture spec; everything before P49 was M-mode-only with the S CSRs as storage. The next rung is S-mode trap routing.