从单周期到五级流水:手把手教你用Verilog搭建一个最简单的LoongArch CPU(附完整代码)
从单周期到五级流水:手把手教你用Verilog搭建一个最简单的LoongArch CPU
第一次接触CPU设计时,看着那些复杂的流水线结构图,我完全摸不着头脑。直到自己动手用Verilog从零开始实现一个单周期CPU,再逐步演进到五级流水线,才真正理解了计算机体系结构的精妙之处。本文将带你完整走一遍这个学习过程,用最直观的方式掌握CPU设计的核心思想。
1. 准备工作:认识LoongArch与设计工具
LoongArch是近年来兴起的一种精简指令集架构(RISC),其设计理念与MIPS类似但更加现代化。选择它作为学习对象有几个优势:
- 指令集精简:基础指令不到100条,学习曲线平缓
- 文档丰富:官方提供了详细的参考手册和示例代码
- 实践性强:可以直接在FPGA上运行验证
开发环境配置:
# 推荐使用以下工具链 iverilog -v # Icarus Verilog仿真器 gtkwave # 波形查看工具硬件描述语言我们选择Verilog,因为它:
- 语法相对简单,适合初学者
- 仿真工具链成熟
- 被业界广泛采用
2. 单周期CPU:理解计算机的基本工作原理
单周期CPU是最基础的设计,所有指令在一个时钟周期内完成。虽然效率低,但非常适合教学。
2.1 核心部件设计
一个最简单的CPU需要这些组件:
| 部件 | 功能描述 | Verilog模块示例 |
|---|---|---|
| PC寄存器 | 保存下条指令地址 | reg [31:0] pc |
| 指令存储器 | 存储程序指令 | reg [31:0] inst_mem[0:1023] |
| 寄存器堆 | 32个通用寄存器 | reg [31:0] reg_file[0:31] |
| ALU | 算术逻辑运算单元 | 下文详细实现 |
2.2 数据通路实现
关键数据流动路径:
// 取指阶段 wire [31:0] inst = inst_mem[pc>>2]; // 译码阶段 wire [4:0] rs1 = inst[19:15]; wire [4:0] rs2 = inst[24:20]; wire [4:0] rd = inst[11:7]; wire [31:0] rs1_data = reg_file[rs1]; wire [31:0] rs2_data = reg_file[rs2]; // 执行阶段 wire [31:0] alu_result = alu(rs1_data, rs2_data, alu_op); // 写回阶段 always @(posedge clk) begin if (reg_write_en) reg_file[rd] <= alu_result; pc <= pc + 4; end注意:单周期设计下,时钟周期必须足够长以完成最复杂指令,这导致性能低下。
3. 流水线原理:性能提升的关键技术
流水线就像工厂的装配线,将指令执行分成多个阶段并行处理。五级流水线典型划分:
- IF:取指令
- ID:指令译码
- EXE:执行运算
- MEM:数据存取
- WB:结果写回
3.1 流水线寄存器设计
各阶段间需要寄存器保存中间结果:
// IF/ID流水线寄存器 reg [31:0] id_pc; reg [31:0] id_inst; always @(posedge clk) begin if (!stall) begin id_pc <= if_pc; id_inst <= if_inst; end end3.2 流水线冲突与解决
虽然我们暂时不考虑冲突处理,但需要了解三类主要冲突:
- 结构冲突:硬件资源争用
- 数据冲突:数据依赖关系
- 控制冲突:分支指令导致
4. 五级流水线实现:从理论到实践
让我们分阶段实现这个LoongArch流水线CPU。
4.1 取指阶段(IF)
module if_stage( input clk, input reset, input [31:0] br_target, input br_taken, output [31:0] pc, output [31:0] inst ); reg [31:0] pc_reg; wire [31:0] next_pc = br_taken ? br_target : pc_reg + 4; always @(posedge clk) begin if (reset) pc_reg <= 32'h1c000000; else pc_reg <= next_pc; end assign pc = pc_reg; inst_rom u_inst_rom(.addr(pc[31:2]), .data(inst)); endmodule4.2 译码阶段(ID)
这个阶段需要:
- 解析指令字段
- 读取寄存器堆
- 生成控制信号
wire [6:0] opcode = id_inst[6:0]; wire [2:0] funct3 = id_inst[14:12]; wire [6:0] funct7 = id_inst[31:25]; // 控制信号生成 always @(*) begin case(opcode) 7'b0110011: begin // R-type reg_write_en = 1'b1; alu_src = 2'b00; mem_write = 1'b0; end // 其他指令类型... endcase end4.3 执行阶段(EXE)
ALU是这一阶段的核心:
module alu( input [31:0] a, b, input [3:0] alu_op, output reg [31:0] result ); always @(*) begin case(alu_op) 4'b0000: result = a + b; // ADD 4'b0001: result = a - b; // SUB 4'b0010: result = a & b; // AND // 其他ALU操作... default: result = 32'b0; endcase end endmodule4.4 访存阶段(MEM)
module mem_stage( input clk, input mem_read, input mem_write, input [31:0] addr, input [31:0] write_data, output [31:0] read_data ); data_ram u_data_ram( .clk(clk), .we(mem_write), .addr(addr[31:2]), .din(write_data), .dout(read_data) ); endmodule4.5 写回阶段(WB)
always @(posedge clk) begin if (reg_write_en) begin if (mem_to_reg) reg_file[write_reg] <= mem_data; else reg_file[write_reg] <= alu_result; end end5. 系统集成与测试
将各阶段模块连接起来:
module cpu_top( input clk, input reset ); // 各阶段间连线 wire [31:0] if_pc, if_inst; wire [31:0] id_pc, id_inst; wire [31:0] exe_alu_result; // ...其他信号 // 实例化各阶段 if_stage u_if_stage(.clk(clk), .reset(reset), /*...*/); id_stage u_id_stage(.clk(clk), .reset(reset), /*...*/); // ...其他阶段 endmodule测试程序示例:
main: addi x1, x0, 10 # x1 = 10 addi x2, x0, 20 # x2 = 20 add x3, x1, x2 # x3 = x1 + x2 sw x3, 0(x0) # 存储结果在仿真中观察波形,确认各寄存器值变化符合预期。第一次看到自己设计的CPU正确执行程序时,那种成就感是无与伦比的。
