第14课 · 单周期CPU
完整CPUADD/LW/SW/BEQVerilator
📌 学习目标:将数据通路和控制单元组合成完整的 RISC-V 单周期 CPU,执行 ADD、LW、SW、BEQ 四类指令,通过 Verilator 验证程序执行结果。
一、单周期 CPU 架构
单周期 CPU 在每个时钟周期完成一条指令的全部操作:取指→译码→执行→访存→写回。
┌────────┐ ┌───────────┐ ┌──────────┐
│ PC │──→│ 指令存储器 │──→│ 译码器 │──→ 控制信号
└───┬────┘ └───────────┘ └──────────┘
│ │
│ ┌───────────────┤
↓ ↓ ↓
PC+4/跳转 ┌──────────┐ ┌─────────┐
│寄存器文件 │──→│ ALU │
└──────────┘ └─────────┘
↑ │
imm(立即数) ↓
┌──────────┐
│数据存储器 │
└──────────┘
│
↓
写回寄存器文件
二、Verilog 实现
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;
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;
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;
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 测试台
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;
uut.imem[0] = 32'h00500093;
uut.imem[1] = 32'h00300113;
uut.imem[2] = 32'h002081B3;
uut.imem[3] = 32'h00302023;
uut.imem[4] = 32'h00002203;
uut.imem[5] = 32'h00208463;
#12; rst = 0;
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课:流水线基础