Control and Status Registers

The Control and Status Registers (CSRs) are RISC-V’s separate, 4096-entry register file for everything that does not belong in the 32 general-purpose integer registers: privilege state, trap vectors, interrupt enables, performance counters, memory-protection configuration, and floating-point status. They live in a 12-bit address space (csr[11:0], giving 2^12 = 4096 possible CSRs), they can only be touched by the six instructions of the Zicsr extension (csrrw, csrrs, csrrc and their immediate variants), and the same 12-bit encoding that names them also encodes the minimum privilege level required to access them and whether they are read-write or read-only (Zicsr spec; Privileged ISA, machine-level CSR table). The privileged ISA defines a canonical machine-mode set (mstatus, misa, mie, mip, mtvec, mscratch, mepc, mcause, mtval, mhartid, mcounteren, mcycle, minstret, …) whose precise field semantics are described using the WARL / WPRI / WLRL conventions explained below.

The reader who has not yet seen RISC-V’s privilege model should skim RISC-V Privilege Modes first, since CSR access rules are stated in terms of M-mode, S-mode, and U-mode. The CSRs that do trap routing live in RISC-V Trap Handling; the CSRs that delegate traps to S-mode are described in RISC-V Trap Delegation; the syscall instruction that uses several of them is ecall Instruction; the ABI built on top of M-mode traps is the Supervisor Binary Interface. This note is the central reference for what CSRs are and how they are accessed; the sibling notes lean on it.

Mental Model: A Side Bus to a Tiny Register File

A general-purpose register (x0..x31) is wired directly into the datapath. CSRs are not. They sit in a separate, much smaller register file with its own address space and its own access port. The Zicsr extension is the only legal way to reach that port. Whenever a programmer or the hardware needs to read or modify processor state that is not “an integer the program is computing with” (privilege, interrupt mask, the address the trap handler runs at, a cycle counter), the path is through one of the six Zicsr instructions, each of which performs a single atomic read-modify-write against one CSR.

flowchart LR
  subgraph CORE["RISC-V hart"]
    direction LR
    GPR["General-purpose<br/>register file<br/>(x0..x31)"]
    ALU["ALU / pipeline"]
    GPR --> ALU
    ALU --> GPR
  end
  subgraph CSRFILE["CSR file (12-bit address space, 4096 slots)"]
    direction TB
    M["Machine-level CSRs<br/>0x300..0x3FF (RW)<br/>0xF11..0xF14 (RO)<br/>0xB00..0xB1F (counters)"]
    S["Supervisor-level CSRs<br/>0x100..0x1FF, 0xD00..0xDFF"]
    U["Unprivileged CSRs<br/>0x001..0x00F, 0xC00..0xC1F"]
  end
  ALU -. "csrrw / csrrs / csrrc<br/>(Zicsr opcode SYSTEM)" .-> CSRFILE
  CSRFILE -. "old value (rd)" .-> GPR
  TRAP["Trap entry hardware"]
  IRQ["Interrupt controllers<br/>(CLINT, PLIC)"]
  TRAP -- "writes mepc, mcause, mstatus.MPP/MPIE" --> M
  IRQ -- "drives mip bits" --> M

The CSR file is a parallel register file with its own naming, its own access path, and its own clients. What it shows: software talks to it only via Zicsr instructions, while hardware (trap entry, interrupt controllers) writes some CSRs directly as side effects of events. The insight to take: CSRs are the shared notebook between hardware and software, and the rules about who can read or write each entry are baked into the 12-bit address itself.

Mechanical Walk-through: From a 12-bit Address to Bits in a Register

The address space and what its bits mean

The privileged spec allocates a flat 12-bit address space for CSRs and partitions it by the high four bits of the address itself, so the encoding tells you both who can touch the CSR and whether it is read-only. The exact partition, copied from the machine-level CSR access table in the privileged spec, is (Privileged ISA, “CSR Listing”):

csr[11:10]csr[9:8]meaning
0000unprivileged read/write, 0x000..0x0FF
0001supervisor read/write, 0x100..0x1FF
0010hypervisor / VS read/write, 0x200..0x2FF
0011machine read/write, 0x300..0x3FF
01..10(same priv)further read/write blocks (debug, custom)
1100..11read-only blocks (0xC00..0xC7F unprivileged RO, 0xF00..0xF7F machine RO, …)

