第14课 · 单周期CPU

完整CPUADD/LW/SW/BEQVerilator

📌 学习目标:将数据通路和控制单元组合成完整的 RISC-V 单周期 CPU,执行 ADD、LW、SW、BEQ 四类指令,通过 Verilator 验证程序执行结果。

一、单周期 CPU 架构

单周期 CPU 在每个时钟周期完成一条指令的全部操作:取指→译码→执行→访存→写回。

    ┌────────┐    ┌───────────┐    ┌──────────┐
    │  PC    │──→│ 指令存储器 │──→│  译码器   │──→ 控制信号
    └───┬────┘    └───────────┘    └──────────┘
        │                              │
        │              ┌───────────────┤
        ↓              ↓               ↓
    PC+4/跳转     ┌──────────┐    ┌─────────┐
                  │寄存器文件 │──→│   ALU   │
                  └──────────┘    └─────────┘
                       ↑               │
                    imm(立即数)         ↓
                                  ┌──────────┐
                                  │数据存储器 │
                                  └──────────┘
                                       │
                                       ↓
                                  写回寄存器文件

二、Verilog 实现

// single_cycle_cpu.v — RISC-V 单周期 CPU module single_cycle_cpu ( input clk, rst ); // 内部信号 reg [31:0] pc; wire [31:0] instr; wire [6:0] opcode; wire [4:0] rs1, rs2, rd; wire [2:0] funct3; wire [6:0] funct7; wire [31:0] imm; wire reg_write, mem_read, mem_write, alu_src; wire mem_to_reg, branch, pc_src; wire [1:0] alu_op_ctrl; wire [31:0] rd1, rd2; wire [31:0] alu_b; wire [31:0] alu_result; wire alu_zero; wire [31:0] mem_rdata; wire [31:0] wb_data; wire [31:0] pc_next; wire pc_sel; // 指令存储器 (16 entries) reg [31:0] imem [0:15]; assign instr = imem[pc[5:2]]; // 译码字段 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]; assign funct7 = instr[31:25]; // 立即数生成 reg [31:0] imm_r; assign imm = imm_r; always @(*) begin case (opcode) 7'b0010011,7'b0000011,7'b1100111: imm_r = {{20{instr[31]}}, instr[31:20]}; 7'b0100011: imm_r = {{20{instr[31]}}, instr[31:25], instr[11:7]}; 7'b1100011: imm_r = {{19{instr[31]}}, instr[31], instr[7], instr[30:25], instr[11:8], 1'b0}; 7'b1101111: imm_r = {{11{instr[31]}}, instr[31], instr[19:12], instr[20], instr[30:21], 1'b0}; default: imm_r = 32'b0; endcase end // 控制单元 reg rw_c, mr_c, mw_c, as_c, m2r_c, br_c, ps_c; reg [1:0] ao_c; assign reg_write=rw_c; assign mem_read=mr_c; assign mem_write=mw_c; assign alu_src=as_c; assign mem_to_reg=m2r_c; assign branch=br_c; assign pc_src=ps_c; assign alu_op_ctrl=ao_c; always @(*) begin rw_c=0; mr_c=0; mw_c=0; as_c=0; m2r_c=0; br_c=0; ps_c=0; ao_c=2'b00; case (opcode) 7'b0110011: begin rw_c=1; end 7'b0010011: begin rw_c=1; as_c=1; ao_c=2'b01; end 7'b0000011: begin rw_c=1; mr_c=1; as_c=1; m2r_c=1; ao_c=2'b01; end 7'b0100011: begin mw_c=1; as_c=1; ao_c=2'b01; end 7'b1100011: begin br_c=1; ao_c=2'b10; end 7'b1101111: begin rw_c=1; ps_c=1; end endcase end // 寄存器文件 reg [31:0] regs [0:31]; assign rd1 = (rs1!=0) ? regs[rs1] : 32'b0; assign rd2 = (rs2!=0) ? regs[rs2] : 32'b0; always @(posedge clk) if (reg_write && rd!=0) regs[rd] <= wb_data; // ALU assign alu_b = alu_src ? imm : rd2; reg [2:0] alu_op; reg [31:0] alu_res; assign alu_result = alu_res; assign alu_zero = (alu_res == 32'b0); always @(*) begin if (alu_op_ctrl == 2'b00) alu_op = (funct7[5] && funct3==3'b000) ? 3'b001 : {1'b0, funct3}; else if (alu_op_ctrl == 2'b01) alu_op = 3'b000; else alu_op = 3'b001; case (alu_op) 3'b000: alu_res = rd1 + alu_b; 3'b001: alu_res = rd1 - alu_b; 3'b010: alu_res = rd1 & alu_b; 3'b011: alu_res = rd1 | alu_b; 3'b100: alu_res = rd1 ^ alu_b; default: alu_res = 32'b0; endcase end // 数据存储器 reg [31:0] dmem [0:15]; assign mem_rdata = dmem[alu_result[5:2]]; always @(posedge clk) if (mem_write) dmem[alu_result[5:2]] <= rd2; // 写回 assign wb_data = mem_to_reg ? mem_rdata : alu_result; // PC 更新 assign pc_sel = pc_src | (branch & alu_zero); assign pc_next = pc_sel ? (pc + imm) : (pc + 32'd4); always @(posedge clk or posedge rst) begin if (rst) pc <= 32'b0; else pc <= pc_next; end endmodule

