项目概述
在《计算机组成原理》课程中,五级流水线、数据旁路、控制冒险等概念往往停留在理论层面。实际在 Vivado 中进行 FPGA 开发时,PC 跳转、寄存器读写冲突、分支预测失败后的处理等问题容易让人困惑。
本项目从零开始,在 Xilinx Artix-7 FPGA 上实现一个完整的 RISC-V 五级流水线 CPU。支持跑通汇编程序并验证硬件功能。
我们不堆术语,只讲实战细节:每个模块怎么写,关键信号怎么连,坑在哪里,怎么绕过去。
设计选型依据
为什么选 RISC-V + 五级流水?
性能和资源的平衡是关键。
单周期 CPU 虽然代码简单,但综合结果主频仅能上 50MHz,且大部分时间 ALU、内存处于空闲状态。一条指令走完所有阶段,延迟全压在一条路径上。
五级流水线将指令拆成五个小步,每步只做一点点事。第一条指令仍需 5 个周期完成,但从第 5 个周期开始,每个周期都能送出一条新指令的结果,实现吞吐率的飞跃。
RISC-V 的开放性和简洁性,特别是 RV32I 基础整数集只有几十条指令,控制逻辑清晰,非常适合 FPGA 新手练手。
技术选型:Artix-7 XC7A35T 开发板 + Vivado 2023.1 + 支持 RV32I 的轻量级核心设计
五级流水线架构
让多条指令像工厂流水线上的产品一样,并行推进。
| 阶段 | 干什么 | 关键任务 |
|---|---|---|
| IF(取指) | 取指令 | 给 PC 找地址,从 IMEM 拿指令 |
| ID(译码) | 拆指令 | 解析 opcode,读寄存器,生成控制信号 |
| EX(执行) | 算东西 | ALU 运算,地址计算,判断分支 |
| MEM(访存) | 访问内存 | Load/Store 数据,其他指令透传 |
| WB(写回) | 写结果 | 把数据写回寄存器 |
理想状态下,每一拍都有五条指令分布在不同阶段,CPU 利用率达到极致。但这背后有个大前提:不能出乱子。一旦前面卡住,后面全得等着——这就是所谓的'流水线冒险'。
核心模块拆解
第一关:取指单元(IF)
最简单的 IF 单元就是更新 PC 地址,但现实远没这么简单。
三大挑战:
- 跳转指令来了怎么办? JAL 直接跳,BEQ 条件满足才跳。必须在 EX 阶段判断后反馈给 IF,否则会多取一条错误指令。
- 分支预测怎么做? 初期用'默认不跳'。如果跳了,那就清空 IF/ID 寄存器,重新从目标地址取指。
- IMEM 怎么实现? 用 Xilinx XPM 原语创建双端口 RAM。
xpm_memory_sdpram #( .ADDR_WIDTH_A(10), // 1KB = 256 words .DATA_WIDTH_A(32) ) imem_inst ( .clka(clk), .addra(pc[3:2]), // 字对齐 .douta(inst_out) );
提示:PC 更新必须受控!加入 pc_en 和 pc_src 多路选择器,支持 jump、branch、exception 等多种来源。
第二关:译码单元(ID)
ID 阶段的核心任务就两个字:拆和读。
- 拆:把 32 位指令按格式分解
- 读:根据
rs1和rs2编号,从寄存器文件里拿出数据
寄存器文件实现要点
这是整个 CPU 最容易出错的地方之一。
module regfile ( input clk, input we, // 写使能 input [4:0] waddr, // 写地址 input [31:0] wdata, // 写数据 input [4:0] raddr1, input [4:0] raddr2, output [31:0] rdata1, output [31:0] rdata2 ); reg [31:0] regs [0:31]; // 同步写:只在上升沿更新 always @(posedge clk) begin if (we && waddr != 5'd0) // x0 永远为 0! regs[waddr] <= wdata; end // 异步读:组合逻辑输出 assign rdata1 = (raddr1 == 5'd0) ? 32'd0 : regs[raddr1]; assign rdata2 = (raddr2 == 5'd0) ? 32'd0 : regs[raddr2]; endmodule

