这个文章我们从0开始实现一个CPU,完整地实现取指、译码、执行、回写四大操作过程。
1、PC计数器
核心逻辑 (Logic Flow): ①复位 (Reset):rst_n 拉低时,PC 归零。 ②跳转 (Jump):当 jump_en 为 1,PC 强制更新为目标地址 jump_addr(用于分支或函数调用)。 ③顺序执行 (Fetch):默认情况下,PC 每周期 +4(对应 32 位指令字长),自动指向下一条指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 module pc_counter#( parameter AW =32 )( input logic clk, input logic rst_n, input logic jump_en, input logic [AW-1:0] jump_addr, output logic [AW-1:0] pc_pointer ); always_ff@(posedge clk or negedge rst_n)begin if(!rst_n) pc_pointer<='h0; else if(jump_en) pc_pointer<=jump_addr; else pc_pointer<=pc_pointer+'h4; end endmodule
2、ROM存储器
核心逻辑 (Logic Flow): ①初始化 (Initialization):利用 $readmemh 在仿真开始瞬间,从指定路径(如 rv32ui-p-addi.txt)读取十六进制指令,填满 4KB (4096 x 32bit) 的数组。 ②字对齐 (Word Alignment):核心代码是 rom_reg[addr_in[AW-1:2]]。因为 PC 是按字节 (Byte) 寻址,而 ROM 是按 32位字 (Word) 存储,所以取地址时必须丢弃低两位(相当于除以 4),否则取到的指令全是错的。 ③组合逻辑读取 (Asynchronous Read):使用 always_comb,数据输出不依赖时钟边沿,地址一变,指令立马出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 module rom #( parameter AW=32 , parameter DW=32 , parameter file = "rv32ui-p-addi.txt" )( input logic [AW-1 :0 ] addr_in, output logic [DW-1 :0 ] data_out ); static string PATH="../test_data/" ;static string full_path={PATH,file};logic [DW-1 :0 ] rom_reg [0 :4095 ];initial begin $readmemh (full_path,rom_reg); end always_comb begin data_out=rom_reg[addr_in[AW-1 :2 ]]; end endmodule
3、if2id 我们的CPU的使用流水线寄存器 (Pipeline Register),if2id体位于 取指 (IF) 和 译码 (ID) 阶段之间。它的作用是切断关键路径,让 CPU 能够跑得更快。
这个模块核心的功能就是:把pc寄存器给的地址,还有把从ROM取得的指令,用寄存器切割,然后再传到下一级
但是这里是涉及到流水线冲刷的,我们来用几个图简单看一下流水线冲刷机制是啥:
检测到有跳转指令后,进行指令冲刷
赋值为 NOP (No Operation),唯一的目的就是为了无害化处理。
为什么要保护那 32 个寄存器?(The Why)
CPU 的流水线最终都会汇聚到一个阶段:写回 (Write Back, WB)。 正常情况:指令经过运算后,会把结果写进寄存器堆(Register File,那 32 个通用寄存器)。 危险情况:如果刚才因为分支预测失败(Flush),流水线里残留了一条错误的指令(比如 add x1, x2, x3),如果不把它变成 NOP,它就会一路跑到 WB 阶段,把 x1 的值改掉。 后果:程序逻辑直接崩盘,因为 x1 被错误地污染了。 NOP 的作用就是告诉 WB 阶段:“别动!什么都别写!当作无事发生。
在 RISC-V 中,NOP 其实是一个伪指令。它实际上是:$$\text{addi } x0, x0, 0$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 `include "define .sv" module if2id #( parameter AW=32 , parameter DW=32 )( input logic clk, input logic rst_n, input logic [AW-1 :0 ] in_addr, input logic [DW-1 :0 ] in_instruction, input logic instr_flush, output logic [AW-1 :0 ] out_addr, output logic [DW-1 :0 ] out_instruction ); always_ff @(posedge clk or negedge rst_n)begin if (!rst_n)begin out_addr<='h0 ; out_instruction<=`INST_NOP; end else if (instr_flush)begin out_addr<='h0 ; out_instruction<=`INST_NOP; end else begin out_addr<=in_addr; out_instruction<=in_instruction; end end endmodule
4、deine.sv指令解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 `timescale 1ns / 1ps `define AW 32 `define DW 32 `define FILE "rv32ui-p-addi.dat" `define INST_TYPE_I 7'b0010011 `define INST_ADDI 3'b000 `define INST_SLTI 3'b010 `define INST_SLTIU 3'b011 `define INST_XORI 3'b100 `define INST_ORI 3'b110 `define INST_ANDI 3'b111 `define INST_SLLI 3'b001 `define INST_SRI 3'b101 `define INST_TYPE_L 7'b0000011 `define INST_LB 3'b000 `define INST_LH 3'b001 `define INST_LW 3'b010 `define INST_LBU 3'b100 `define INST_LHU 3'b101 `define INST_TYPE_S 7'b0100011 `define INST_SB 3'b000 `define INST_SH 3'b001 `define INST_SW 3'b010 `define INST_TYPE_R_M 7'b0110011 `define INST_ADD_SUB 3'b000 `define INST_SLL 3'b001 `define INST_SLT 3'b010 `define INST_SLTU 3'b011 `define INST_XOR 3'b100 `define INST_SR 3'b101 `define INST_OR 3'b110 `define INST_AND 3'b111 `define INST_MUL 3'b000 `define INST_MULH 3'b001 `define INST_MULHSU 3'b010 `define INST_MULHU 3'b011 `define INST_DIV 3'b100 `define INST_DIVU 3'b101 `define INST_REM 3'b110 `define INST_REMU 3'b111 `define INST_JAL 7'b1101111 `define INST_JALR 7'b1100111 `define INST_LUI 7'b0110111 `define INST_LUIPC 7'b0010111 `define INST_NOP 32'h00000013 `define INST_NOP_OP 7'b0000001 `define INST_MRET 32'h30200073 `define INST_RET 32'h00008067 `define INST_FENCE 7'b0001111 `define INST_ECALL 32'h00000073 `define INST_EBREAK 32'h00100073 `define INST_TYPE_B 7'b1100011 `define INST_BEQ 3'b000 `define INST_BNE 3'b001 `define INST_BLT 3'b100 `define INST_BGE 3'b101 `define INST_BLTU 3'b110 `define INST_BGEU 3'b111
5、通用寄存器register
2R1W 结构:支持 2路读 (read_rs1, read_rs2) 和 1路写 (ex_write)。这是现代 RISC-V 整数流水线的标准配置。
32个寄存器:通过 regs[0:31] 定义了 x0 到 x31。
x0 恒零保护 (Zero Register Protection) 读的时候:read_rs1_addr == ‘h0 时,强制输出 0。 写的时候:ex_write_addr != ‘h0 时才允许写入。 作用:完美符合 RISC-V 规范,确保 x0 永远是 0,怎么写都没用。
内部前递 (Internal Forwarding / Write-through) —— 这段代码的精华!
1 2 else if (ex_wirite_en && (read_rs1_addr == ex_write_addr)) out_rs1_data = ex_write_data;
场景:如果在同一个时钟周期内,你既要读地址 x5,又要往 x5 写新值。如果没有这段逻辑,读出来的就是“旧值”;有了这段逻辑,读出来的就是“新值”。这能让流水线少停顿一个周期。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 module register #( parameter DW=32 )( input logic clk, input logic rst_n, input logic [4 :0 ] read_rs1_addr, input logic [4 :0 ] read_rs2_addr, output logic [DW-1 :0 ] out_rs1_data, output logic [DW-1 :0 ] out_rs2_data, input logic ex_write_en, input logic [4 :0 ] ex_write_addr, input logic [DW-1 :0 ] ex_write_data ); logic [DW-1 :0 ] regs[0 :31 ];always_comb begin if (!rst_n) out_rs1_data='h0 ; else if (read_rs1_addr=='h0 ) out_rs1_data='h0 ; else if (ex_wirite_en&&(read_rs1_addr==ex_write_addr)) out_rs1_data=ex_write_data; else out_rs1_data=regs[read_rs1_addr]; end always_comb begin if (!rst_n) out_rs2_data='h0 ; else if (read_rs2_addr=='h0 ) out_rs2_data='h0 ; else if (ex_write_en&&(read_rs2_addr==ex_write_addr)) out_rs2_data=ex_write_data; else out_rs2_data=regs[read_rs2_addr]; end always_ff @(posedge clk or negedge rst_n)begin if (!rst_n)begin for (int i=0 ;i<32 ;i++)begin regs[i]<='h0 ; end end else if (ex_write_en&&(ex_write_addr!='h0 )) regs[ex_write_addr]<=ex_write_data; end endmodule
6、decoder ①JALR指令: jalr 指令使用 12 位立即数(有符号数)作为偏移量,与操作数寄存器 rs1中的值相加,然后将结果的最低有效位置0。jalr指令将其下一条指令的 PC(即当前指令PC+4)的值写入其结果寄存器 rd。
立即数为20位到31位的指令:
L型指令:
S型指令:
B型指令:
模块名称:Decoder (译码器) 核心功能:
切片 (Slicing):将 32 位指令切割成 opcode, rs1, rs2, rd, funct3/7。
立即数生成 (Imm Gen):根据指令类型(I/S/B/J/U),将碎片化的立即数拼接并扩展为 32 位。
操作数分发 (Operand Mux):根据指令类型,决定 ALU 的两个输入操作数 (op1, op2) 到底来自寄存器堆 (rs_data)、PC (in_addr) 还是立即数 (imm)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 `timescale 1ns /1ps `include "define .sv" module decode #( parameter AW = 32 , parameter DW = 32 )( input logic [AW-1 :0 ] instr_addr_in input logic [DW-1 :0 ] instr_in output logic [4 :0 ] rd_rs1_addr output logic [4 :0 ] rd_rs2_addr input logic [DW-1 :0 ] rd_rs1_data input logic [DW-1 :0 ] rd_rs2_data output logic [DW-1 :0 ] op1_out output logic [DW-1 :0 ] op2_out ); logic [6 :0 ] opcode ;logic [4 :0 ] rd ;logic [2 :0 ] func3 ;logic [4 :0 ] rs1 ;logic [4 :0 ] rs2 ;logic [6 :0 ] func7 ;logic [31 :0 ] imm ;assign opcode = instr_in[6 :0 ] ;assign rd = instr_in[11 :7 ] ;assign func3 = instr_in[14 :12 ] ;assign rs1 = instr_in[19 :15 ] ;assign rs2 = instr_in[24 :20 ] ;assign func7 = instr_in[31 :25 ] ;always_comb begin case (opcode) `INST_TYPE_I,`INST_TYPE_L,`INST_JALR: imm = {{20 {instr_in[31 ]}},instr_in[31 :20 ]}; `INST_TYPE_S: imm = {{20 {instr_in[31 ]}},instr_in[31 :25 ],instr_in[11 :7 ]}; `INST_TYPE_B: imm = {{20 {instr_in[31 ]}},instr_in[7 ],instr_in[30 :25 ],instr_in[11 :8 ],1'b0 }; `INST_JAL: imm = {{12 {instr_in[31 ]}},instr_in[19 :12 ],instr_in[20 ],instr_in[30 :21 ], 1'b0 }; `INST_LUI,`INST_LUIPC: imm = {instr_in[31 :12 ],12'h0 }; default :imm = 32'h0 ; endcase end always_comb begin case (opcode) `INST_TYPE_I:begin case (func3) `INST_ADDI:begin rd_rs1_addr = rs1 ; rd_rs2_addr = 5'h0 ; op1_out = rd_rs1_data ; op2_out = imm ; end default :begin rd_rs1_addr = 'h0 ; rd_rs2_addr = 'h0 ; op1_out = 'h0 ; op2_out = 'h0 ; end endcase end `INST_TYPE_R_M:begin case (func3) `INST_ADD_SUB:begin rd_rs1_addr = rs1 ; rd_rs2_addr = rs2 ; op1_out = rd_rs1_data ; op2_out = rd_rs2_data ; end default :begin rd_rs1_addr = 'h0 ; rd_rs2_addr = 'h0 ; op1_out = 'h0 ; op2_out = 'h0 ; end endcase end `INST_TYPE_B:begin case (func3) `INST_BNE:begin rd_rs1_addr = rs1 ; rd_rs2_addr = rs2 ; op1_out = rd_rs1_data ; op2_out = rd_rs2_data ; end default :begin rd_rs1_addr = 'h0 ; rd_rs2_addr = 'h0 ; op1_out = 'h0 ; op2_out = 'h0 ; end endcase end `INST_JAL:begin rd_rs1_addr = 5'h0 ; rd_rs2_addr = 5'h0 ; op1_out = 32'h0 ; op2_out = imm ; end `INST_JALR:begin rd_rs1_addr = rs1 ; rd_rs2_addr = 5'h0 ; op1_out = rd_rs1_data ; op2_out = imm ; end `INST_LUI:begin rd_rs1_addr = 5'h0 ; rd_rs2_addr = 5'h0 ; op1_out = 32'h0 ; op2_out = imm ; end `INST_LUIPC:begin rd_rs1_addr = 5'h0 ; rd_rs2_addr = 5'h0 ; op1_out = instr_addr_in ; op2_out = imm ; end `INST_NOP_OP: begin rd_rs1_addr = 5'h0 ; rd_rs2_addr = 5'h0 ; op1_out = 32'h0 ; op2_out = 32'h0 ; end default :begin rd_rs1_addr = 'h0 ; rd_rs2_addr = 'h0 ; op1_out = 'h0 ; op2_out = 'h0 ; end endcase end endmodule
7、id2ex instruction to excute
核心位置:位于 译码阶段 (ID) 和 执行阶段 (EX) 之间。
主要功能:打拍(隔离)。在时钟上升沿,将译码出的 PC地址、指令码、操作数 1 (op1)、操作数 2 (op2) 锁存并传递给执行单元。
instr_flush 信号在这里 不是“保持” (Stall),而是 “冲刷” (Flush)。
当 instr_flush 为 1 时,它把输出强制设为 NOP (空指令) 和 0,相当于向流水线插入了一个气泡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 `include "define .sv" module id2ex #( parameter AW = 32 , parameter DW = 32 )( input logic clk , input logic rst_n , input logic instr_flush , input logic [AW-1 :0 ] instr_addr_in , input logic [DW-1 :0 ] instr_in , input logic [DW-1 :0 ] op1_in , input logic [DW-1 :0 ] op2_in , output logic [AW-1 :0 ] instr_addr_out , output logic [DW-1 :0 ] instr_out , output logic [DW-1 :0 ] op1_out , output logic [DW-1 :0 ] op2_out ); always_ff @(posedge clk or negedge rst_n) if (!rst_n) begin instr_addr_out <= 'h0 ; instr_out <= `INST_NOP; op1_out <= 'h0 ; op2_out <= 'h0 ; end else if (instr_flush) begin instr_addr_out <= 'h0 ; instr_out <= `INST_NOP; op1_out <= 'h0 ; op2_out <= 'h0 ; end else begin instr_addr_out <= instr_addr_in; instr_out <= instr_in ; op1_out <= op1_in ; op2_out <= op2_in ; end
8、excute 我们重点来看一下B型指令的组成:
第 0 位 (imm[0]):强制填 0 来源:无(不需要指令提供)。 操作:直接补一个 0。 为什么:RISC-V 规定跳转地址必须是 2 的倍数(偶数),所以最低位永远是 0。
第 1~4 位 (imm[4:1]):抓指令的 [11:8] 来源:instr[11:8] 操作:直接搬过来。
第 5~10 位 (imm[10:5]):抓指令的 来源:instr[30:25] 操作:直接搬过来。
第 11 位 (imm[11]):抓指令的 [7] 来源:instr[7] 注意:这就是最“乱”的那一位,它独自藏在指令的低位里。
第 12 位 (imm[12]):抓指令的 [31] 来源:instr[31] (最高位) 意义:这是符号位(正负号)。
第 13~31 位 (imm[31:13]):复制符号位 来源:还是 instr[31] 操作:把第 12 位拿到的那个数(0或1),疯狂复制 19 次,填满剩下的高位。这叫符号扩展。
我们来看一下B型、JAL型、JALR型指令地址的转换是怎么计算的
B型 / JAL:以 PC 为基准起跳 (PC + imm)。
JALR:以 寄存器 (rs1) 为基准起跳 (rs1 + imm)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 `timescale 1ns / 1ps `include "define .sv" module execute #( parameter AW = 32 , parameter DW = 32 )( input logic [AW-1 :0 ] instr_addr, input logic [DW-1 :0 ] instr, input logic [DW-1 :0 ] op1, input logic [DW-1 :0 ] op2, output logic wr_reg_en, output logic [4 :0 ] wr_reg_addr, output logic [DW-1 :0 ] wr_reg_data, output logic jump_en, output logic [AW-1 :0 ] jump_addr, output logic jump_hold ); logic [6 :0 ] opcode; logic [4 :0 ] rd; logic [2 :0 ] func3; logic [6 :0 ] func7; logic [31 :0 ] imm; logic equal; logic [AW-1 :0 ] jump_imm; assign opcode = instr[6 :0 ]; assign rd = instr[11 :7 ]; assign func3 = instr[14 :12 ]; assign func7 = instr[31 :25 ]; assign imm = {{20 {instr[31 ]}},instr[7 ],instr[30 :25 ],instr[11 :8 ],1'b0 }; assign equal = (op1 == op2) ? 1'b1 : 1'b0 ; assign jump_imm = instr_addr + imm; always_comb begin case (opcode) `INST_TYPE_I:begin case (func3) `INST_ADDI:begin wr_reg_en = 1'b1 ; wr_reg_addr = rd; wr_reg_data = op1 + op2; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end default :begin wr_reg_en = 1'b0 ; wr_reg_addr = 5'h0 ; wr_reg_data = 'h0 ; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end endcase end `INST_TYPE_R_M:begin case (func3) `INST_ADD_SUB:begin if (func7 == 7'b000_0000 ) begin wr_reg_en = 1'b1 ; wr_reg_addr = rd; wr_reg_data = op1 + op2; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end else begin wr_reg_en = 1'b1 ; wr_reg_addr = rd; wr_reg_data = op1 - op2; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end end default :begin wr_reg_en = 1'b0 ; wr_reg_addr = 5'h0 ; wr_reg_data = 'h0 ; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end endcase end `INST_TYPE_B: begin case (func3) `INST_BNE: begin wr_reg_en = 1'b0 ; wr_reg_addr = 5'h0 ; wr_reg_data = 'h0 ; jump_en = ~equal; jump_addr = ~equal ? jump_imm : 'h0 ; jump_hold = 1'b0 ; end default :begin wr_reg_en = 1'b0 ; wr_reg_addr = 5'h0 ; wr_reg_data = 'h0 ; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end endcase end `INST_JAL:begin wr_reg_en = 1'b1 ; wr_reg_addr = rd; wr_reg_data = instr_addr + 32'h4 ; jump_en = 1'b1 ; jump_addr = instr_addr + op2; jump_hold = 1'b0 ; end `INST_JALR:begin wr_reg_en = 1'b1 ; wr_reg_addr = rd; wr_reg_data = instr_addr + 32'h4 ; jump_en = 1'b1 ; jump_addr = op1 + op2; jump_hold = 1'b0 ; end `INST_LUI:begin wr_reg_en = 1'b1 ; wr_reg_addr = rd; wr_reg_data = op2; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end `INST_LUIPC:begin wr_reg_en = 1'b1 ; wr_reg_addr = rd; wr_reg_data = op1 + op2; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end `INST_NOP_OP:begin wr_reg_en = 1'b0 ; wr_reg_addr = 5'h0 ; wr_reg_data = 'h0 ; jump_en = 1'b0 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end default :begin wr_reg_en = 1'b0 ; wr_reg_addr = 5'h0 ; wr_reg_data = 'h0 ; jump_en = 1'b1 ; jump_addr = 'h0 ; jump_hold = 1'b0 ; end endcase end endmodule