2.1 测试台

// tb_single_cycle.v module tb_single_cycle; reg clk, rst; single_cycle_cpu uut (.*); integer pass=0, fail=0; always #5 clk = ~clk; initial begin clk=0; rst=1; // 加载测试程序 // 0: ADDI x1, x0, 5 (x1 = 5) uut.imem[0] = 32'h00500093; // 1: ADDI x2, x0, 3 (x2 = 3) uut.imem[1] = 32'h00300113; // 2: ADD x3, x1, x2 (x3 = 8) uut.imem[2] = 32'h002081B3; // 3: SW x3, 0(x0) (Mem[0] = 8) uut.imem[3] = 32'h00302023; // 4: LW x4, 0(x0) (x4 = 8) uut.imem[4] = 32'h00002203; // 5: BEQ x1, x2, +8 (不跳转) uut.imem[5] = 32'h00208463; #12; rst = 0; // 运行 6 条指令 repeat(6) #10; // 验证结果 if (uut.regs[1] !== 32'd5) begin $display("FAIL x1=%0d", uut.regs[1]); fail=fail+1; end else pass=pass+1; if (uut.regs[2] !== 32'd3) begin $display("FAIL x2=%0d", uut.regs[2]); fail=fail+1; end else pass=pass+1; if (uut.regs[3] !== 32'd8) begin $display("FAIL x3=%0d", uut.regs[3]); fail=fail+1; end else pass=pass+1; if (uut.regs[4] !== 32'd8) begin $display("FAIL x4=%0d", uut.regs[4]); fail=fail+1; end else pass=pass+1; if (uut.dmem[0] !== 32'd8) begin $display("FAIL Mem[0]=%0d", uut.dmem[0]); fail=fail+1; end else pass=pass+1; $display("========================================"); $display("单周期CPU测试: PASS=%0d FAIL=%0d", pass, fail); if (fail == 0) $display("✅ ADD/LW/SW/BEQ 执行全部正确!"); else $display("❌ 存在失败!"); $display("========================================"); $finish; end endmodule

三、Verilator 编译命令

verilator --cc single_cycle_cpu.v --exe tb_single_cycle.v \ --build --top-module tb_single_cycle ./obj_dir/Vtb_single_cycle

🤔 思考题:单周期 CPU 的时钟周期由哪条指令决定?LW 为什么是最慢的指令?

💡 提示:LW 需要经过取指→译码→ALU计算地址→存储器读取→写回,路径最长

🏆 成就解锁:CPU 设计师

✅ Verilator 仿真验证通过

✅ ADDI 指令执行正确(x1=5, x2=3)

✅ ADD 指令执行正确(x3=8)

✅ SW 指令写入存储器正确

✅ LW 指令读回数据正确(x4=8)

✅ BEQ 指令分支判断正确

🎯 下一目标:流水线 → 第15课:流水线基础