Two facts fall out of the table. First, csr[11:10] = 11 marks a register as read-only, so any attempt to write into 0xC00 (cycle), 0xF11 (mvendorid), 0xF14 (mhartid), or any other address in that quadrant from any privilege level raises an illegal-instruction trap regardless of mode (the immediate forms of csrrs/csrrc with rs1 = x0 do not write, however, and so do not trap; see “special-case writes” below). Second, csr[9:8] sets the minimum privilege: an M-mode CSR (csr[9:8] = 11) is unreachable from S-mode or U-mode, and the attempt is, again, an illegal-instruction exception (Zicsr spec, enforcement described in the privileged spec section on CSRs).

The naming convention then mirrors the address-space partition. Names that start with m are machine-mode (mstatus, mtvec, mcause), names that start with s are supervisor-mode (sstatus, stvec, scause), names that start with u were the old user-mode N-extension trap CSRs (utvec, uepc) and have been removed from the ratified privileged spec; counters (cycle, time, instret) and the floating-point CSRs (fflags, frm, fcsr) live in the unprivileged quadrant. The recurring pattern of “the M-mode version of CSR X exists at address 0x3xx, the S-mode version at 0x1xx” makes the address layout easy to memorize once the convention clicks.

The six instructions of Zicsr

All CSR access goes through the Zicsr (“Z” for the standard-but-optional flag, “icsr” for “integer CSR”) extension, six instructions sharing the SYSTEM major opcode 0b1110011 (0x73) and distinguished by the funct3 field:

mnemonicfunct3sourcesemantics
csrrw rd, csr, rs1001registeratomically write rs1 to CSR, return old value in rd
csrrs rd, csr, rs1010registeratomically OR rs1 into CSR (set bits), return old value in rd
csrrc rd, csr, rs1011registeratomically AND NOT rs1 into CSR (clear bits), return old value in rd
csrrwi rd, csr, zimm1015-bit immediatesame as csrrw, but rs1 field is a zero-extended 5-bit immediate
csrrsi rd, csr, zimm1105-bit immediatesame as csrrs with immediate source
csrrci rd, csr, zimm1115-bit immediatesame as csrrc with immediate source

The 12-bit CSR address sits in instruction bits 31:20 (the I-type immediate slot, here repurposed as the CSR specifier). The semantics quoted from the Zicsr chapter of the ratified spec are crisp: “All CSR instructions atomically read-modify-write a single CSR, whose CSR specifier is encoded in the 12-bit csr field of the instruction held in bits 31-20. The immediate forms use a 5-bit zero-extended immediate encoded in the rs1 field” (Zicsr spec).

A subtlety that catches people: each of these instructions is one machine-level atomic read-modify-write. That matters because mstatus.MIE (the global interrupt enable) lives in a single bit, and turning interrupts off then back on via two arithmetic operations would leak a window in which an interrupt could fire. A single csrrci x0, mstatus, 8 (clear bit 3) atomically masks interrupts, and the matching csrrsi x0, mstatus, 8 atomically unmasks them.

Special-case writes: rd = x0 and rs1 = x0

The Zicsr semantics carve out two exemptions that exist precisely so software can pretend there are separate “read CSR” and “write CSR” instructions even though there are not:

  • csrrw rd=x0, csr, rs1: the spec states, “If rd=x0, then the instruction shall not read the CSR and shall not cause any of the side effects that might occur on a CSR read” (Zicsr spec). So csrrw x0, mtvec, t0 is a pure write of t0 into mtvec, the assembler pseudo-instruction csrw mtvec, t0.
  • csrrs/csrrc with rs1=x0: from the same chapter, “For both insn:csrrs[] and insn:csrrc[], if rs1=x0, then the instruction will not write to the CSR at all, and so shall not cause any of the side effects that might otherwise occur on a CSR write, nor raise illegal-instruction exceptions on accesses to read-only CSRs.” So csrrs t0, mhartid, x0 is the pseudo csrr t0, mhartid, a pure read, and crucially it works on the read-only mhartid CSR without trapping.

