)
从单周期到五级流水手把手教你用Verilog搭建一个能跑起来的LoongArch CPU第一次在FPGA上看到自己设计的CPU运行起来时那种成就感至今难忘。记得当时为了调试一个跳转指令的错误连续三天熬到凌晨两点最终发现是流水线握手信号的时序问题。本文将分享如何从零开始构建支持LoongArch指令集的五级流水线CPU这段经历让我深刻理解了计算机体系结构中那些教科书上的概念是如何在硬件中真实运作的。1. 硬件设计基础准备1.1 开发环境搭建构建CPU前需要准备以下工具链Verilog开发环境推荐使用VivadoXilinx FPGA或QuartusIntel FPGA仿真工具ModelSim或VerilatorLoongArch工具链包括交叉编译器和模拟器测试程序预先编译好的LoongArch二进制文件安装完成后建议先运行一个简单的LED闪烁程序验证工具链是否正常工作。我在第一次搭建环境时因为PATH配置错误浪费了半天时间排查。1.2 单周期CPU回顾五级流水线是在单周期CPU基础上演进而来的先回顾单周期设计的关键点module single_cycle_cpu( input clk, input reset, output [31:0] pc, input [31:0] instr, output mem_write, output [31:0] mem_addr, output [31:0] mem_data ); // 主要组件 reg [31:0] reg_file[31:0]; wire [31:0] alu_result; // 控制信号 wire reg_write, alu_src, mem_to_reg; wire [2:0] alu_control; // 数据通路连接 // ... endmodule单周期设计的主要局限在于所有指令必须在一个时钟周期完成时钟频率受最慢指令限制硬件利用率低大部分组件每个周期只工作一次2. 流水线架构设计2.1 五级流水线划分将CPU工作划分为五个阶段后每个阶段只需完成特定任务流水级名称主要功能关键组件IF取指指令获取PC寄存器指令存储器ID译码指令解码寄存器读取控制单元寄存器文件EXE执行算术逻辑运算ALUMEM访存数据存储器访问数据存储器WB写回结果写回寄存器寄存器文件写端口这种划分使得五条指令可同时在不同阶段执行时钟频率可大幅提高硬件利用率显著提升2.2 流水线缓存设计各级之间需要插入流水线寄存器保存中间结果// IF/ID流水线寄存器示例 reg [31:0] if_id_pc; reg [31:0] if_id_instr; reg if_id_valid; always (posedge clk) begin if (reset) begin if_id_valid 0; end else if (if_allowin) begin if_id_pc if_pc; if_id_instr instr_mem_out; if_id_valid if_valid; end end每个流水线寄存器包含有效位valid标识当前数据是否有效数据字段传递到下个阶段的所有信息控制信号解码产生的控制信号3. 关键模块实现细节3.1 取指阶段IF取指阶段需要处理两个关键问题PC更新逻辑wire [31:0] next_pc br_taken ? br_target : pc 4; always (posedge clk) begin if (reset) pc 32h1bfffffc; // 复位地址 else if (if_allowin) pc next_pc; end指令存储器接口assign inst_sram_en if_valid if_allowin; assign inst_sram_addr next_pc; assign inst_sram_we 4h0; // 只读常见问题指令存储器延迟需要特别注意。在我的实现中指令在时钟上升沿后一个周期才能准备好因此需要额外的等待状态。3.2 译码阶段ID译码阶段是设计中最复杂的部分之一// 主要解码逻辑 always (*) begin case (opcode) 6h00: begin // ADD.W alu_op ADD; reg_write 1; mem_write 0; end 6h0a: begin // LD.W alu_op ADD; reg_write 1; mem_write 0; mem_en 1; end // 其他指令解码... endcase end设计技巧我创建了一个解码器模块来简化控制信号生成module decoder( input [31:0] instr, output reg [11:0] alu_control, output reg mem_write, output reg reg_write, // 其他控制信号... ); // 解码逻辑... endmodule3.3 执行阶段EXE执行阶段主要包含ALU运算和数据存储器地址计算alu u_alu( .alu_op(alu_control), .src1(alu_src1), .src2(alu_src2), .result(alu_result) ); // 数据存储器接口 assign data_sram_addr alu_result; assign data_sram_wdata store_data; assign data_sram_we mem_write ? 4hf : 4h0;ALU设计要点支持LoongArch所有算术逻辑指令注意有符号和无符号运算的区别移位指令需要特殊处理4. 流水线控制机制4.1 握手信号设计流水线各级之间通过握手信号协调// 典型握手信号生成逻辑 assign stage_ready_go 1b1; // 本阶段已完成工作 assign stage_allowin !stage_valid || (stage_ready_go next_stage_allowin); assign stage_to_next_valid stage_valid stage_ready_go;信号说明ready_go本阶段工作完成allowin下一阶段可以接收数据to_next_valid传递到下一阶段的数据有效4.2 数据冒险处理虽然本实验不考虑冲突处理但实际设计中必须解决三种冒险结构冒险通过分离指令和数据存储器解决数据冒险可采用前递Forwarding或停顿Stalling控制冒险通过分支预测和延迟槽解决前递逻辑示例// EXE阶段结果前递到ID阶段 wire [31:0] forwarded_data (exe_reg_write exe_rd id_rs1) ? exe_result : (mem_reg_write mem_rd id_rs1) ? mem_result : id_rs1_data;5. 测试与验证方法5.1 仿真测试框架构建完善的测试环境至关重要module tb_cpu(); reg clk 0; reg reset 1; // 时钟生成 always #5 clk ~clk; initial begin #100 reset 0; #1000 $finish; end // 实例化CPU和存储器 cpu u_cpu(.clk(clk), .reset(reset)); memory u_mem(.clk(clk)); endmodule5.2 功能测试用例建议按以下顺序测试算术指令测试ADD、SUB、SLT等访存指令测试LD、ST跳转指令测试B、BEQ、JIRL综合测试小型计算程序调试技巧遇到问题时可以检查流水线寄存器值是否正确传递验证握手信号时序查看波形图中关键信号的变化6. 完整代码结构项目建议采用如下目录结构/rtl /cpu if_stage.v id_stage.v exe_stage.v mem_stage.v wb_stage.v mycpu_top.v /lib alu.v regfile.v decoder.v /tb tb_cpu.v test_programs关键文件说明mycpu_top.vCPU顶层模块连接各流水级alu.v算术逻辑单元实现regfile.v寄存器文件实现7. 性能优化方向基础流水线完成后可考虑以下优化分支预测减少控制冒险带来的性能损失指令缓存提高指令读取速度数据前递完善数据冒险处理超标量支持多指令并行实测数据在我的Basys3 FPGA上优化前后的性能对比优化措施最高频率(MHz)Dhrystone分数基础流水线501.0加入分支预测651.4加入指令缓存751.8记得第一次成功运行Dhrystone测试时虽然分数不高但那种它真的工作了的兴奋感正是硬件设计的魅力所在。建议读者在完成基础版本后尝试添加自己的优化创新这才是学习计算机体系结构最有效的方式。