SOURCE ARCHIVE
EXTRACTED CONTENT
8,301 charsDAY 21 · PHASE 4 — ADVANCED & REAL HARDWARE
CSRs, Traps & Exceptions
By EcrioniX · Updated 2026-06-11
The pipeline is complete. Now we add the privilege layer — the mechanism that makes a CPU safe and supervisable. Control and Status Registers (CSRs) give software a way to control processor behaviour. Traps give the CPU a way to handle errors and system calls. Together they form the foundation of operating system support.
Key M-Mode CSRs
| CSR Name | Address | Purpose |
|---|---|---|
| mstatus | 0x300 | Global interrupt enable (MIE bit[3]), previous interrupt state (MPIE bit[7]) |
| mtvec | 0x305 | Trap handler base address — PC jumps here on any trap |
| mepc | 0x341 | Machine Exception PC — saves the PC of the trapped instruction |
| mcause | 0x342 | Cause of the trap (bit[31]=interrupt/exception, bits[30:0]=cause code) |
| mscratch | 0x340 | Scratch register for trap handler software use |
| mip | 0x344 | Machine Interrupt Pending — which interrupts are waiting |
| mie | 0x304 | Machine Interrupt Enable — which interrupts are enabled |
CSR Instructions
CSRs are accessed with three atomic read-modify-write instructions. All three return the old CSR value in rd before modifying it:
- CSRRW rd, csr, rs1 — atomically swap: rd = CSR; CSR = rs1
- CSRRS rd, csr, rs1 — set bits: rd = CSR; CSR = CSR | rs1
- CSRRC rd, csr, rs1 — clear bits: rd = CSR; CSR = CSR & ~rs1
// csr_file.v — M-mode Control and Status Register file
// Supports CSRRW, CSRRS, CSRRC and hardware trap signals
module csr_file (
input clk, rst,
// CSR instruction interface
input [11:0] csr_addr, // 12-bit CSR address from instruction
input [ 2:0] csr_op, // 001=CSRRW, 010=CSRRS, 011=CSRRC
input csr_we, // 1 = write CSR
input [31:0] csr_wdata, // write data (from rs1)
output reg [31:0] csr_rdata, // read data (old value, to rd)
// Hardware trap interface
input trap, // exception/interrupt asserted
input [31:0] trap_pc, // PC of trapped instruction
input [31:0] trap_cause, // mcause code
output [31:0] mtvec_out, // trap vector — where to jump on trap
output [31:0] mepc_out, // saved PC — used by MRET
// MRET
input mret // 1 = MRET instruction executing
);
// ── CSR storage ───────────────────────────────────────────────
reg [31:0] mstatus; // 0x300
reg [31:0] mie; // 0x304
reg [31:0] mtvec; // 0x305
reg [31:0] mscratch; // 0x340
reg [31:0] mepc; // 0x341
reg [31:0] mcause; // 0x342
reg [31:0] mip; // 0x344
assign mtvec_out = mtvec;
assign mepc_out = mepc;
// ── CSR Read (combinatorial) ──────────────────────────────────
always @(*) begin
case (csr_addr)
12'h300: csr_rdata = mstatus;
12'h304: csr_rdata = mie;
12'h305: csr_rdata = mtvec;
12'h340: csr_rdata = mscratch;
12'h341: csr_rdata = mepc;
12'h342: csr_rdata = mcause;
12'h344: csr_rdata = mip;
default: csr_rdata = 32'h0;
endcase
end
// ── CSR Write (synchronous) ───────────────────────────────────
task do_csr_write;
input [31:0] old_val;
input [31:0] wdata;
input [ 2:0] op;
output reg [31:0] new_val;
begin
case (op)
3'b001: new_val = wdata; // CSRRW
3'b010: new_val = old_val | wdata; // CSRRS
3'b011: new_val = old_val & ~wdata; // CSRRC
default: new_val = old_val;
endcase
end
endtask
always @(posedge clk or posedge rst) begin
if (rst) begin
mstatus <= 32'h0000_1800; // MPP=11 (M-mode)
mie <= 0; mtvec <= 0;
mscratch <= 0; mepc <= 0;
mcause <= 0; mip <= 0;
end else if (trap) begin
// Hardware trap: save PC, cause; jump to handler
mepc <= trap_pc;
mcause <= trap_cause;
// Clear MIE, save to MPIE
mstatus <= {mstatus[31:8], mstatus[3], mstatus[6:4], 1'b0, mstatus[2:0]};
end else if (mret) begin
// Return from trap: restore MIE from MPIE
mstatus <= {mstatus[31:8], 1'b1, mstatus[6:4], mstatus[7], mstatus[2:0]};
end else if (csr_we) begin
// Software CSR write
case (csr_addr)
12'h300: do_csr_write(mstatus, csr_wdata, csr_op, mstatus);
12'h304: do_csr_write(mie, csr_wdata, csr_op, mie);
12'h305: do_csr_write(mtvec, csr_wdata, csr_op, mtvec);
12'h340: do_csr_write(mscratch,csr_wdata, csr_op, mscratch);
12'h341: do_csr_write(mepc, csr_wdata, csr_op, mepc);
12'h342: do_csr_write(mcause, csr_wdata, csr_op, mcause);
default: ; // read-only or unimplemented CSR
endcase
end
end
endmodule
Trap Handling Flow
When an exception occurs (illegal instruction, misaligned address, ecall), the hardware automatically:
- Saves the current PC to
mepc - Writes the exception code to
mcause(e.g., 2 = illegal instruction, 11 = ECALL from M-mode) - Clears the MIE bit in
mstatus(disables further interrupts) - Jumps the PC to the address stored in
mtvec
The trap handler reads mcause to determine what happened, services the event, and executes MRET to return.
// tb_csr.v — Test CSRRW and trap handling
`timescale 1ns/1ps
module tb_csr;
reg clk=0, rst=1;
always #5 clk=~clk;
// Direct instantiation of csr_file for unit testing
reg [11:0] csr_addr;
reg [2:0] csr_op;
reg csr_we, trap, mret;
reg [31:0] csr_wdata, trap_pc, trap_cause;
wire [31:0] csr_rdata, mtvec_out, mepc_out;
csr_file dut (
.clk(clk), .rst(rst),
.csr_addr(csr_addr), .csr_op(csr_op),
.csr_we(csr_we), .csr_wdata(csr_wdata), .csr_rdata(csr_rdata),
.trap(trap), .trap_pc(trap_pc), .trap_cause(trap_cause),
.mtvec_out(mtvec_out), .mepc_out(mepc_out), .mret(mret)
);
initial begin
$dumpfile("tb_csr.vcd"); $dumpvars(0, tb_csr);
csr_we=0; trap=0; mret=0; csr_op=3'b001;
@(posedge clk); @(posedge clk); rst=0;
// Write mtvec = 0x8000
csr_addr=12'h305; csr_wdata=32'h8000; csr_we=1; csr_op=3'b001;
@(posedge clk); csr_we=0;
@(posedge clk);
if(mtvec_out===32'h8000) $display("PASS: mtvec=0x8000");
else $display("FAIL: mtvec=%h",mtvec_out);
// Trigger a trap (ecall, cause=11)
trap=1; trap_pc=32'h100; trap_cause=32'd11;
@(posedge clk); trap=0;
@(posedge clk);
if(mepc_out===32'h100) $display("PASS: mepc=0x100");
else $display("FAIL: mepc=%h",mepc_out);
$finish;
end
endmodule
Day 21 Takeaways
- CSRs are 32-bit special-purpose registers accessed by 12-bit addresses — completely separate from the integer register file.
- CSRRW/CSRRS/CSRRC always return the old value atomically — this enables safe read-modify-write sequences.
- On a trap, the CPU hardware saves PC → mepc, cause → mcause, and jumps to mtvec. Software handles the rest.
- MRET restores the PC from mepc — without it, you cannot return from a trap handler.
- M-mode is the highest privilege level. S-mode (supervisor) and U-mode (user) are added for OS support (Day 25).
FAQ
What are RISC-V CSRs?
Control and Status Registers are special 32-bit registers that control CPU behavior (interrupt enable, trap vector) and record status (exception cause, saved PC). They are accessed with CSRRW/CSRRS/CSRRC — not normal load/store instructions.
What happens when a RISC-V trap occurs?
Hardware saves PC to mepc, writes cause to mcause, clears MIE in mstatus, and jumps to mtvec. Software reads mcause, handles the event, and executes MRET to return.
What is MRET?
Machine Return — restores the PC from mepc, re-enables interrupts (restores MIE from MPIE), and returns from the trap handler to the interrupted code.