This is why the assembler exposes csrr, csrw, csrs, csrc, csrwi, csrsi, csrci as friendly pseudo-instructions: each lowers to one of the six real Zicsr opcodes with rd or rs1 forced to x0.

Field-type conventions: WARL, WPRI, WLRL

Most CSR fields are described in the privileged spec using one of three abbreviations that govern what reads and writes are legal. These are not enforced by the hardware in a uniform way; they are a contract between the spec and the implementer that tells software how much it can trust a read-back. The definitions, from the privileged spec (Privileged ISA, CSR conventions):

  • WPRI (Reserved Writes Preserve Values, Reads Ignore Values). Reserved or future-use fields. Software must preserve their values when writing other parts of the register (read-modify-write, do not zero them out) and must ignore what it reads back. This is how the spec keeps room to add fields later without breaking software.
  • WLRL (Write/Read Only Legal Values). Fields whose legal values are a defined set. Writing a non-legal value is undefined; reading back after a non-legal write may return garbage. mcause’s exception-code field is WLRL: it is only guaranteed to hold values that appear in the standard cause table.
  • WARL (Write Any, Read Legal). Fields where any write is permitted but the hardware silently coerces the result to one of the legal values, and reads always return a legal value. mtvec.BASE is WARL: software can write a misaligned address, but the hardware clips low bits to satisfy alignment, and a subsequent read returns the clipped value. This is the most software-friendly convention because it lets a kernel probe a field by writing then reading.

What software actually does with each canonical CSR

The privileged spec’s allocated address table (machine-level CSR list, lines 367-385) anchors the M-mode CSRs the user’s project, definitely-not-esp32, must implement for its RV32IMC core. Their canonical addresses (verified against the spec, not recalled from memory; the popular rvemu book lists mtvec at 0x303 which conflicts with mideleg and is wrong) are listed below, each annotated with its single-paragraph job.

Machine information (read-only, address quadrant 0xF1x)

  • 0xF11 mvendorid: 32-bit JEDEC vendor code, or 0 for “non-commercial implementation.”
  • 0xF12 marchid: an architecture ID assigned by RISC-V International; 0 = “open implementation.”
  • 0xF13 mimpid: implementation version, vendor-defined.
  • 0xF14 mhartid: integer ID of the hardware thread; the boot code reads this to discriminate hart 0 from secondary harts. Always read-only.

Machine trap setup (RW, 0x30x)

  • 0x300 mstatus: the heart of the privilege model. Holds global interrupt-enable bits (MIE, SIE), the previous-interrupt-enable stack (MPIE, SPIE), the previous-privilege stack (MPP two bits, SPP one bit), plus modifier bits like MPRV (modify privilege for loads/stores), MXR (make executable readable), SUM (supervisor user-memory access). Per the spec, “When a trap is taken from privilege mode y into privilege mode x, __x__PIE is set to the value of __x__IE; __x__IE is set to 0; and __x__PP is set to y” (machine.adoc lines 397-398). This is the privilege stack RISC-V Trap Handling elaborates.
  • 0x301 misa: WARL register reporting the implemented ISA. The high two bits (MXL) encode XLEN (1 = 32, 2 = 64, 3 = 128), and the low 26 bits are a bit per uppercase letter A..Z (I = base integer, M = mul/div, C = compressed, A = atomics, F = single-precision float, D = double, etc.). A core can advertise that it implements M and C by setting bits 8 (I), 12 (M), and 2 (C). The register being WARL means an implementation may make some extension bits read-only zero if it does not implement them.
  • 0x302 medeleg: machine exception delegation. A bitmask where bit i delegates the synchronous exception with cause code i directly to S-mode. The spec mandates medeleg[11] (ecall-from-M) read-only zero (machine.adoc lines 1329-1331). Described in detail in RISC-V Trap Delegation.
  • 0x303 mideleg: machine interrupt delegation. Same idea, for interrupts.
  • 0x304 mie: machine interrupt-enable mask. Bit positions match mip and mcause’s interrupt half (bit 3 = MSIP, bit 7 = MTIP, bit 11 = MEIP for machine software, timer, external).
  • 0x305 mtvec: trap vector base address plus a 2-bit MODE field in the low bits. MODE = 0 (Direct) sends every trap to BASE; MODE = 1 (Vectored) sends synchronous exceptions to BASE but interrupts to BASE + 4 * cause (machine.adoc lines 1203-1220). The remaining MODE values are reserved.
  • 0x306 mcounteren: counter-enable bitmask. Each bit grants S-mode and U-mode read access to a corresponding counter CSR (bit 0 = cycle, bit 1 = time, bit 2 = instret). Without this, a user-mode rdcycle traps as illegal-instruction.

