基于Vivado的RISC-V五级流水线CPU FPGA实现详解
为什么选 RISC-V + 五级流水?
性能和资源的平衡是核心考量。
单周期 CPU 虽然代码简单,但综合结果主频较低,大部分时间 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 到 WB,逐级打通
第一关:取指单元(IF)
最简单的 IF 单元就是更新程序计数器(PC)。
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pc <= 32'h0;
else
pc <= pc + 4;
end
现实远没这么简单,主要面临三大挑战:
- 跳转指令来了怎么办? 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

