Ready-to-run snippets

Code Examples

Ready-to-run Verilog, SystemVerilog, UVM, bus protocol, formal verification, STA, and physical design examples. Click any to open it live in the IDE.

← All examples

Boot C on a RISC-V Core

Computer Architecture Computer Architecture

Compile C for RISC-V and boot it on an editable cvr32 core. Press Run Simulation: the program prints to the Console and every pipeline signal appears in the waveform. Change the C, the memory map in the Software panel, or the RTL.

Open in IDE
main.c
/* Boot C on a RISC-V core.

   Press Run Simulation. The lab compiles this file, loads main.hex into the RAM of
   the testbench and boots the cvr32 core, which runs the code below instruction by
   instruction. The text in the Console is printed by the core, and the Waveform
   Viewer shows every signal in the pipeline while it happens.

   Things to try:
   - Change the message or the numbers and run again.
   - Change the optimization level in the Software panel and watch the cycle count.
   - Open main.lst from Build Output to see the instructions this file became.
   - Edit cvr32_core.sv: this project's copy of the core is yours. */
#include "memory_map.h"

static const char banner[] = "Booted on my RISC-V core\n";

unsigned int count = 10;        /* .data: start-up code leaves it initialized */
unsigned int fib[16];           /* .bss: start-up code clears it */

/* rv32i has no divide instruction: the compiler calls a helper from libgcc */
static void print_number(unsigned int n)
{
    char digits[10];
    int i = 0;

    do {
        digits[i++] = '0' + n % 10;
        n /= 10;
    } while (n);
    while (i)
        cv_putc(digits[--i]);
}