Machine trap handling (RW, 0x34x)

  • 0x340 mscratch: a free machine-mode pointer. Almost universally used to stash a per-hart trap-context pointer so the trap stub can atomically swap with sp to bootstrap a stack.
  • 0x341 mepc: machine exception PC. On trap entry, hardware writes the virtual address of the trapping instruction (for synchronous exceptions including ecall) or the instruction that would have executed next (for interrupts). MRET returns to the address in mepc. mepc is WARL and its bit 0 is always zero (machine.adoc lines 1693-1718).
  • 0x342 mcause: trap cause. High bit set = interrupt, clear = synchronous exception; the rest is the exception code from the table (machine.adoc lines 1774-1900). The exception-code field is WLRL.
  • 0x343 mtval: trap value. Populated for faulting traps with the diagnostic value: the faulting virtual address for misaligned, access-fault, and page-fault exceptions; the instruction bits for illegal-instruction. Zero (or unspecified) for ecall.
  • 0x344 mip: machine interrupt pending. Read-only-ish; bits like MTIP (machine timer pending) and MEIP (machine external pending) are driven by the CLINT and PLIC respectively; software-writable bits like MSIP can be set by writing the corresponding bit in the CLINT MMIO.

Machine counters and timers (RW, 0xB0x..0xB1x)

  • 0xB00 mcycle (and 0xB80 mcycleh for the upper 32 bits on RV32): MXLEN-bit cycle counter, counts clock cycles since reset.
  • 0xB02 minstret (and 0xB82 minstreth): instructions retired counter.
  • 0xB03..0xB1F mhpmcounter3..mhpmcounter31: 29 platform-defined performance-monitoring counters.

There is also 0xF15 mconfigptr (pointer to a configuration data structure, may be zero if not implemented), the PMP address and configuration registers at 0x3A0..0x3EF (covered by Physical Memory Protection), and the menvcfg environment-configuration register at 0x30A. The user’s RV32IMC project does not need any S-mode CSRs in v1.0 because it does not implement S-mode; it can also leave the H-extension and N-extension CSRs entirely unimplemented.

Permission semantics: what happens when a CSR access is illegal

The privileged spec specifies three failure conditions for a CSR access (Privileged ISA, CSR access permissions):

  1. Insufficient privilege: accessing a CSR whose csr[9:8] field requires a privilege the hart is not currently in. Reading or writing 0x300 (mstatus) from U-mode raises an illegal-instruction exception (cause = 2).
  2. Writing a read-only CSR: a real write (csrrw with any rd, csrrs/csrrc with rs1 ≠ x0) to a CSR whose csr[11:10] = 11 raises an illegal-instruction exception. The carve-out from the Zicsr spec is important: “if rs1 = x0, [csrrs/csrrc] shall not… raise illegal-instruction exceptions on accesses to read-only CSRs,” meaning csrr t0, mhartid is well-defined.
  3. Accessing an unimplemented CSR: the spec treats an attempt to access a CSR address that the implementation does not implement as an illegal-instruction exception. (Some implementations choose to alias unimplemented addresses to zero, but the ratified text mandates the trap.)

The illegal-instruction trap that results, like all such traps, has mcause set to 2 and mtval set to the encoding of the offending instruction (so the handler can see which CSR access failed).

Configuration / Code / Specification

Example 1: a minimal trap-vector setup in M-mode

