HomeRISC-V from ScratchDay 21
DAY 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 NameAddressPurpose
mstatus0x300Global interrupt enable (MIE bit[3]), previous interrupt state (MPIE bit[7])
mtvec0x305Trap handler base address — PC jumps here on any trap
mepc0x341Machine Exception PC — saves the PC of the trapped instruction
mcause0x342Cause of the trap (bit[31]=interrupt/exception, bits[30:0]=cause code)
mscratch0x340Scratch register for trap handler software use
mip0x344Machine Interrupt Pending — which interrupts are waiting
mie0x304Machine 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:

csr_file.v
// 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:

  1. Saves the current PC to mepc
  2. Writes the exception code to mcause (e.g., 2 = illegal instruction, 11 = ECALL from M-mode)
  3. Clears the MIE bit in mstatus (disables further interrupts)
  4. 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
// 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

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.

Previous
← Day 20: Load-Use Stalls & Hazard Unit

← Full roadmap