Zicsr Extension
The Zicsr extension defines six instructions that atomically read-modify-write a single Control and Status Register (CSR):
csrrw,csrrs,csrrc, and their immediate variantscsrrwi,csrrsi,csrrci. The CSR address is encoded as a 12-bit field, giving 4096 register slots per hart, of which the privileged spec assigns specific ones (mstatus,mtvec,mepc,mcause, … see Control and Status Registers). Zicsr was originally part of base I; it was retrospectively detached into its own extension in the v2.2 revision after the working group recognized that “CSRs might not be present on very low-end hardware” (Red Hat Research, RISC-V extensions). For any chip that wants traps, interrupts, timers, or privilege levels, Zicsr is mandatory in practice. The current ratified version is Zicsr v2.0, indexed alongside the base in the RISC-V Unprivileged ISA reference.
This note is about the instructions the extension defines and their semantics, the encoding, and the rationale for the split. The catalog of CSRs themselves (what mstatus.MIE controls, what mtvec points at, etc.) belongs to Control and Status Registers and the privilege model lives in RISC-V Privilege Modes.
Mental Model
flowchart LR subgraph INSN["Single CSR instruction"] OP["csrrw rd, csr, rs1"] end subgraph ATOMIC["Atomic read-modify-write"] R["1) read CSR -> tmp"] W["2) write rs1 -> CSR"] WB["3) write tmp -> rd"] R --> W --> WB end OP --> ATOMIC ATOMIC --> EFFECT["Single architecturally-visible step.<br/>No interrupt can split the RMW."]
A single Zicsr instruction is an atomic three-step on architectural state. What it shows: the value of the CSR is captured into a temporary, the new value from rs1 is written into the CSR, and the captured value is written to rd. No interrupt or exception can split the read from the write; both effects are either fully visible or fully not. The insight to take: Zicsr is the only way software touches privileged machine state in RISC-V. There is no MOV-from-CSR or MOV-to-CSR; every access goes through one of these six instructions and inherits their atomic RMW semantics.
The Six Instructions
All six are I-type encodings, with the 12-bit imm field repurposed as the CSR address (bits 31:20 of the instruction word) (RISC-V Unprivileged ISA, 2024-11-26 release).
31 20 19 15 14 12 11 7 6 0
[ csr[11:0] ][ rs1 ][funct3][ rd ][opcode] ; for csrrw, csrrs, csrrc
[ csr[11:0] ][ uimm5 ][funct3][ rd ][opcode] ; for csrrwi, csrrsi, csrrci
The opcode is always 0b1110011 (SYSTEM, shared with ecall and ebreak which use different funct3/funct12 combinations). The funct3 field selects which of the six CSR instructions; values are 001/010/011 for csrrw/csrrs/csrrc and 101/110/111 for the immediate variants (RISC-V docs, Zicsr).
CSRRW: atomic swap
csrrw rd, csr, rs1 (CSR Read/Write) is the simplest of the three families (RISC-V docs, Zicsr):
- Atomically read the current value of
csrinto a temporary. - Write
rs1intocsr. - Zero-extend the temporary to
XLENbits and write it tord.
In one line: rd, csr = csr, rs1 performed atomically.
A common idiom is csrrw x0, csr, rs1, which writes without bothering to read. The spec calls this out explicitly: “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.” This matters because many CSRs (counter registers, interrupt-pending registers) have read-side-effects (clear-on-read semantics, for example, do not exist in the base spec but can be implementation-defined for performance counters). By writing x0 as the destination, software signals it does not want the read at all; the hardware skips it.
CSRRS: atomic read-and-set-bits
csrrs rd, csr, rs1 (CSR Read and Set Bits) treats rs1 as a bitmask:
- Atomically read
csrinto a temporary. - Compute
csr | rs1and write it back tocsr. - Zero-extend the temporary to
XLENbits and write tord.
So any bit set in rs1 becomes set in csr (subject to writability constraints on per-bit basis), while bits not set in rs1 are unchanged.
The mirror idiom is csrrs rd, csr, x0, which reads but does not write. With rs1 = x0 the spec says: “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”. This is the canonical read-only CSR access; the assembler exposes it as csrr rd, csr (a pseudoinstruction expanding to csrrs rd, csr, x0).
CSRRC: atomic read-and-clear-bits
csrrc rd, csr, rs1 is the dual: any bit set in rs1 becomes cleared in csr. Same atomic RMW, same rs1 = x0 behavior (no write, no side effect). This is how you disable interrupts: li t0, MIE_BIT; csrrc x0, mstatus, t0.
Immediate variants: csrrwi, csrrsi, csrrci
The i-suffixed forms take a 5-bit zero-extended unsigned immediate (in the position the rs1 field would otherwise occupy) instead of a register value (RISC-V docs, Zicsr). The immediate covers small bitmasks (0-31) which is exactly the range you need for things like “set bit 3 in this CSR” or “write value 7 to this 3-bit field”. For larger values you must use the register form.
The immediate variants exist because so many CSR operations need only a handful of distinct bit positions and burning a temporary register on li t0, 8 before every csrrs would be wasteful in tight trap-handling code.
Pseudoinstructions
The assembler exposes friendlier mnemonics for common patterns:
| Pseudo | Expands to | Meaning |
|---|---|---|
csrr rd, csr | csrrs rd, csr, x0 | read only, no write |
csrw csr, rs1 | csrrw x0, csr, rs1 | write only, no read |
csrs csr, rs1 | csrrs x0, csr, rs1 | set bits, discard old |
csrc csr, rs1 | csrrc x0, csr, rs1 | clear bits, discard old |
csrwi csr, imm | csrrwi x0, csr, imm | immediate write |
csrsi csr, imm | csrrsi x0, csr, imm | immediate set |
csrci csr, imm | csrrci x0, csr, imm | immediate clear |
These cover the vast majority of real CSR accesses. csrrw with both rd and rs1 non-zero is rare; the typical use case is bootstrapping (swap the trap vector into mscratch atomically while installing a new one).
Atomic Read-Modify-Write: What Atomic Means Here
The spec’s exact wording: “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.” (RISC-V Unprivileged ISA). What “atomic” means here is “within the hart’s own observation”: no trap, interrupt, or other CSR access on the same hart can interleave between the read and the write. Across multiple harts this is not a global memory-ordering primitive; CSRs are per-hart state, and the few inter-hart CSR effects (writing to interrupt-pending bits to signal another hart’s IPI) go through dedicated mechanisms outside Zicsr.
The atomicity matters most for trap handlers. Consider disabling interrupts: a naive read-modify-write sequence (csrr t0, mstatus; andi t0, t0, ~MIE; csrw mstatus, t0) has a window between the read and the write during which an interrupt could fire, change mstatus (the trap handler always saves and restores other bits), and lose the update. With csrrc x0, mstatus, MIE_BIT the entire RMW happens in a single architectural step. There is no window.
The implementation cost is small. CSRs are typically a small set of flip-flops or a tiny register file; a one-cycle read-modify-write is straightforward. The CSR access usually completes in a single cycle on the boundary between two pipeline stages (or in its own dedicated micro-op slot), with the write committing only on instruction retirement.
Program-Order Semantics
The spec adds: “Each RISC-V hart normally observes its own CSR accesses, including its implicit CSR accesses, as performed in program order. In particular, unless specified otherwise, a CSR access is performed after the execution of any prior instructions in program order whose behavior modifies or is modified by the CSR state and before the execution of any subsequent instructions in program order whose behavior modifies or is modified by the CSR state.” (RISC-V docs, Zicsr).
This is much stronger than the standard RVWMO memory model permits for ordinary memory accesses. CSR accesses are serialized against the surrounding instruction stream: a csrw mtvec, t0 cannot be reordered with a subsequent ecall that depends on the trap vector being installed. Out-of-order implementations have to flush or stall around CSR writes to maintain this; the result is that CSR-write-heavy code is one of the few RISC-V workloads that breaks an out-of-order engine. Trap handlers are not hot paths, so the cost is acceptable.
A handful of CSRs are explicitly relaxed from this rule (the floating-point status fcsr and rounding-mode bits, where serializing every FP operation behind an fcsr access would be expensive); see Chapter 11 of the manual.
Why Zicsr Is a Separate Extension
The Zicsr instructions used to live in the base I integer set. They were split out in the v2.2 revision (around 2017) along with fence.i (now Zifencei) (Red Hat Research, GCC mailing list discussion). The technical motivation was simple:
- Some chips do not have CSRs at all. A pure-compute accelerator (think of a tensor-multiply unit instantiated as a RISC-V hart for compiler-targeting convenience) needs only
add,mul, branches, and loads/stores. It has no privilege levels, no traps, no timer, and no interrupt sources, so it has no use formstatus/mtvec/etc. Forcing such a chip to implement Zicsr would force it to implement at least some CSR semantics, even if they are read-as-zero. The split lets such cores claim conformance torv32iwithout lying. - The split is retrospective. Most real chips implemented the original “base I plus CSRs” set, and toolchains assumed CSRs were available. The split caused a real toolchain headache: GCC 11 and earlier defaulted to ISA spec v2.2 (CSRs in base); GCC 12 bumped to v20191213 (CSRs in Zicsr). Code compiled with one and linked with the other can produce confusing “illegal instruction” errors at runtime (GCC mailing list). The current convention is to always spell out Zicsr explicitly in
-march, for example-march=rv32imc_zicsr_zifencei. - Conformance independence. Once Zicsr is its own extension, it can be ratified, versioned, and amended independently. A future addition of new CSR instructions can go into Zicsr v2.1 without touching the base I spec.
The pragmatic upshot for everyone except micro-accelerator designers: always include Zicsr. The ESP32-C3, every Rocket variant, every Ibex variant, every Hazard3 variant, every Cortex-equivalent RISC-V core, all implement Zicsr. The “no CSRs” case is theoretical; the split exists for the spec’s cleanliness, not because pure RV32I cores are common in shipping silicon.
The CSR Address Space
The 12-bit CSR address field gives a 4096-entry space, partitioned by the privileged spec (RISC-V docs, Privileged CSRs). The top four bits encode access permissions:
- Bits [11:10]: read/write disposition.
00,01,10are read/write;11is read-only. - Bits [9:8]: minimum privilege level required to access.
00is User-accessible,01is Supervisor,10reserved/Hypervisor,11Machine-only.
So a CSR at address 0x300 (binary 0011_0000_0000) is read/write, accessible at M-mode only. 0xC00 (binary 1100_0000_0000) is read-only and User-accessible (this is cycle, the cycle counter). Attempting to access a CSR your current privilege level does not authorize raises an illegal-instruction exception (RISC-V docs, Privileged CSRs). The hardware decode does not need to look at the CSR’s read/write disposition before checking the privilege bits; that ordering matters for spec compliance under speculative execution.
A small sampling of standard CSRs the Zicsr instructions reach:
| Address | Name | Role |
|---|---|---|
0x300 | mstatus | machine-mode status, interrupt-enable, privilege stack |
0x301 | misa | ISA and supported extensions |
0x304 | mie | machine interrupt enables |
0x305 | mtvec | machine trap vector base |
0x340 | mscratch | machine scratch register |
0x341 | mepc | machine exception program counter |
0x342 | mcause | machine trap cause |
0x343 | mtval | machine trap value (faulting address, etc.) |
0x344 | mip | machine interrupt pending |
0x3A0-0x3BF | pmpcfg0-pmpcfg15 | PMP region configurations |
0x3B0-0x3EF | pmpaddr0-pmpaddr63 | PMP region addresses |
0xB00 | mcycle | cycle counter (RW) |
0xB02 | minstret | retired-instruction counter (RW) |
0xC00 | cycle | cycle counter (RO, user-visible) |
0xC01 | time | wall-clock time |
0xC02 | instret | retired-instruction counter (RO, user-visible) |
0xF11 | mvendorid | vendor ID (RO) |
0xF14 | mhartid | hardware thread ID (RO) |
The full table is in the privileged spec (RISC-V Privileged ISA) and the per-CSR semantics are in Control and Status Registers.
Worked Examples
Disabling interrupts atomically
To clear the machine interrupt-enable bit (mstatus.MIE, bit 3) without losing concurrent updates to other mstatus fields:
li t0, 0x8 # bitmask for MIE
csrrc x0, mstatus, t0 # atomically clear MIE in mstatusThe csrrc x0 form means “do not bother reading; just clear”. One instruction; no window. The equivalent on x86 would be the dedicated cli; here there is no dedicated instruction because the CSR access machinery is general.
Reading the cycle counter
csrr a0, cycle # a0 = current cycle count
# (expands to: csrrs a0, 0xC00, x0)This works in User mode because cycle (CSR 0xC00) is User-accessible. M-mode could read mcycle (0xB00) instead, which is the writable mirror.
Saving and swapping the trap vector during boot
la t0, my_trap_vector # t0 = new trap-vector address
csrrw t1, mtvec, t0 # atomically: t1 = old mtvec; mtvec = t0The previous trap vector is captured in t1 (for restoration later, perhaps to chain to a prior handler), and the new one is installed, in one atomic step. No window during which a trap would dispatch to a partially-written vector.
Setting up the machine-mode timer compare CSR
The CLINT exposes mtimecmp as memory-mapped state, not as a CSR. But to enable the timer interrupt the kernel writes mie:
li t0, 0x80 # bit 7 = MTIE (machine timer enable)
csrrs x0, mie, t0 # set bit 7 in mie; leave other bits aloneReturning from a trap
mret is not a Zicsr instruction (it is in the privileged spec, opcode SYSTEM with funct12 = 0x302), but it interacts directly with CSRs: it pops the mstatus.MPP/MPIE privilege stack and jumps to mepc. See RISC-V Trap Handling.
Failure Modes and Common Misunderstandings
- “Reading is free.” Not for all CSRs. Performance counters can have read-side-effects; reading-then-writing one in two separate instructions (instead of using
csrrw) can produce inconsistent values. Use the right form. - “The immediate variants take a register number.” No. They take a literal 5-bit zero-extended value.
csrwi mstatus, 0x8writes the constant8intomstatus; it does not write the contents ofx8. - “You can fence around a CSR access.” You usually do not need to. The spec already serializes CSR accesses against program order. The exception is multi-hart inter-CSR interaction (e.g., one hart writing an MIP bit to signal another), which needs an explicit
fenceto guarantee visibility ordering. - “
csrrw x0, csr, x0is a NOP.” Almost. It writesx0’s value (always 0) to the CSR. If the CSR has WARL (Write-Any, Read-Legal) semantics, this may silently fail to change anything, but it will trigger any write-side-effect. The actual NOP is omitted from the program. - “Zicsr provides full mutex semantics.” Only within one hart. CSRs are per-hart; cross-hart synchronization needs the A extension (load-reserved/store-conditional and AMOs) or memory-mapped synchronization primitives.
- “All chips implement all listed CSRs.” No. The privileged spec defines the address space; each implementation chooses which CSRs to provide. The ESP32-C3 implements only a subset of the privileged spec; reading a non-implemented CSR raises an illegal-instruction exception. (Cross-referenced from secondary summaries of the Espressif TRM; see the ESP32-C3 section in RV32IMC for the provenance caveat.)
Alternatives in Other ISAs
The Zicsr design is recognizably a deliberate counter-example to two prior conventions:
- x86’s MSRs. x86 puts model-specific registers behind
RDMSR/WRMSRopcodes, which are unprivileged-illegal but otherwise just MOVs. There is no atomic RMW form; you read, modify, and write in two instructions, and the windows have caused real bugs. RISC-V’s choice to provide atomic forms eliminates that class of bug at the ISA level. - ARM’s system registers. ARM uses
MRS(move-from-system-register) andMSR(move-to-system-register) as separate read and write operations. Again no atomic RMW; the canonical disable-IRQ idiom on ARMv8-A is a multi-instruction sequence. ARM compensates with extra instructions (MSR DAIFSet, #imm) for the most common cases, somewhat reproducing Zicsr’s immediate variants but without generality.
Zicsr’s atomic RMW with the bitmask immediates is, in this comparison, unusually clean: one instruction, no window, no special-case opcodes.
Production Notes
In the definitely-not-esp32 project the Zicsr implementation is one of the load-bearing pieces of the RTL. The CSR file is a flat array of registers (a few dozen of them; the chip does not implement the full 4096 address space), addressed by the 12-bit field; the decoder routes Zicsr instructions to it via a small mux that selects the right CSR; the read-modify-write happens in the Execute stage, and the writeback to rd happens in the standard Writeback stage; the side effects on, for example, mstatus.MIE are routed back to the interrupt-pending logic combinationally. The atomicity falls out of the in-order single-issue pipeline: only one CSR access is in flight at a time, and the entire instruction commits or does not.
The Rust embedded ecosystem provides the riscv crate which wraps Zicsr instructions in safe abstractions: riscv::register::mstatus::read() and riscv::register::mstatus::set_mie() compile down to single csrr/csrs instructions. The crate validates at compile time that the CSR access is at the right privilege level for the target. This is one of the cleaner places where Rust’s zero-cost abstractions earn their keep on bare metal.
A subtle real-world headache: when porting code between toolchain versions, the implicit Zicsr-in-base-I assumption from older GCC can cause silent breakage. The cure is the same as the diagnosis: always spell -march=rv32imc_zicsr_zifencei explicitly, never trust the toolchain default. Multiple distribution-maintainer complaints in the GCC and LLVM trackers concern exactly this confusion.
See Also
- Control and Status Registers: the catalog of CSRs the Zicsr instructions access
- RISC-V Privilege Modes: the privilege model that defines who can access which CSR
- RISC-V Trap Handling: the trap mechanism that consumes most CSR traffic
- RV32IMC: the base subset Zicsr is paired with in this project
- RISC-V Instruction Set Architecture: the parent ISA family
- Instruction Set Architecture: the general ISA concept
- Computer Architecture MOC: parent map