This is the kind of code the user’s microkernel will run at boot. It writes mtvec with the address of the trap-entry stub in direct mode, sets mscratch to a per-hart context pointer, and unmasks machine interrupts.

# Set trap vector to direct mode, base = trap_entry.
# MODE field is the low 2 bits; direct = 00. So writing the symbol
# address (which must be 4-byte aligned) into mtvec is sufficient.
    la    t0, trap_entry
    csrw  mtvec, t0           # pseudo: csrrw x0, 0x305, t0
 
# Stash per-hart context pointer (trap_ctx_for_this_hart).
    la    t0, trap_ctx_array
    csrr  t1, mhartid          # pseudo: csrrs t1, 0xF14, x0
    slli  t1, t1, CTX_SHIFT    # context-size index
    add   t0, t0, t1
    csrw  mscratch, t0
 
# Enable machine-timer and machine-external interrupts in mie.
    li    t0, (1 << 7) | (1 << 11)    # MTIE | MEIE
    csrs  mie, t0              # pseudo: csrrs x0, 0x304, t0
 
# Globally enable interrupts in mstatus.MIE (bit 3).
    csrsi mstatus, (1 << 3)    # pseudo: csrrsi x0, 0x300, 8

Line-by-line: la t0, trap_entry materializes the address of the handler; csrw mtvec, t0 writes it to address 0x305 with MODE = 0. csrr t1, mhartid is csrrs t1, mhartid, x0, which reads the read-only mhartid without touching the CSR otherwise. The csrs mie, t0 is csrrs x0, mie, t0, atomically setting the MTIE and MEIE bits without disturbing other bits, and without ever reading the result (rd = x0). The final csrsi mstatus, 8 sets bit 3 (MIE) using the 5-bit-immediate variant; the immediate is zero-extended, so 8 == 0b01000 sets bit 3 only.

The privileged spec describes how to probe a WARL register: write a candidate value, read back, observe the coerced result. mtvec.MODE is WARL; an implementation that only supports direct mode will coerce MODE = 1 to 0 on a write-then-read cycle.

    li    t0, 1               # try MODE = 1 (vectored)
    csrrw t1, mtvec, t0       # write candidate, save old value in t1
    csrr  t2, mtvec           # read back what stuck
    andi  t2, t2, 0x3         # isolate MODE field
    csrw  mtvec, t1           # restore original mtvec
    # t2 now holds 0 (only direct supported) or 1 (vectored supported).

Example 3: a portable assembler convention to enable interrupts

To enable machine interrupts globally, atomically, in a single instruction:

    csrsi mstatus, 0x8    # set mstatus.MIE (bit 3) atomically

The 5-bit-immediate form encodes 0x8 = 0b01000, which sets only bit 3 of mstatus. Crucially the operation is a single read-modify-write that the implementation must execute atomically with respect to interrupts, so the moment between “interrupts disabled” and “interrupts enabled” is exactly one instruction-retire boundary.

Failure Modes and Common Misunderstandings

The single most common mistake is confusing CSR addresses with general-purpose register numbers. They share a “twelve-bit immediate field” in the SYSTEM instruction encoding, but they live in entirely separate name spaces; csrr x0, x5 is not a CSR access at all, it is a syntax error (or, depending on the assembler, an obscure pseudo-instruction).

A subtler trap: read-modify-write through a non-atomic sequence does not work. Code that writes lw t0, (mstatus_shadow); ori t0, t0, 0x8; sw t0, (mstatus_shadow); csrw mstatus, t0 is broken because the hardware can update mstatus between the load and the store (for instance, by taking a trap which clears MIE). The correct pattern is csrsi mstatus, 0x8, the atomic set-bit form.

A third confusion is WARL vs WLRL: WARL is software-friendly (write anything, the hardware coerces), WLRL is software-hostile (write only legal values, otherwise undefined). Treating an mcause exception code as WARL and writing 99 to it produces implementation-defined garbage; treating it as WLRL and only writing values from the spec’s cause table keeps software portable.

