rv32g-core

Pipelined, synthesizable, and fabricable RISC-V core in SystemVerilog+Yosys+OpenROAD
Log | Files | Refs | LICENSE

commit 8b56d0a6a6b0256eff987ce8fa89a176d9fc6738
parent 78d9f04bd0c4aa9e4853d1fa336d4064b3ad2f91
Author: Vineet Kumar <git@vineetk.net>
Date:   Sat, 11 Jul 2026 14:55:42 +0530

do initial pipelining

Diffstat:
Mrtl/csr_regfile.sv | 10+++-------
Mrtl/rv32g_hart.sv | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mrtl/rv32g_pkg.sv | 40++++++++++++++++++++++++++++++++++++++++
Mrtl/stage_decode.sv | 23+++++++++++++++++++----
Mrtl/stage_execute.sv | 19+++++++++++--------
Mrtl/stage_fetch.sv | 20++++++++++----------
Mrtl/stage_memory.sv | 3++-
Mrtl/tb_rv32g.sv | 27++++++++++++++++++++++-----
8 files changed, 255 insertions(+), 106 deletions(-)

diff --git a/rtl/csr_regfile.sv b/rtl/csr_regfile.sv @@ -95,18 +95,14 @@ module csr_regfile default: ; endcase end - - // FIXME remove once hart is pipelined - // perform combinational read when rden - assign rdata = (rden && !illegal_rd) ? regfile[reg_ridx] : '0; always_ff @(posedge clk or negedge rstN) begin if (!rstN) begin mepc_reg <= '0; end else begin - // // perform sequential read when rden - // if (rden && !illegal_rd) rdata <= regfile[reg_ridx]; - // else rdata <= '0; + // perform sequential read when rden + if (rden && !illegal_rd) rdata <= regfile[reg_ridx]; + else rdata <= '0; // perform sequential write when wren and not read-only if (wren && !illegal_wr) begin diff --git a/rtl/rv32g_hart.sv b/rtl/rv32g_hart.sv @@ -18,53 +18,101 @@ module rv32g_hart // x0 to x31 reg_t regfile [0:31]; - reg_t pc; reg_t next_pc; - logic [31:0] inst; - ctrl_t ctrl; - logic [31:0] imm; - reg_t rs1_val; - reg_t rs2_val; - logic [4:0] rd_num; - reg_t alu_out; - reg_t mem_out; - logic branch_taken; - // Zicsr - logic [11:0] csr_addr; - reg_t csr_rdata; - reg_t csr_out; - logic csr_illegal; + reg_t csr_rdata; + logic csr_illegal; // CSR has xEPC registers for use with xRET instructions - reg_t csr_mepc; + reg_t csr_mepc; + + // pipeline registers + ifid_reg_t ifid_reg, ifid_next; + idex_reg_t idex_reg; + exmem_reg_t exmem_reg; + memwb_reg_t memwb_reg; + + // hazards + logic if_stall; + assign if_stall = !imem_valid; + + // TODO move in between decode and execute for better organization? csr_regfile u_csr(.clk, .rstN, .illegal(csr_illegal), .mepc_shadow(csr_mepc), - .raddr(csr_addr), .rden(ctrl.csr_rden), .rdata(csr_rdata), - .waddr(csr_addr), .wren(ctrl.csr_wren), .wdata(csr_out)); + .raddr(idex_reg.csr_addr), .rden(idex_reg.ctrl.csr_rden), .rdata(csr_rdata), + .waddr(idex_reg.csr_addr), .wren(idex_reg.ctrl.csr_wren), .wdata(exmem_reg.csr_out)); + + // ============= + // === FETCH === + // ============= + + // temporary signals + logic [31:0] fetch_inst; + reg_t pc_reg; - stage_fetch u_fetch(.clk, .rstN, .flush_i(ctrl.fence_flush), .pc, .imem_addr, .imem_data, .inst); + stage_fetch u_fetch(.clk, .rstN, .flush_i(idex_reg.ctrl.fence_flush), .pc(pc_reg), .imem_addr, .imem_data, .inst(fetch_inst)); + // IF/ID pipeline register + always_comb begin + ifid_next = ifid_reg; // holds by default + if (imem_valid) begin + ifid_next.pc = pc_reg; + ifid_next.inst = fetch_inst; + end + end always_ff @(posedge clk or negedge rstN) begin - $display("IF: pc: %h, next_pc: %h, imem_valid: %d, imem[%h]: %h, inst: %h", pc, next_pc, imem_valid, imem_addr, imem_data, inst); - if (!rstN) pc <= BASE_ADDR; - else if (imem_valid) pc <= next_pc; - // else begin - // // TODO (pipeline) IF/ID stall. if no instruction, just wait? - // end + $display("IF: pc: %h, next_pc: %h, imem_valid: %d, imem[%h]: %h, inst: %h", + ifid_reg.pc, next_pc, imem_valid, imem_addr, imem_data, ifid_reg.inst); + if (!rstN) begin + pc_reg <= BASE_ADDR; + ifid_reg <= '0; + end else if (!if_stall) begin + pc_reg <= next_pc; + ifid_reg <= ifid_next; + end end - stage_decode u_decode(.inst, .regfile, .ctrl, .imm, .rs1_val, .rs2_val, .rd_num, .csr_addr); - // TODO ID/EX pipeline register -// always_ff @(posedge clk or negedge rstN) begin - always_ff @(posedge clk) begin - $display("ID: inst: %h, ctrl: %h, imm: %h, rs1_val: %h, rs2_val: %h, rd_num: %d", inst, ctrl, imm, rs1_val, rs2_val, rd_num); -// if (!rstN) ; // TODO reset pipeline register - if (ctrl.exception != EXC_NONE) begin - // TODO handle exceptions - unique case (ctrl.exception) + // ============== + // === DECODE === + // ============== + + // temporary signals + ctrl_t dec_ctrl; + reg_t dec_pc, dec_imm, dec_rs1_val, dec_rs2_val; + logic [4:0] dec_rd_num; + logic [11:0] dec_csr_addr; + + stage_decode u_decode(.clk, .rstN, .regfile, .inst(ifid_reg.inst), + .pc_in(ifid_reg.pc), .pc_out(dec_pc), + .ctrl(dec_ctrl), .imm(dec_imm), + .rs1_val(dec_rs1_val), .rs2_val(dec_rs2_val), + .rd_num(dec_rd_num), .csr_addr(dec_csr_addr)); + // ID/EX pipeline register + always_ff @(posedge clk or negedge rstN) begin + $display("ID: ctrl: %h, imm: %h, rs1_val: %h, rs2_val: %h, rd_num: %d", + idex_reg.ctrl, idex_reg.imm, idex_reg.rs1_val, idex_reg.rs2_val, idex_reg.rd_num); + if (!rstN) begin + // NOP by default + idex_reg <= '0; + end else if (if_stall) begin + // insert NOP + idex_reg <= '0; + end else begin + // latch decode outputs in normal operation + idex_reg.ctrl <= dec_ctrl; + idex_reg.pc <= dec_pc; + idex_reg.imm <= dec_imm; + idex_reg.rs1_val <= dec_rs1_val; + idex_reg.rs2_val <= dec_rs2_val; + idex_reg.rd_num <= dec_rd_num; + idex_reg.csr_addr <= dec_csr_addr; + end + + if (idex_reg.ctrl.exception != EXC_NONE) begin + // TODO handle exceptions. might need moving. + unique case (idex_reg.ctrl.exception) EXC_NONE: ; - EXC_ILL_INST: $display("Exception: illegal instruction, inst: %h", inst); + EXC_ILL_INST: $display("Exception: illegal instruction, inst: %h", ifid_reg.inst); EXC_CALL: $display("Exception: ECALL"); EXC_BKPT: $display("Exception: BREAK"); endcase @@ -72,66 +120,95 @@ module rv32g_hart end // TODO find out what else needs to be done in between stages like exceptions - stage_execute u_execute(.op(ctrl.aluop), - .op1src(ctrl.aluop1src), - .op2src(ctrl.aluop2src), - .branchop(ctrl.branchop), - .out(alu_out), - .imm, .pc, .rs1_val, .rs2_val, .branch_taken, - .csrop(ctrl.csrop), - .csr_rden(ctrl.csr_rden), - .csr_wren(ctrl.csr_wren), - .csr_immen(ctrl.csr_immen), - .csr_imm(ctrl.csr_imm), + // =============== + // === EXECUTE === + // =============== + + stage_execute u_execute(.clk, + .imm(idex_reg.imm), + .pc(idex_reg.pc), + .rs1_val(idex_reg.rs1_val), + .rs2_val(idex_reg.rs2_val), + .op(idex_reg.ctrl.aluop), + .op1src(idex_reg.ctrl.aluop1src), + .op2src(idex_reg.ctrl.aluop2src), + .branchop(idex_reg.ctrl.branchop), + .csrop(idex_reg.ctrl.csrop), + .csr_rden(idex_reg.ctrl.csr_rden), + .csr_wren(idex_reg.ctrl.csr_wren), + .csr_immen(idex_reg.ctrl.csr_immen), + .csr_imm(idex_reg.ctrl.csr_imm), .csr_rdata(csr_rdata), - .csr_out(csr_out), - .is_mret(ctrl.is_mret)); - // TODO EX/MEM pipeline register -// always_ff @(posedge clk or negedge rstN) begin - always_ff @(posedge clk) begin - $display("EX: op: %h, op1src: %d, op2src: %d, branchop: %d, out: %h, branch_taken: %d", ctrl.aluop, ctrl.aluop1src, ctrl.aluop2src, ctrl.branchop, alu_out, branch_taken); -// if (!rstN) ; // TODO reset pipeline register + .is_mret(idex_reg.ctrl.is_mret), + .out(exmem_reg.alu_out), + .branch_taken(exmem_reg.branch_taken), + .csr_out(exmem_reg.csr_out)); + // EX/MEM pipeline register + always_ff @(posedge clk or negedge rstN) begin + $display("EX: op: %h, op1src: %d, op2src: %d, branchop: %d, out: %h, branch_taken: %d", + idex_reg.ctrl.aluop, idex_reg.ctrl.aluop1src, idex_reg.ctrl.aluop2src, + idex_reg.ctrl.branchop, exmem_reg.alu_out, exmem_reg.branch_taken); + if (!rstN) exmem_reg = '0; // if (!dmem_valid && ctrl.memop != MEM_NONE) begin // // TODO stall // end end // TODO stall when dmem_valid is 0 and ctrl.memop != 0. find out what else needs to be done in between stages + + // ============== + // === MEMORY === + // ============== - stage_memory u_mem(.memop(ctrl.memop), .alu_out, .rs2_val, - .dmem_addr, .dmem_rdata, .dmem_wdata, .dmem_we, .dmem_be, .mem_out); - // TODO MEM/WB pipeline register -// always_ff @(posedge clk or negedge rstN) begin - always_ff @(posedge clk) begin - $display("MEM: memop: %h, dmem_addr: %h, dmem_rdata: %h, dmem_wdata: %h, dmem_we: %d, dmem_be: %b, mem_out: %h", ctrl.memop, dmem_addr, dmem_rdata, dmem_wdata, dmem_we, dmem_be, mem_out); - // if (!rstN) ; // TODO reset pipeline register + stage_memory u_mem(.memop(exmem_reg.memop), + .alu_out(exmem_reg.alu_out), + .rs2_val(exmem_reg.rs2_val), + .mem_out(memwb_reg.mem_out), + .alu_out_passthrough(memwb_reg.alu_out), + .dmem_addr, .dmem_rdata, .dmem_wdata, .dmem_we, .dmem_be); + // MEM/WB pipeline register + always_ff @(posedge clk or negedge rstN) begin + $display("MEM: memop: %h, dmem_addr: %h, dmem_rdata: %h, dmem_wdata: %h, dmem_we: %d, dmem_be: %b, mem_out: %h", + exmem_reg.memop, dmem_addr, dmem_rdata, dmem_wdata, dmem_we, dmem_be, memwb_reg.mem_out); + if (!rstN) memwb_reg = '0; // else ; // TODO I need to think of what else should happen end // TODO find out what else needs to be done in between stages - stage_writeback u_wb(.clk, .rstN, .wbsrc(ctrl.wbsrc), .rd_num, .regfile, - .pc, .imm, .alu_out, .mem_out); + // ================= + // === WRITEBACK === + // ================= + + stage_writeback u_wb(.clk, .rstN, .regfile, + .wbsrc(memwb_reg.wbsrc), + .rd_num(memwb_reg.rd_num), + .pc(memwb_reg.pc), + .imm(memwb_reg.imm), + .alu_out(memwb_reg.alu_out), + .mem_out(memwb_reg.mem_out)); always_ff @(posedge clk) begin - $display("WB: wbsrc: %d, rd_num: %d, pc: %h, imm: %h, alu_out: %h, mem_out: %h", ctrl.wbsrc, rd_num, pc, imm, alu_out, mem_out); + $display("WB: wbsrc: %d, rd_num: %d, pc: %h, imm: %h, alu_out: %h, mem_out: %h", + memwb_reg.wbsrc, memwb_reg.rd_num, memwb_reg.pc, memwb_reg.imm, + memwb_reg.alu_out, memwb_reg.mem_out); end - // TODO find out what else needs to be done after this stage + // TODO move to writeback stage or as separate stage? always_comb begin - unique case (ctrl.pcsrc) + unique case (exmem_reg.pcsrc) // normal case - PC_SEQ: next_pc = pc + 4; + PC_SEQ: next_pc = pc_reg + 4; // branch or jal PC_IMM: begin - if ((ctrl.branchop != BRANCH_NONE) ? branch_taken : 1'b1) next_pc = pc + imm; - else next_pc = pc + 4; + if ((exmem_reg.branchop != BRANCH_NONE) ? exmem_reg.branch_taken : 1'b1) next_pc = pc_reg + memwb_reg.imm; + else next_pc = pc_reg + 4; end // jalr - PC_ALU: next_pc = alu_out & ~32'b1; // spec says to clear LSB and /may/ raise an exception + PC_ALU: next_pc = memwb_reg.alu_out & ~32'b1; // spec says to clear LSB and /may/ raise an exception // CSR xRET PC_MRET: next_pc = csr_mepc & ~32'b11; // bit 0 always cleared for alignment, bit 1 when no C-extension endcase end - assign exception = (ctrl.exception != EXC_NONE) ? ctrl.exception : - (ctrl.csrop != CSR_NONE && csr_illegal) ? EXC_ILL_INST : EXC_NONE; + assign exception = (idex_reg.ctrl.exception != EXC_NONE) ? idex_reg.ctrl.exception : + (idex_reg.ctrl.csrop != CSR_NONE && csr_illegal) ? EXC_ILL_INST : EXC_NONE; endmodule: rv32g_hart diff --git a/rtl/rv32g_pkg.sv b/rtl/rv32g_pkg.sv @@ -297,4 +297,44 @@ package rv32g_pkg; logic fence_flush; logic is_mret; } ctrl_t; + + // pipeline registers + typedef struct packed { + reg_t pc; + logic [31:0] inst; + } ifid_reg_t; + + typedef struct packed { + ctrl_t ctrl; + reg_t pc; + logic [31:0] imm; + reg_t rs1_val; + reg_t rs2_val; + logic [4:0] rd_num; + logic [11:0] csr_addr; + } idex_reg_t; + + typedef struct packed { + reg_t alu_out; + reg_t mem_out; + reg_t csr_out; + reg_t rs2_val; + reg_t pc; + logic [4:0] rd_num; + logic branch_taken; + branchop_t branchop; + memop_t memop; + wbsrc_t wbsrc; + pcsrc_t pcsrc; + } exmem_reg_t; + + typedef struct packed { + reg_t alu_out; + reg_t mem_out; + reg_t csr_out; + reg_t imm; + reg_t pc; + logic [4:0] rd_num; + wbsrc_t wbsrc; + } memwb_reg_t; endpackage: rv32g_pkg diff --git a/rtl/stage_decode.sv b/rtl/stage_decode.sv @@ -1,13 +1,18 @@ module stage_decode import rv32g_pkg::*; - (input logic [31:0] inst, + (input logic clk, + input logic rstN, + input logic [31:0] inst, input logic [31:0] regfile [0:31], output ctrl_t ctrl, output logic [31:0] imm, output logic [31:0] rs1_val, output logic [31:0] rs2_val, output logic [4:0] rd_num, - output logic [11:0] csr_addr); + output logic [11:0] csr_addr, + // passthrough + input reg_t pc_in, + output reg_t pc_out); logic [6:0] funct7; logic [4:0] rs2_num; @@ -24,6 +29,9 @@ module stage_decode funct3 = inst[14:12]; rd_num = inst[11:7]; opcode = opcode_t'(inst[6:0]); + + // passthrorugh + pc_out = pc_in; end // parse the type for each instruction @@ -338,7 +346,14 @@ module stage_decode endcase end - assign rs1_val = regfile[rs1_num]; - assign rs2_val = regfile[rs2_num]; + always_ff @(posedge clk or negedge rstN) begin + if (!rstN) begin + rs1_val <= '0; + rs2_val <= '0; + end else begin + rs1_val <= regfile[rs1_num]; + rs2_val <= regfile[rs2_num]; + end + end endmodule: stage_decode diff --git a/rtl/stage_execute.sv b/rtl/stage_execute.sv @@ -1,6 +1,7 @@ module stage_execute import rv32g_pkg::*; - (input aluop_t op, + (input logic clk, + input aluop_t op, input aluop1src_t op1src, input aluop2src_t op2src, input branchop_t branchop, @@ -26,7 +27,10 @@ module stage_execute logic [31:0] csr_wdata; // privileged machine-mode (subset) - reg_t mstatus_old, mstatus_new; + reg_t mstatus_q, mstatus_d; + always_ff @(posedge clk) begin + if (csr_wren) mstatus_q <= mstatus_d; + end always_comb begin // assign inputs @@ -83,15 +87,14 @@ module stage_execute endcase // privileged machine-mode (subset) + mstatus_d = mstatus_q; if (is_mret) begin - mstatus_old = csr_rdata; - mstatus_new = mstatus_old; // MIE bit 3, MPIE bit 7, MPP bits 12-11 - mstatus_new[3] = mstatus_old[7]; // set MIE to MPIE - mstatus_new[7] = 1'b1; // enable MPIE 1 - mstatus_new[12:11] = 2'b11; // set MPP to M-mode + mstatus_d[3] = mstatus_q[7]; // set MIE to MPIE + mstatus_d[7] = 1'b1; // enable MPIE 1 + mstatus_d[12:11] = 2'b11; // set MPP to M-mode // with only M-mode, don't need to clear MPRV (bit 17) - csr_out = mstatus_new; + csr_out = mstatus_d; end end diff --git a/rtl/stage_fetch.sv b/rtl/stage_fetch.sv @@ -10,16 +10,16 @@ module stage_fetch `define USE_ICACHE // TODO ifdef can be removed once pipelined with BSRAM `ifdef USE_ICACHE - localparam CACHE_SIZE = 256; // words - localparam IDX_WIDTH = $clog2(CACHE_SIZE); - localparam TAG_WIDTH = 32 - IDX_WIDTH - 2; // only be byte offset - - logic [31:0] cache [0:CACHE_SIZE-1]; - logic valid [0:CACHE_SIZE-1]; - logic [IDX_WIDTH-1:0] idx; - logic [TAG_WIDTH-1:0] tag_mem [0:CACHE_SIZE-1]; - logic [TAG_WIDTH-1:0] tag_pc; - logic hit; + localparam CACHE_SIZE = 256; // words + localparam IDX_WIDTH = $clog2(CACHE_SIZE); + localparam TAG_WIDTH = 32 - IDX_WIDTH - 2; // only be byte offset + + (* keep *) logic [31:0] cache [0:CACHE_SIZE-1]; + (* keep *) logic valid [0:CACHE_SIZE-1]; + logic [IDX_WIDTH-1:0] idx; + (* keep *) logic [TAG_WIDTH-1:0] tag_mem [0:CACHE_SIZE-1]; + logic [TAG_WIDTH-1:0] tag_pc; + logic hit; always_comb begin imem_addr = pc; diff --git a/rtl/stage_memory.sv b/rtl/stage_memory.sv @@ -11,7 +11,8 @@ module stage_memory // byte-enable signal is used so that memory controller doesn't overwrite adjacent bytes in word output logic [3:0] dmem_be, - output logic [31:0] mem_out); + output logic [31:0] mem_out, + output logic [31:0] alu_out_passthrough); logic [7:0] bdata; logic [15:0] hdata; diff --git a/rtl/tb_rv32g.sv b/rtl/tb_rv32g.sv @@ -70,12 +70,29 @@ module tb_rv32g .dmem_rdata, .dmem_wdata, .dmem_we, .dmem_be); + logic seen_first_real_inst; + always_ff @(posedge clk or negedge rstN) begin + if (!rstN) + seen_first_real_inst <= 0; + else if (dut.ifid_reg.inst != 32'b0) + seen_first_real_inst <= 1; + end + + parameter int MAX_CYCLES = 10000; always_ff @(posedge clk) begin - if ((!imem_valid && rstN) || - (cycle_cnt > 0 && imem_data == 32'b0) || - (cycle_cnt > 0 && rstN && exception == EXC_ILL_INST)) begin - // if ((!imem_valid && rstN) || - // (cycle_cnt > 0 && imem_data == 32'b0)) begin + if (exception == EXC_ILL_INST && seen_first_real_inst) begin + $display("!!! Program terminated (exception ILL_INST at PC %h) !!!", dut.idex_reg.pc); + $finish(); + end + + if (exception == EXC_CALL || exception == EXC_BKPT) begin + $display("*** Program finished (exception %s) at cycle %0d ***", + exception == EXC_CALL ? "ECALL" : "EBREAK", cycle_cnt); + $finish(); + end + + if (cycle_cnt > MAX_CYCLES) begin + $display("!!! Timeout after %0d cycles !!!", MAX_CYCLES); $finish(); end end