int main(void)
{
    unsigned int sum = 0;

    cv_puts(banner);

    fib[0] = 0;
    fib[1] = 1;
    for (unsigned int i = 2; i < count; i++)
        fib[i] = fib[i - 1] + fib[i - 2];

    cv_puts("fibonacci:");
    for (unsigned int i = 0; i < count; i++) {
        cv_putc(' ');
        print_number(fib[i]);
        sum += fib[i];
    }
    cv_puts("\nsum: ");
    print_number(sum);
    cv_putc('\n');

    return sum;                 /* the testbench prints this as the result */
}
cvr32_alu.sv
// cvr32_alu: the integer ALU of the cvr32 core (RV32I)
module cvr32_alu (
    input  logic [3:0]  op,      // operation, one of the ALU_* codes below
    input  logic [31:0] a,       // first operand (rs1, PC or zero)
    input  logic [31:0] b,       // second operand (rs2 or immediate)
    output logic [31:0] y        // result
);

    localparam logic [3:0] ALU_ADD  = 4'd0;
    localparam logic [3:0] ALU_SUB  = 4'd1;
    localparam logic [3:0] ALU_SLL  = 4'd2;
    localparam logic [3:0] ALU_SLT  = 4'd3;
    localparam logic [3:0] ALU_SLTU = 4'd4;
    localparam logic [3:0] ALU_XOR  = 4'd5;
    localparam logic [3:0] ALU_SRL  = 4'd6;
    localparam logic [3:0] ALU_SRA  = 4'd7;
    localparam logic [3:0] ALU_OR   = 4'd8;
    localparam logic [3:0] ALU_AND  = 4'd9;

    always @(*) begin
        case (op)
            ALU_ADD:  y = a + b;
            ALU_SUB:  y = a - b;
            ALU_SLL:  y = a << b[4:0];                       // shift amount is the low 5 bits
            ALU_SLT:  y = {31'd0, $signed(a) < $signed(b)};
            ALU_SLTU: y = {31'd0, a < b};
            ALU_XOR:  y = a ^ b;
            ALU_SRL:  y = a >> b[4:0];
            ALU_SRA:  y = $unsigned($signed(a) >>> b[4:0]);  // arithmetic shift keeps the sign
            ALU_OR:   y = a | b;
            ALU_AND:  y = a & b;
            default:  y = a + b;
        endcase
    end

endmodule
cvr32_regfile.sv
// cvr32_regfile: 32 x 32-bit register file, two read ports and one write port.
// x0 always reads as zero. A write in the same cycle as a read of the same
// register is passed straight to the read port (write-through), so the
// write-back stage never causes a stall.
module cvr32_regfile (
    input  logic        clk,
    input  logic [4:0]  ra1,     // read address 1 (rs1)
    input  logic [4:0]  ra2,     // read address 2 (rs2)
    output logic [31:0] rd1,
    output logic [31:0] rd2,
    input  logic        we,      // write enable from write-back
    input  logic [4:0]  wa,      // write address (rd)
    input  logic [31:0] wd       // write data
);

    logic [31:0] regs [1:31];

    always_ff @(posedge clk) begin
        if (we && wa != 5'd0)
            regs[wa] <= wd;
    end

    always @(*) begin
        if (ra1 == 5'd0)
            rd1 = 32'd0;
        else if (we && wa == ra1)
            rd1 = wd;
        else
            rd1 = regs[ra1];

        if (ra2 == 5'd0)
            rd2 = 32'd0;
        else if (we && wa == ra2)
            rd2 = wd;
        else
            rd2 = regs[ra2];
    end

endmodule
cvr32_decode.sv
// cvr32_decode: turns one RV32I instruction into the control signals the
// pipeline needs. Pure combinational logic, used in the decode (ID) stage.
module cvr32_decode (
    input  logic [31:0] instr,
    output logic [4:0]  rs1,
    output logic [4:0]  rs2,
    output logic [4:0]  rd,
    output logic [31:0] imm,        // sign-extended immediate for this format
    output logic [3:0]  alu_op,
    output logic [1:0]  a_sel,      // ALU operand A: 0 = rs1, 1 = PC, 2 = zero
    output logic        b_imm,      // ALU operand B: 1 = immediate, 0 = rs2
    output logic        reg_write,
    output logic        mem_read,
    output logic        mem_write,
    output logic [2:0]  funct3,     // load/store size or branch condition
    output logic        is_branch,
    output logic        is_jal,
    output logic        is_jalr,
    output logic        uses_rs1,   // used by the hazard logic
    output logic        uses_rs2
);

    localparam logic [6:0] OP_LUI    = 7'b0110111;
    localparam logic [6:0] OP_AUIPC  = 7'b0010111;
    localparam logic [6:0] OP_JAL    = 7'b1101111;
    localparam logic [6:0] OP_JALR   = 7'b1100111;
    localparam logic [6:0] OP_BRANCH = 7'b1100011;
    localparam logic [6:0] OP_LOAD   = 7'b0000011;
    localparam logic [6:0] OP_STORE  = 7'b0100011;
    localparam logic [6:0] OP_IMM    = 7'b0010011;
    localparam logic [6:0] OP_REG    = 7'b0110011;

    localparam logic [3:0] ALU_ADD  = 4'd0;
    localparam logic [3:0] ALU_SUB  = 4'd1;
    localparam logic [3:0] ALU_SLL  = 4'd2;
    localparam logic [3:0] ALU_SLT  = 4'd3;
    localparam logic [3:0] ALU_SLTU = 4'd4;
    localparam logic [3:0] ALU_XOR  = 4'd5;
    localparam logic [3:0] ALU_SRL  = 4'd6;
    localparam logic [3:0] ALU_SRA  = 4'd7;
    localparam logic [3:0] ALU_OR   = 4'd8;
    localparam logic [3:0] ALU_AND  = 4'd9;

    logic [6:0] opcode;
    logic [3:0] alu_f3;             // ALU operation chosen by funct3 (OP and OP-IMM)

    // the five immediate formats: the immediate's bits are spread over the
    // instruction differently in each, and every one is sign-extended
    // from instruction bit 31 to 32 bits
    logic [31:0] imm_i, imm_s, imm_b, imm_u, imm_j;
    assign imm_i = {{20{instr[31]}}, instr[31:20]};
    assign imm_s = {{20{instr[31]}}, instr[31:25], instr[11:7]};
    assign imm_b = {{20{instr[31]}}, instr[7], instr[30:25], instr[11:8], 1'b0};
    assign imm_u = {instr[31:12], 12'd0};
    assign imm_j = {{12{instr[31]}}, instr[19:12], instr[20], instr[30:21], 1'b0};

    assign opcode = instr[6:0];
    assign rd     = instr[11:7];
    assign funct3 = instr[14:12];
    assign rs1    = instr[19:15];
    assign rs2    = instr[24:20];

    // funct3 to ALU operation; bit 30 picks SUB/SRA
    always @(*) begin
        case (funct3)
            3'b000:  alu_f3 = ALU_ADD;
            3'b001:  alu_f3 = ALU_SLL;
            3'b010:  alu_f3 = ALU_SLT;
            3'b011:  alu_f3 = ALU_SLTU;
            3'b100:  alu_f3 = ALU_XOR;
            3'b101:  alu_f3 = instr[30] ? ALU_SRA : ALU_SRL;
            3'b110:  alu_f3 = ALU_OR;
            default: alu_f3 = ALU_AND;
        endcase
    end

    always @(*) begin
        // defaults: an instruction that does nothing (FENCE and SYSTEM end up here)
        imm       = 32'd0;
        alu_op    = ALU_ADD;
        a_sel     = 2'd0;
        b_imm     = 1'b1;
        reg_write = 1'b0;
        mem_read  = 1'b0;
        mem_write = 1'b0;
        is_branch = 1'b0;
        is_jal    = 1'b0;
        is_jalr   = 1'b0;
        uses_rs1  = 1'b0;
        uses_rs2  = 1'b0;

        case (opcode)
            OP_LUI: begin                                    // rd = imm << 12
                imm       = imm_u;
                a_sel     = 2'd2;
                reg_write = 1'b1;
            end
            OP_AUIPC: begin                                  // rd = PC + (imm << 12)
                imm       = imm_u;
                a_sel     = 2'd1;
                reg_write = 1'b1;
            end
            OP_JAL: begin                                    // rd = PC + 4, jump to PC + imm
                imm       = imm_j;
                reg_write = 1'b1;
                is_jal    = 1'b1;
            end
            OP_JALR: begin                                   // rd = PC + 4, jump to rs1 + imm
                imm       = imm_i;
                reg_write = 1'b1;
                is_jalr   = 1'b1;
                uses_rs1  = 1'b1;
            end
            OP_BRANCH: begin                                 // compare rs1 and rs2, jump to PC + imm
                imm       = imm_b;
                is_branch = 1'b1;
                uses_rs1  = 1'b1;
                uses_rs2  = 1'b1;
            end
            OP_LOAD: begin                                   // rd = mem[rs1 + imm]
                imm       = imm_i;
                reg_write = 1'b1;
                mem_read  = 1'b1;
                uses_rs1  = 1'b1;
            end
            OP_STORE: begin                                  // mem[rs1 + imm] = rs2
                imm       = imm_s;
                mem_write = 1'b1;
                uses_rs1  = 1'b1;
                uses_rs2  = 1'b1;
            end
            OP_IMM: begin                                    // rd = rs1 op imm
                imm       = imm_i;
                alu_op    = alu_f3;                          // no SUBI: funct3 000 is always ADD
                reg_write = 1'b1;
                uses_rs1  = 1'b1;
            end
            OP_REG: begin                                    // rd = rs1 op rs2
                alu_op    = (funct3 == 3'b000 && instr[30]) ? ALU_SUB : alu_f3;
                b_imm     = 1'b0;
                reg_write = 1'b1;
                uses_rs1  = 1'b1;
                uses_rs2  = 1'b1;
            end
            default: ;
        endcase
    end

endmodule
cvr32_core.sv
// cvr32_core: a 5-stage in-order RV32I pipeline (IF, ID, EX, MEM, WB).
//
// - Branches and jumps resolve in EX. The core predicts "not taken", so a
//   taken branch or a jump flushes the two younger instructions.
// - With cfg_fwd_en = 1, results are forwarded from MEM and WB to EX, and only
//   a load followed by a dependent instruction stalls (one cycle).
//   With cfg_fwd_en = 0, a dependent instruction waits in ID until its source
//   reaches write-back.
// - Both memory ports have a ready signal; a slow data access freezes the pipe.
// - Every pipeline slot carries a "cause" tag. When WB sees an empty slot, the
//   tag says why it is empty, so cycles = instructions + sum of all bubbles.
module cvr32_core (
    input  logic        clk,
    input  logic        rst_n,
    input  logic        cfg_fwd_en,     // 1 = forwarding on

    // instruction port
    output logic [31:0] imem_addr,
    input  logic [31:0] imem_rdata,
    input  logic        imem_ready,

    // data port
    output logic [31:0] dmem_addr,
    output logic [31:0] dmem_wdata,
    output logic [3:0]  dmem_wstrb,
    output logic        dmem_we,
    output logic        dmem_re,
    input  logic [31:0] dmem_rdata,
    input  logic        dmem_ready
);

    // why a pipeline slot is empty
    localparam logic [2:0] B_START   = 3'd0;   // pipeline filling after reset
    localparam logic [2:0] B_FETCH   = 3'd1;   // instruction memory not ready
    localparam logic [2:0] B_LOADUSE = 3'd2;   // load followed by a dependent instruction
    localparam logic [2:0] B_RAW     = 3'd3;   // data dependency without forwarding
    localparam logic [2:0] B_FLUSH   = 3'd4;   // wrong-path instruction after a taken branch or jump
    localparam logic [2:0] B_DMEM    = 3'd5;   // data memory not ready

    // ------------------------------------------------------------------
    // Pipeline registers
    // ------------------------------------------------------------------
    logic [31:0] pc_f;

    // IF/ID
    logic        d_valid;
    logic [2:0]  d_cause;
    logic [31:0] d_pc, d_instr;

    // ID/EX
    logic        e_valid;
    logic [2:0]  e_cause;
    logic [31:0] e_pc, e_rv1, e_rv2, e_imm;
    logic [4:0]  e_rs1, e_rs2, e_rd;
    logic [3:0]  e_alu_op;
    logic [1:0]  e_a_sel;
    logic        e_b_imm, e_reg_write, e_mem_read, e_mem_write;
    logic        e_branch, e_jal, e_jalr;
    logic [2:0]  e_funct3;

    // EX/MEM
    logic        m_valid;
    logic [2:0]  m_cause;
    logic [31:0] m_result, m_store_data;
    logic [4:0]  m_rd;
    logic        m_reg_write, m_mem_read, m_mem_write;
    logic [2:0]  m_funct3;

    // MEM/WB
    logic        w_valid;
    logic [2:0]  w_cause;
    logic [31:0] w_value;
    logic [4:0]  w_rd;
    logic        w_reg_write;

    // ------------------------------------------------------------------
    // ID: decode and register read
    // ------------------------------------------------------------------
    logic [4:0]  d_rs1, d_rs2, d_rd;
    logic [31:0] d_imm, d_rv1, d_rv2;
    logic [3:0]  d_alu_op;
    logic [1:0]  d_a_sel;
    logic [2:0]  d_funct3;
    logic        d_b_imm, d_reg_write, d_mem_read, d_mem_write;
    logic        d_branch, d_jal, d_jalr, d_uses_rs1, d_uses_rs2;

    cvr32_decode u_decode (
        .instr     (d_instr),
        .rs1       (d_rs1),
        .rs2       (d_rs2),
        .rd        (d_rd),
        .imm       (d_imm),
        .alu_op    (d_alu_op),
        .a_sel     (d_a_sel),
        .b_imm     (d_b_imm),
        .reg_write (d_reg_write),
        .mem_read  (d_mem_read),
        .mem_write (d_mem_write),
        .funct3    (d_funct3),
        .is_branch (d_branch),
        .is_jal    (d_jal),
        .is_jalr   (d_jalr),
        .uses_rs1  (d_uses_rs1),
        .uses_rs2  (d_uses_rs2)
    );

    cvr32_regfile u_regfile (
        .clk (clk),
        .ra1 (d_rs1),
        .ra2 (d_rs2),
        .rd1 (d_rv1),
        .rd2 (d_rv2),
        .we  (w_valid && w_reg_write),
        .wa  (w_rd),
        .wd  (w_value)
    );

    // ------------------------------------------------------------------
    // Hazard detection
    // ------------------------------------------------------------------
    logic dep_e, dep_m;           // the ID instruction reads a register that EX / MEM will write
    logic load_use, raw_wait, id_stall, mem_stall, redirect;
    logic [2:0] stall_cause;

    assign dep_e = e_valid && e_reg_write && (e_rd != 5'd0) &&
                   ((d_uses_rs1 && d_rs1 == e_rd) || (d_uses_rs2 && d_rs2 == e_rd));
    assign dep_m = m_valid && m_reg_write && (m_rd != 5'd0) &&
                   ((d_uses_rs1 && d_rs1 == m_rd) || (d_uses_rs2 && d_rs2 == m_rd));

    // a load result is ready only after MEM, so even forwarding needs one bubble
    assign load_use = dep_e && e_mem_read;
    // without forwarding, wait until the producer has left EX and MEM
    assign raw_wait = !cfg_fwd_en && (dep_e || dep_m);

    assign id_stall    = d_valid && (load_use || raw_wait);
    assign stall_cause = load_use ? B_LOADUSE : B_RAW;

    // a data access that is not ready freezes the whole pipeline
    assign mem_stall = m_valid && (m_mem_read || m_mem_write) && !dmem_ready;

    // ------------------------------------------------------------------
    // EX: forwarding, ALU, branch resolution
    // ------------------------------------------------------------------
    logic [31:0] fwd_a, fwd_b, alu_a, alu_b, alu_y, ex_result, target;
    logic        taken;

    always @(*) begin
        fwd_a = e_rv1;
        fwd_b = e_rv2;
        if (cfg_fwd_en) begin
            if (m_valid && m_reg_write && m_rd != 5'd0 && m_rd == e_rs1)
                fwd_a = m_result;
            else if (w_valid && w_reg_write && w_rd != 5'd0 && w_rd == e_rs1)
                fwd_a = w_value;
            if (m_valid && m_reg_write && m_rd != 5'd0 && m_rd == e_rs2)
                fwd_b = m_result;
            else if (w_valid && w_reg_write && w_rd != 5'd0 && w_rd == e_rs2)
                fwd_b = w_value;
        end
    end

    assign alu_a = (e_a_sel == 2'd1) ? e_pc : (e_a_sel == 2'd2) ? 32'd0 : fwd_a;
    assign alu_b = e_b_imm ? e_imm : fwd_b;

    cvr32_alu u_alu (
        .op (e_alu_op),
        .a  (alu_a),
        .b  (alu_b),
        .y  (alu_y)
    );

    always @(*) begin
        case (e_funct3)
            3'b000:  taken = (fwd_a == fwd_b);                    // BEQ
            3'b001:  taken = (fwd_a != fwd_b);                    // BNE
            3'b100:  taken = ($signed(fwd_a) <  $signed(fwd_b));  // BLT
            3'b101:  taken = ($signed(fwd_a) >= $signed(fwd_b));  // BGE
            3'b110:  taken = (fwd_a <  fwd_b);                    // BLTU
            3'b111:  taken = (fwd_a >= fwd_b);                    // BGEU
            default: taken = 1'b0;
        endcase
    end

    assign target    = e_jalr ? ((fwd_a + e_imm) & ~32'd1) : (e_pc + e_imm);
    assign ex_result = (e_jal || e_jalr) ? (e_pc + 32'd4) : alu_y;
    assign redirect  = e_valid && !mem_stall && (e_jal || e_jalr || (e_branch && taken));

    // ------------------------------------------------------------------
    // MEM: data port and load alignment
    // ------------------------------------------------------------------
    logic [31:0] load_shifted, load_value, m_value;

    assign dmem_addr = m_result;
    assign dmem_re   = m_valid && m_mem_read;
    assign dmem_we   = m_valid && m_mem_write;

    always @(*) begin
        case (m_funct3[1:0])
            2'b00: begin                                          // SB
                dmem_wdata = {4{m_store_data[7:0]}};
                dmem_wstrb = 4'b0001 << m_result[1:0];
            end
            2'b01: begin                                          // SH
                dmem_wdata = {2{m_store_data[15:0]}};
                dmem_wstrb = m_result[1] ? 4'b1100 : 4'b0011;
            end
            default: begin                                        // SW
                dmem_wdata = m_store_data;
                dmem_wstrb = 4'b1111;
            end
        endcase
    end

    assign load_shifted = dmem_rdata >> {m_result[1:0], 3'b000};

    always @(*) begin
        case (m_funct3)
            3'b000:  load_value = {{24{load_shifted[7]}},  load_shifted[7:0]};   // LB
            3'b001:  load_value = {{16{load_shifted[15]}}, load_shifted[15:0]};  // LH
            3'b100:  load_value = {24'd0, load_shifted[7:0]};                    // LBU
            3'b101:  load_value = {16'd0, load_shifted[15:0]};                   // LHU
            default: load_value = dmem_rdata;                                    // LW
        endcase
    end

    assign m_value = m_mem_read ? load_value : m_result;

    // ------------------------------------------------------------------
    // Pipeline register updates
    // ------------------------------------------------------------------
    assign imem_addr = pc_f;

    // IF: program counter
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            pc_f <= 32'd0;
        else if (mem_stall)
            pc_f <= pc_f;
        else if (redirect)
            pc_f <= target;
        else if (id_stall || !imem_ready)
            pc_f <= pc_f;
        else
            pc_f <= pc_f + 32'd4;
    end

    // IF/ID
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            d_valid <= 1'b0;
            d_cause <= B_START;
            d_pc    <= 32'd0;
            d_instr <= 32'h0000_0013;                             // NOP (addi x0, x0, 0)
        end else if (mem_stall || (id_stall && !redirect)) begin
            // hold
        end else if (redirect) begin
            d_valid <= 1'b0;
            d_cause <= B_FLUSH;
        end else if (!imem_ready) begin
            d_valid <= 1'b0;
            d_cause <= B_FETCH;
        end else begin
            d_valid <= 1'b1;
            d_pc    <= pc_f;
            d_instr <= imem_rdata;
        end
    end

    // ID/EX: valid bit and bubble cause (reset)
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            e_valid <= 1'b0;
            e_cause <= B_START;
        end else if (mem_stall) begin
            // hold
        end else if (redirect) begin
            e_valid <= 1'b0;
            e_cause <= B_FLUSH;
        end else if (id_stall) begin
            e_valid <= 1'b0;
            e_cause <= stall_cause;
        end else begin
            e_valid <= d_valid;
            e_cause <= d_cause;
        end
    end

    // ID/EX: instruction data (no reset needed, e_valid qualifies it)
    always_ff @(posedge clk) begin
        if (mem_stall) begin
            // hold the instruction, but keep its operands current: a value it
            // gets by forwarding from WB disappears while the pipe is frozen
            e_rv1 <= fwd_a;
            e_rv2 <= fwd_b;
        end else if (!redirect && !id_stall) begin
            e_pc        <= d_pc;
            e_rv1       <= d_rv1;
            e_rv2       <= d_rv2;
            e_imm       <= d_imm;
            e_rs1       <= d_rs1;
            e_rs2       <= d_rs2;
            e_rd        <= d_rd;
            e_alu_op    <= d_alu_op;
            e_a_sel     <= d_a_sel;
            e_b_imm     <= d_b_imm;
            e_reg_write <= d_reg_write;
            e_mem_read  <= d_mem_read;
            e_mem_write <= d_mem_write;
            e_branch    <= d_branch;
            e_jal       <= d_jal;
            e_jalr      <= d_jalr;
            e_funct3    <= d_funct3;
        end
    end

    // EX/MEM
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            m_valid <= 1'b0;
            m_cause <= B_START;
        end else if (!mem_stall) begin
            m_valid      <= e_valid;
            m_cause      <= e_cause;
            m_result     <= ex_result;
            m_store_data <= fwd_b;
            m_rd         <= e_rd;
            m_reg_write  <= e_reg_write;
            m_mem_read   <= e_mem_read;
            m_mem_write  <= e_mem_write;
            m_funct3     <= e_funct3;
        end
    end

    // MEM/WB
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            w_valid <= 1'b0;
            w_cause <= B_START;
        end else if (mem_stall) begin
            w_valid <= 1'b0;
            w_cause <= B_DMEM;
        end else begin
            w_valid     <= m_valid;
            w_cause     <= m_cause;
            w_value     <= m_value;
            w_rd        <= m_rd;
            w_reg_write <= m_reg_write;
        end
    end

    // ------------------------------------------------------------------
    // Performance counters (counted where instructions retire, in WB)
    // ------------------------------------------------------------------
    logic [63:0] perf_cycles, perf_instret;
    logic [31:0] perf_bubble [0:5];
    logic [31:0] perf_branches, perf_taken, perf_jumps, perf_loads, perf_stores;

    // cycles and retired instructions: the two numbers behind CPI
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            perf_cycles  <= '0;
            perf_instret <= '0;
        end else begin
            perf_cycles <= perf_cycles + 1'b1;
            if (w_valid)
                perf_instret <= perf_instret + 1'b1;
        end
    end

    // empty pipeline slots, by the reason they are empty
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            for (int i = 0; i < 6; i++)
                perf_bubble[i] <= 32'd0;
        end else begin
            if (!w_valid)
                perf_bubble[w_cause] <= perf_bubble[w_cause] + 32'd1;
        end
    end

    // instruction mix, counted when an instruction leaves EX
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            perf_branches <= 32'd0;
            perf_taken    <= 32'd0;
            perf_jumps    <= 32'd0;
            perf_loads    <= 32'd0;
            perf_stores   <= 32'd0;
        end else if (e_valid && !mem_stall) begin
            if (e_branch)
                perf_branches <= perf_branches + 32'd1;
            if (e_branch && taken)
                perf_taken <= perf_taken + 32'd1;
            if (e_jal || e_jalr)
                perf_jumps <= perf_jumps + 32'd1;
            if (e_mem_read)
                perf_loads <= perf_loads + 32'd1;
            if (e_mem_write)
                perf_stores <= perf_stores + 32'd1;
        end
    end

endmodule
cvr32_sysctl.sv
// cvr32_sysctl: the two memory-mapped ports a bare-metal program uses to talk
// to the outside world in simulation.
//   0xF000_0000  EXIT     write: the program has finished, the value is its result
//   0xF000_0004  CONSOLE  write: print one character (the low 8 bits)
// It watches the core's data port; nothing is stored in memory at these addresses.
module cvr32_sysctl (
    input  logic        clk,
    input  logic        rst_n,
    input  logic [31:0] addr,           // core data port
    input  logic [31:0] wdata,
    input  logic        we,
    input  logic        ready,          // the access completes in this cycle
    output logic        done,           // stays 1 after the program has exited
    output logic [31:0] code,           // the value written to EXIT
    output logic        char_valid,     // one-cycle pulse: a character was printed
    output logic [7:0]  char_data
);

    localparam logic [31:0] EXIT    = 32'hF000_0000;
    localparam logic [31:0] CONSOLE = 32'hF000_0004;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            done <= 1'b0;
            code <= 32'd0;
        end else if (we && ready && addr == EXIT && !done) begin
            done <= 1'b1;
            code <= wdata;
        end
    end

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            char_valid <= 1'b0;
            char_data  <= 8'd0;
        end else begin
            char_valid <= we && ready && addr == CONSOLE;
            char_data  <= wdata[7:0];
        end
    end

endmodule
cv_ram.sv
// cv_ram: a simulation RAM with one read-only instruction port and one read/write
// data port, loaded from the hex file the Software panel builds.
//
// The hex file holds one 32-bit word per line and word 0 is the memory's first
// address. The file name comes from +firmware=NAME (default main.hex), and the
// simulation receives the latest build automatically.
module cv_ram #(
    parameter logic [31:0] BASE  = 32'h0000_0000,
    parameter int          BYTES = 64 * 1024
) (
    input  logic        clk,

    input  logic [31:0] iaddr,
    output logic [31:0] irdata,

    input  logic [31:0] daddr,
    input  logic [31:0] dwdata,
    input  logic [3:0]  dwstrb,
    input  logic        dwe,
    output logic [31:0] drdata
);

    localparam int WORDS = BYTES / 4;

    logic [31:0] mem [0:WORDS-1];
    string       firmware;

    initial begin
        for (int i = 0; i < WORDS; i++)
            mem[i] = 32'd0;
        if (!$value$plusargs("firmware=%s", firmware))
            firmware = "main.hex";
        $readmemh(firmware, mem);
    end

    // word index inside this memory, for addresses that fall in it
    function automatic logic hit(input logic [31:0] addr);
        hit = (addr >= BASE) && (addr - BASE < 32'(BYTES));
    endfunction

    function automatic int index(input logic [31:0] addr);
        index = int'((addr - BASE) >> 2);
    endfunction

    assign irdata = hit(iaddr) ? mem[index(iaddr)] : 32'd0;
    assign drdata = hit(daddr) ? mem[index(daddr)] : 32'd0;

    always @(posedge clk) begin
        if (dwe && hit(daddr)) begin
            for (int b = 0; b < 4; b++)
                if (dwstrb[b])
                    mem[index(daddr)][8*b +: 8] <= dwdata[8*b +: 8];
        end
    end

endmodule
tb_boot.sv
// tb_boot: boots the cvr32 core from the program in main.c. cv_ram holds the program:
// its $readmemh line loads main.hex, one 32-bit word per line.
//
// Pressing Run Simulation builds the C code first, so main.hex and memory_map.svh
// always match the sources and the memory map.
`include "memory_map.svh"

module tb_boot;

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    always #5 clk = ~clk;                       // 100 MHz

    logic [31:0] imem_addr, imem_rdata;
    logic [31:0] dmem_addr, dmem_wdata, dmem_rdata;
    logic [3:0]  dmem_wstrb;
    logic        dmem_we, dmem_re;

    cvr32_core u_core (
        .clk        (clk),
        .rst_n      (rst_n),
        .cfg_fwd_en (1'b1),
        .imem_addr  (imem_addr),
        .imem_rdata (imem_rdata),
        .imem_ready (1'b1),
        .dmem_addr  (dmem_addr),
        .dmem_wdata (dmem_wdata),
        .dmem_wstrb (dmem_wstrb),
        .dmem_we    (dmem_we),
        .dmem_re    (dmem_re),
        .dmem_rdata (dmem_rdata),
        .dmem_ready (1'b1)
    );

    cv_ram #(.BASE(`CV_RAM_BASE), .BYTES(`CV_RAM_SIZE)) u_ram (
        .clk    (clk),
        .iaddr  (imem_addr),
        .irdata (imem_rdata),
        .daddr  (dmem_addr),
        .dwdata (dmem_wdata),
        .dwstrb (dmem_wstrb),
        .dwe    (dmem_we),
        .drdata (dmem_rdata)
    );

    // the program ends by writing its result to `CV_EXIT_ADDR, and prints one
    // character per write to `CV_CONSOLE_ADDR
    logic [31:0] result;
    logic        done = 1'b0;

    always @(posedge clk) begin
        if (dmem_we && dmem_addr == `CV_CONSOLE_ADDR)
            $write("%c", dmem_wdata[7:0]);
        if (dmem_we && dmem_addr == `CV_EXIT_ADDR && !done) begin
            result <= dmem_wdata;
            done   <= 1'b1;
        end
    end

    integer cycles = 0;
    always @(posedge clk)
        if (rst_n) cycles <= cycles + 1;

    initial begin
        repeat (3) @(posedge clk);
        rst_n <= 1'b1;
        #1;
        $display("First fetch at 0x%08x, program loaded at 0x%08x", imem_addr, `CV_RESET_VECTOR);
        if (imem_addr !== `CV_RESET_VECTOR)
            $display("WARNING: the core does not start where the program is loaded. Change the core's reset address or the first region in memory_map.json.");

        fork
            begin
                wait (done);
                $display("\nProgram finished after %0d cycles, result 0x%08x", cycles, result);
            end
            begin
                repeat (200000) @(posedge clk);
                $display("\nTIMEOUT: the program did not finish within 200000 cycles. Check the core's reset address and that main returns.");
            end
        join_any
        $finish;
    end

endmodule
memory_map.json
{
  "version": 1,
  "regions": [
    {
      "name": "ram",
      "kind": "ram",
      "origin": "0x00000000",
      "length": "0x00010000",
      "width": 32
    }
  ],
  "exit_addr": "0xF0000000",
  "console_addr": "0xF0000004",
  "attached": true
}
Admin mode — edit and delete controls are visible on each row. Use the IDE to add new snippets via File → Save as Snippet.