跳到主要内容
Xilinx FPGA 实现 RISC-V 五级流水线 CPU 设计与实战 | 极客日志
汇编 算法
Xilinx FPGA 实现 RISC-V 五级流水线 CPU 设计与实战 RISC-V 五级流水线 CPU 基于 Xilinx FPGA 的 Verilog 实现方案。文章详细阐述了取指、译码、执行、访存、写回五大阶段的设计原理,重点解决结构冲突、数据冒险与控制冒险问题。通过前递单元与暂停逻辑优化性能,结合 Vivado 工具链完成综合约束与 BRAM 内存映射。实测案例验证了数组求和程序的运行正确性,并提供了后续扩展方向如缓存、外设接入及压缩指令支持。适合希望深入理解计算机组成原理与硬件设计的开发者参考。
忘忧 发布于 2026/3/28 更新于 2026/9/10 68 浏览从一块 FPGA 开始,亲手造一颗 CPU:RISC-V 五级流水线实战全记录
设计处理器是数字世界的核心挑战。本文介绍如何在 Xilinx FPGA 上,用 Verilog 从零实现一个完整的 RISC-V 五级流水线 CPU。包含取指、译码、执行、访存、写回五大阶段,并解决真实数据冒险与控制冒险的可运行核心。
为什么选择 RISC-V + FPGA?
开放免费 :没有授权费,文档齐全,寄存器编码明确。
简洁清晰 :RV32I 只有 40 多条指令,无历史包袱。
模块化扩展 :基础整数指令够用,后续可加浮点、压缩指令、向量扩展。
FPGA 允许灵活修改逻辑,重新综合后快速烧录验证。ILA(集成逻辑分析仪)可以抓取内部任意信号波形,提供芯片级调试的透明度。
五级流水线:让 CPU'并行'起来的秘密
现代 CPU 靠让多条指令同时处于不同阶段来提高吞吐率。
拆解五大阶段
阶段 对应操作 类比 IF(Instruction Fetch) 根据 PC 读取指令 店员去仓库拿食谱 ID(Instruction Decode) 解码指令,读取寄存器值 看懂食谱,准备好原料 EX(Execute) ALU 运算或地址计算 开始揉面、发酵 MEM(Memory Access) 访问内存(load/store) 把成品放进/拿出烤箱 WB(Write Back) 写结果回寄存器 把做好的面包摆上货架
理想情况下,每个时钟周期都有一个新任务进入流程。关键指标:CPI ≈ 1,加速比 ≈ 5 倍于非流水线设计。
但现实会遇到三大障碍:结构冲突、数据冒险、控制冒险。
如何在 Xilinx FPGA 上搭建这个 CPU?
目标:使用 Digilent Nexys A7 开发板(XC7A35T),基于 Vivado 2023.1 工具链,构建完整 SoC。
系统架构总览
整个系统采用哈佛架构(分离指令与数据存储),关键模块如下:
+
| Clock (50 MHz) |
+
|
+
| | |
+
PC Reg Inst ROM (BRAM)
IF Stage
( instruction PC)
ID
Control Unit ← Opcode Decoder
RegFile Read ← rs1, rs2
EX
ALU Control ← funct3 funct7
ALU Operation ( sub slt etc.)
MEM
Data Memory (Block RAM)
Handle lw sw
WB
Write Mux ← ALU MEM data
RegFile Write Enable
|
|
|
|
+
|
|
+
|
|
|
fetch
&
update
|
+
|
+
|
|
|
|
|
|
+
|
+
|
|
|
/
|
|
add
/
/
and
/
or
/
/
|
+
|
+
|
|
|
|
|
/
|
+
|
+
|
|
|
out
/
|
|
|
+
Forwarding Unit(前递单元) :解决数据依赖
Stall Logic(暂停逻辑) :应对 load-use 延迟
所有模块通过跨级寄存器连接,形成稳定的流水推进机制。
实战第一步:搭建基础流水线框架
第一步:先让单条指令跑通 先把 IF→ID→EX→MEM→WB 串起来,不考虑并发,只验证一条 add x1, x2, x3 能不能正确执行。
PC 是否自增 4
指令是否正确解析出 rs1/rs2/rd
ALU 是否输出 x2+x3 的结果
最终是否写入 x1
此时可以用最简单的 testbench 模拟几个时钟周期,打印寄存器文件变化。
// 示例:最简 WB 阶段
always_ff @(posedge clk) begin
if (wb_we && rd != 0) begin
regfile[rd] <= wb_data; // 写回结果
end
end
第二步:引入流水线触发器 一旦单指令验证无误,就开始加入各阶段间的缓冲寄存器:
// IF/ID 寄存器组
always_ff @(posedge clk) begin
if (!stall) begin
if_id_pc <= pc;
if_id_inst <= inst_rom[pc>>2];
end
end
注意这里的 !stall 条件——这是未来插入气泡的基础。
此时会发现一个问题:跳转指令会让后面的预取指令失效。比如:
beq x1, x2, label
add x3, x4, x5 # 这条不该被执行!
简单粗暴的办法:一旦发现 branch 且条件成立,立刻清空 IF/ID 流水线(即插入两个 NOP),并更新 PC。
第三步:加入前递与暂停机制
数据冒险怎么破? lw x1, 0(x2) # MEM 阶段才拿到数据
add x3, x1, x4 # 下一条就要用 x1 → RAW 危险!
ALU 在 EX 阶段需要 x1,但 x1 还在 MEM 阶段的路上,来不及写回。有两个选择:
暂停一拍(Insert Bubble)
前递(Forwarding)
对于 lw 之后紧接使用的情况,必须暂停;而对于普通算术指令之间的依赖,可以通过前递解决。
前递单元设计要点 我们需要监控 MEM 和 WB 阶段的输出,看看有没有正在返回的'热数据'可以提前借用:
// Forwarding Unit 核心逻辑
always_comb begin
forward_a = 2'b00;
forward_b = 2'b00;
// EX/MEM 阶段有写操作,且目标寄存器匹配源操作数
if (ex_mem_we && ex_mem_rd != 0 && ex_mem_rd == id_ex_rs1)
forward_a = 2'b01;
// 来自 MEM 阶段 ALU 输出
else if (mem_wb_we && mem_wb_rd != 0 && mem_wb_rd == id_ex_rs1)
forward_a = 2'b10;
// 来自 WB 阶段写回数据
// 同理处理第二个源操作数
if (ex_mem_we && ex_mem_rd != 0 && ex_mem_rd == id_ex_rs2)
forward_b = 2'b01;
else if (mem_wb_we && mem_wb_rd != 0 && mem_wb_rd == id_ex_rs2)
forward_b = 2'b10;
end
然后在 EX 阶段之前,用 MUX 选择实际输入:
assign src1 = (forward_a == 2'b01) ? ex_mem_alu_out : (forward_a == 2'b10) ? mem_wb_wdata : id_ex_src1;
assign src2 = (forward_b == 2'b01) ? ex_mem_alu_out : (forward_b == 2'b10) ? mem_wb_wdata : id_ex_src2;
这样一来,大多数数据冒险都被化解了,只有 load-use 需要额外处理。
处理 load-use 冒险 当检测到当前是 lw 指令,且下一条要用其结果时,必须插入一个 气泡(bubble) ,也就是让 ID/EX 流水线停顿一拍。
assign stall = (id_ex_mem_read && (id_ex_rd == if_id_rs1 || id_ex_rd == if_id_rs2));
在 Vivado 中落地:那些容易踩的坑 你以为写完代码就完了?不,FPGA 综合才是真正的考验。
必须设置的 SDC 约束 哪怕只是一个小型 CPU,也必须告诉 Vivado 你的时钟频率:
create_clock -period 10.000 [get_ports clk]
set_input_delay -clock clk 1.0 [all_inputs]
set_output_delay -clock clk 1.0 [all_outputs]
否则工具可能优化掉关键路径,导致实际运行主频远低于预期。
使用 BRAM 模拟内存 推荐将指令存储和数据存储分别映射到两个独立的 Block RAM 中,初始化为 .bin 文件。
技巧:用 Python 脚本把 RISC-V 汇编编译成机器码,生成 coe 文件直接导入 BRAM。
with open ("program.s" ) as f:
binary = riscv_assembler(f.read())
with open ("inst.coe" , "w" ) as f:
f.write("memory_initialization_radix=16;\n" )
for word in binary:
f.write(f"{word:08x} \n" )
调试利器:ILA 集成逻辑分析仪 千万别等到板级测试才发现问题。提前插入 ILA 核,抓取以下关键信号:
pc, instruction
regfile[32](建议只抓部分常用寄存器)
alu_result, mem_data_out
forward_a, forward_b, stall
你可以亲眼看到一条 beq 指令如何触发流水线刷新,或者一次 lw 如何引发一拍停顿。
它真的能跑吗?实测案例 我写了一段简单的 RISC-V 汇编程序,功能是计算数组求和:
.global _start
_start:
li x5, 10 # counter
li x6, 0 # sum
la x7, arr # base address
loop:
lw x8, 0(x7) # load element
add x6, x6, x8 # accumulate
addi x7, x7, 4 # next address
addi x5, x5, -1 # decrement counter
bne x5, zero, loop
trap:
j trap # infinite loop
x6 最终值为 0x1E(即 30),符合预期
循环共执行 10 次
每次 lw 后确实插入了一拍停顿
分支跳转时 IF/ID 被清空
这意味着,我们的 CPU 不仅能跑,还能正确处理复杂的控制流与数据依赖。
这只是起点:下一步往哪走? 你现在手里握着的,不仅仅是一个能跑通的 CPU 模型,而是一个可无限演进的计算平台原型。
✅ 加乘除法单元(MDU)
实现 mul, div 指令
可采用组合逻辑(快但占资源)或多周期迭代(慢但省面积)
✅ 添加一级缓存(Cache)
在指令侧加入 I-Cache,减少 ROM 访问延迟
数据侧加 D-Cache,提升连续访存效率
✅ 接入外设构成微型 SoC
UART:实现串口打印调试信息
GPIO:点亮 LED、读取按键
Timer:支持时间片调度
✅ 支持 RISC-V 压缩指令(RVC)
将 32 位指令压缩为 16 位,节省代码空间达 30%
需要在 IF 阶段增加解压逻辑
✅ 引入分支目标缓冲(BTB)
记录历史跳转地址,避免每次都冲刷流水线
显著提升含有大量循环/函数调用的程序性能
甚至有一天,你可以试着让它启动一个极简的操作系统内核——当然,前提是加上 MMU 支持虚拟内存。
写在最后:每一个比特都在诉说原理 当我第一次看到 x6 寄存器里的数值变成 30 时,心里有种说不出的震撼。这不是某个开源项目跑起来的结果,而是我自己搭建的电路,在真实硅片上一步一步执行出来的答案。
为什么要有流水线?
为什么需要前递?
为什么分支预测如此重要?
这些曾经停留在课本上的概念,现在变成了我代码里的每一个 always_ff 块、每一个 MUX 选择、每一次时序修复。
也许你会说:'现在都有成熟的 Rocket Chip 了,干嘛还要自己造轮子?'
但我想说:只有亲手造过轮子的人,才知道车是怎么跑起来的。
而今天你在 FPGA 上敲下的每一行 Verilog,都是通往芯片自由之路的一小步。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online