A fourth mistake is forgetting that some bits of mstatus are read-only-zero when a feature is absent. The spec writes things like “MXR is read-only 0 if S-mode is not supported or if satp.MODE is read-only 0” (machine.adoc line 609). On a pure M-mode + U-mode RV32 core (the v1.0 of definitely-not-esp32), SIE/SPIE/SPP/MXR/SUM are all read-only zero, and an attempt to set them is silently dropped by the WARL behavior of mstatus.

Finally, the CSR address space is global to a hart, not per-process. There is no “user copy” of mstatus; the same physical bits are time-multiplexed across all software running on the hart. A trap handler must save/restore CSR state explicitly if it wants to preserve it across a context switch (mscratch is the conventional staging area for the per-context CSR shadow).

Alternatives and When to Choose Them

A few other architectures handle “non-GPR processor state” differently, and the contrast clarifies why RISC-V landed where it did.

x86 model-specific registers (MSRs) are accessed via rdmsr and wrmsr instructions, indexed by a 32-bit identifier in ecx, with the value in edx:eax. The address space is much larger (effectively 2^32), the operation is non-atomic in the sense that interrupts may fire between rdmsr and wrmsr if software wants RMW, and many MSRs require ring 0. The RISC-V choice of a tight 12-bit space and a built-in atomic RMW primitive (csrrs/csrrc) makes the kernel code substantially shorter.

ARM system registers use the mrs/msr instructions and a structured encoding (op0, op1, CRn, CRm, op2) instead of a flat address; the structure encodes which coprocessor and which register. The ARM scheme is more extensible but is harder to disassemble at a glance. RISC-V trades extensibility for simplicity: every CSR is a flat 12-bit number you can look up in one table.

MIPS Coprocessor 0 (CP0) puts privilege state in a literal coprocessor, accessed via mfc0/mtc0. The model is similar to ARM (structured register identifier), simpler than x86, but unlike RISC-V the operations are not atomic read-modify-write, so MIPS kernel code has explicit interrupt-disable sequences around RMW patterns.

For a small RV32 implementation like definitely-not-esp32’s, the Zicsr design is essentially free in silicon: each CSR is a tiny register or a comparator into a fixed location, and the six instructions decode to a single ALU + write-back operation. The 12-bit space is small enough that an implementation can decode the address with a flat case statement and not worry about a large lookup tree.

Production Notes

The ratified privileged ISA spec is Machine ISA v1.13 (per release 20240411) plus the Supervisor ISA additions in the 20241101 interim release, both available from RISC-V International’s specifications page (Ratified Specifications, release notes on GitHub). The canonical machine CSR list shown above has been stable across recent revisions; what has shifted is the Sm/Ss extension prefixes (Smaia, Sscofpmf, Ssccfg, …) that add new CSRs without renaming the old ones.

In real chips, the M-mode CSR set is implemented in two layers. mstatus, mtvec, mepc, mcause, mtval, mscratch, mhartid, and the counter pair are essentially always present (and the rocket-chip, BOOM, CVA6, and SiFive 7-series cores all expose them at the canonical addresses). misa, medeleg, mideleg, mie, mip, mcounteren, the PMP set, and the performance counters are negotiable: a deeply embedded M-mode-only core (the Espressif ESP32-C3, which the user’s project benchmarks against) does not implement S-mode at all and omits the supervisor CSRs entirely.

Linux’s RISC-V port reads misa only for diagnostics during boot (arch/riscv/kernel/cpufeature.c parses it), and relies on the Device Tree / ACPI for primary feature detection. The actual trap-entry path is in arch/riscv/kernel/entry.S and uses sscratch (the S-mode mscratch) to swap in the kernel tp (thread pointer) on each entry, the canonical pattern for trap context bootstrap (covered in RISC-V Trap Handling).

For definitely-not-esp32 specifically: v1.0’s M+U design needs to implement mstatus, mtvec, mepc, mcause, mtval, mscratch, mhartid, mie, mip, mcycle, minstret, the PMP set, and arguably mcounteren (to let U-mode read cycle for benchmarks). misa is recommended even though it can be read-only zero in the simplest implementations. medeleg and mideleg should be unimplemented (they make sense only when S-mode is present); the spec says, “In harts without S-mode, the medeleg and mideleg registers should not exist” (machine.adoc line 1252).

See Also