HomeRISC-V from ScratchDay 12
DAY 12 · PHASE 2 — BUILD THE CPU

R-type & I-type Datapath

By EcrioniX · Updated Jun 11, 2026

Days 8–11 gave you five individual modules. Today you wire them together for the first time into a working partial datapath that can actually execute instructions. Load a program of add, sub, addi instructions and watch the register values change correctly — this is the first moment the CPU comes alive.

1. What R-type and I-type need

Neither R-type nor I-type ALU instructions access memory or branch. The entire flow is: fetch → decode → ALU → write-back. No DMEM, no branch logic needed yet. The modules required:

ModuleRole in this datapath
pc.vAdvances PC+4 every cycle
imem.vReturns the 32-bit instruction at current PC
control.vDecodes opcode → drives RegWrite, ALUSrc, ALUOp
regfile.vReads rs1/rs2; writes result to rd
immgen.vSign-extends immediate (I-type only)
ALUSrc muxSelects rs2 (R) or imm (I) as ALU operand B
alu.vComputes result

2. Wiring diagram

PC IMEM CTRL IMMGEN REGFILE ALUSrc ALU write-back → rd PC+4→pcnext ALUSrc, ALUOp, RegWrite from CTRL
R/I-type datapath: PC → IMEM → CTRL+REGFILE+IMMGEN → ALUSrc mux → ALU → write-back to REGFILE. PC advances by 4 every cycle.

3. core_ri.v — the partial datapath

core_ri.v — R/I-type partial datapath
// Partial RISC-V datapath: handles R-type and I-type ALU instructions
// Does NOT yet handle loads, stores, branches, or jumps
module core_ri (
    input wire clk,
    input wire rst
);
    // --- PC ---
    wire [31:0] pc, pc_next, instr;
    assign pc_next = pc + 32'd4;   // always advance (no branches yet)
    pc    pc_reg (.clk(clk),.rst(rst),.pc_next(pc_next),.pc(pc));
    imem  imem0  (.addr(pc),.instr(instr));

    // --- Decode ---
    wire [6:0] opcode = instr[6:0];
    wire [4:0] rd     = instr[11:7];
    wire [2:0] funct3 = instr[14:12];
    wire [4:0] rs1    = instr[19:15];
    wire [4:0] rs2    = instr[24:20];
    wire [6:0] funct7 = instr[31:25];

    // --- Control ---
    wire       reg_write, alu_src;
    wire [3:0] alu_op;
    wire [2:0] imm_sel;
    wire       mem_read,mem_write,branch,jump; wire[1:0]wb_sel;
    control ctrl(.opcode(opcode),.funct3(funct3),.funct7(funct7),
        .reg_write(reg_write),.alu_src(alu_src),.mem_read(mem_read),
        .mem_write(mem_write),.branch(branch),.jump(jump),
        .wb_sel(wb_sel),.alu_op(alu_op),.imm_sel(imm_sel));

    // --- Register file ---
    wire [31:0] rdata1, rdata2, wb_data;
    regfile rf(.clk(clk),.we(reg_write),.waddr(rd),.wdata(wb_data),
               .raddr1(rs1),.rdata1(rdata1),.raddr2(rs2),.rdata2(rdata2));

    // --- ImmGen ---
    wire [31:0] imm;
    immgen ig(.instr(instr),.imm_sel(imm_sel),.imm(imm));

    // --- ALUSrc mux ---
    wire [31:0] alu_b = alu_src ? imm : rdata2;

    // --- ALU ---
    wire [31:0] alu_result;
    wire        zero;
    alu alu0(.a(rdata1),.b(alu_b),.alu_op(alu_op),.result(alu_result),.zero(zero));

    // --- Write-back (ALU result only for R/I) ---
    assign wb_data = alu_result;
endmodule

4. Testbench — run a mini program

program.hex — mini R/I program
00500093  // addi x1, x0,  5    x1=5
00A00113  // addi x2, x0, 10    x2=10
002081B3  // add  x3, x1, x2    x3=15
40208233  // sub  x4, x1, x2    x4=-5 (0xFFFFFFFB)
0010F2B3  // and  x5, x1, x2    x5=0
0010E333  // or   x6, x1, x2    x6=15
00000013  // nop
00000013  // nop
tb_core_ri.v — testbench
`timescale 1ns/1ps
module tb_core_ri;
    reg clk=0,rst=1;
    core_ri dut(.clk(clk),.rst(rst));
    always #5 clk=~clk;
    // access regfile for checking
    wire [31:0] x1=dut.rf.regs[1],x2=dut.rf.regs[2],x3=dut.rf.regs[3];
    wire [31:0] x4=dut.rf.regs[4],x5=dut.rf.regs[5],x6=dut.rf.regs[6];
    integer errors=0;
    task chk(input [31:0]got,input [31:0]exp,input [63:0]name);
        if(got!==exp)begin $display("FAIL %s: got=%0h exp=%0h",name,got,exp);errors=errors+1;end
        else $display("ok   %s = %0h",name,got);
    endtask
    integer i;
    initial begin
        @(posedge clk); rst=0;
        repeat(8) @(posedge clk); #1; // run 8 cycles
        chk(x1,32'd5,"x1 (addi 5)");
        chk(x2,32'd10,"x2 (addi 10)");
        chk(x3,32'd15,"x3 (add)");
        chk(x4,32'hFFFFFFFB,"x4 (sub)");
        chk(x5,32'd0,"x5 (and)");
        chk(x6,32'd15,"x6 (or)");
        if(errors==0) $display("ALL TESTS PASSED"); else $display("%0d FAILED",errors);
        $finish;
    end
endmodule
expected output
ok   x1 (addi 5) = 5
ok   x2 (addi 10) = a
ok   x3 (add) = f
ok   x4 (sub) = fffffffb
ok   x5 (and) = 0
ok   x6 (or) = f
ALL TESTS PASSED

🎉 The CPU is alive!

You just ran a real RISC-V program on a CPU you built from scratch. Six instructions, six correct register values. Days 13–14 add loads/stores and branches. Day 15 puts it all together into a complete working RV32I core.

🎯 Day 12 takeaways

FAQ

What instructions work in this datapath?

All R-type (add, sub, and, or, xor, slt, sll, srl, sra) and I-type ALU (addi, andi, ori, xori, slti, slli, srli, srai). No loads, stores, branches, or jumps yet.

What is the ALUSrc mux?

Selects ALU operand B: rs2 (R-type, ALUSrc=0) or sign-extended immediate (I-type, ALUSrc=1). The control unit drives it based on the opcode.

Previous
← Day 11: ImmGen & Control Unit

← Full roadmap