跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
汇编算法

Xilinx FPGA 实现 RISC-V 五级流水线 CPU 设计实战

综述由AI生成RISC-V 五级流水线 CPU 在 Xilinx FPGA 上的设计与实现。文章涵盖流水线五大阶段(IF/ID/EX/MEM/WB)原理,解析结构冲突、数据冒险与控制冒险的处理方案,包括前递单元(Forwarding)与暂停逻辑(Stall)。提供 Vivado 环境下的 SDC 约束设置、BRAM 内存映射及 ILA 调试方法。通过汇编程序测试验证了指令执行的正确性与流水线效率。

SqlMaster发布于 2026/4/9更新于 2026/4/256 浏览

从一块 FPGA 开始,亲手造一颗 CPU:RISC-V 五级流水线实战全记录

为什么是 RISC-V + FPGA?

  • 开放免费:没有授权费,文档齐全,连寄存器编码都写得明明白白。
  • 简洁清晰:RV32I 只有 40 多条指令,没有 x86 那样层层嵌套的历史包袱。
  • 模块化扩展:基础整数指令够用,后续想加浮点、压缩指令、向量扩展,都可以一步步来。

FPGA 可以把它捏成任何你想要的电路形态。不像 ASIC 动辄百万成本,也不像 MCU 被固化功能束缚。改个逻辑,重新综合,几分钟后就能烧录验证。

更重要的是,你能看到一切。ILA(集成逻辑分析仪)可以抓取内部任意信号波形,就像给 CPU 做 CT 扫描。这种透明度,在真实芯片里根本不可能实现。

当你把 RISC-V 和 FPGA 放在一起,得到的不只是一个软核,而是一个可观察、可调试、可演进的计算系统沙盒。

五级流水线:让 CPU'并行'起来的秘密

现代 CPU 之所以快,靠的不是单条指令跑得飞快,而是让多条指令同时处于不同阶段,像工厂流水线一样源源不断地出货。

拆解五大阶段
阶段对应操作类比
IF(Instruction Fetch)根据 PC 读取指令店员去仓库拿食谱
ID(Instruction Decode)解码指令,读取寄存器值看懂食谱,准备好原料
EX(Execute)ALU 运算或地址计算开始揉面、发酵
MEM(Memory Access)访问内存(load/store)把成品放进/拿出烤箱
WB(Write Back)写结果回寄存器把做好的面包摆上货架

理想情况下,每个时钟周期都有一个新任务进入流程,也有一个成品离开。虽然单条指令仍需 5 拍才能完成,但吞吐率接近每周期一条指令。

关键指标:

  • CPI(Clocks Per Instruction)≈ 1(理想状态)
  • 加速比 ≈ 5 倍于非流水线设计

但现实没那么美好。流水线会遇到三大障碍:结构冲突、数据冒险、控制冒险。处理不好,性能反而不如单周期。

如何在 Xilinx FPGA 上搭建这个 CPU?

目标明确:使用 Digilent Nexys A7 开发板(XC7A35T),基于 Vivado 2023.1 工具链,构建一个能运行 RISC-V 汇编程序的完整 SoC。

系统架构总览

整个系统采用哈佛架构(分离指令与数据存储),关键模块如下:

+------------------+
| Clock (50MHz)    |
+--------+---------+
         |
+------------------+------------------+
|                 |                   |
+-------v--------+ +---------v---------+
| PC Reg         | | Inst ROM (BRAM)   |
+-------+--------+ +---------+---------+
        |                  |
+-------v-----------------------------------+
| IF Stage                                 |
| (fetch instruction & update PC)          |
+-------------------+-----------------------+
                    |
+-------------------v-----------------------+
| ID                                         |
| Control Unit ← Opcode Decoder              |
| RegFile Read ← rs1, rs2                    |
+-------------------+-----------------------+
                    |
+-------------------v-----------------------+
| EX                                         |
| ALU Control ← funct3/funct7                |
| ALU Operation (add/sub/and/or/slt/etc.)    |
+-------------------+-----------------------+
                    |
+-------------------v-----------------------+
| MEM                                        |
| Data Memory (Block RAM)                    |
| Handle lw/sw                               |
+-------------------+-----------------------+
                    |
+-------------------v-----------------------+
| WB                                         |
| Write Mux ← ALU out / MEM data             |
| RegFile Write Enable                       |
+-----------------------------------------+

此外还有两个幕后英雄:

  • Forwarding Unit(前递单元):解决数据依赖
  • Stall Logic(暂停逻辑):应对 load-use 延迟

所有模块通过跨级寄存器(如 if_id_inst, id_ex_pc)连接,形成稳定的流水推进机制。

实战第一步:搭建基础流水线框架

建议采取三步走策略。

第一步:先让单条指令跑通

先把 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 阶段的路上,来不及写回。有两个选择:

  1. 暂停一拍(Insert Bubble)
  2. 前递(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));

同时,在控制逻辑中阻止 PC 更新和流水线推进。

这样虽然牺牲了一个周期,但保证了正确性。

在 Vivado 中落地:那些容易踩的坑

必须设置的 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。

# 伪代码:asm → machine code → coe
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

烧录进 FPGA 后,通过 ILA 观测到:

  • x6 最终值为 0x1E(即 30),符合预期
  • 循环共执行 10 次
  • 每次 lw 后确实插入了一拍停顿
  • 分支跳转时 IF/ID 被清空

这意味着,我们的 CPU 不仅能跑,还能正确处理复杂的控制流与数据依赖。

这只是起点:下一步往哪走?

接下来可以尝试的方向:

✅ 加乘除法单元(MDU)
  • 实现 mul, div 指令
  • 可采用组合逻辑(快但占资源)或多周期迭代(慢但省面积)
✅ 添加一级缓存(Cache)
  • 在指令侧加入 I-Cache,减少 ROM 访问延迟
  • 数据侧加 D-Cache,提升连续访存效率
✅ 接入外设构成微型 SoC
  • UART:实现串口打印调试信息
  • GPIO:点亮 LED、读取按键
  • Timer:支持时间片调度
✅ 支持 RISC-V 压缩指令(RVC)
  • 将 32 位指令压缩为 16 位,节省代码空间达 30%
  • 需要在 IF 阶段增加解压逻辑
✅ 引入分支目标缓冲(BTB)
  • 记录历史跳转地址,避免每次都冲刷流水线
  • 显著提升含有大量循环/函数调用的程序性能

甚至有一天,你可以试着让它启动一个极简的操作系统内核——当然,前提是加上 MMU 支持虚拟内存。

目录

  1. 从一块 FPGA 开始,亲手造一颗 CPU:RISC-V 五级流水线实战全记录
  2. 为什么是 RISC-V + FPGA?
  3. 五级流水线:让 CPU“并行”起来的秘密
  4. 拆解五大阶段
  5. 如何在 Xilinx FPGA 上搭建这个 CPU?
  6. 系统架构总览
  7. 实战第一步:搭建基础流水线框架
  8. 第一步:先让单条指令跑通
  9. 第二步:引入流水线触发器
  10. 第三步:加入前递与暂停机制
  11. 数据冒险怎么破?
  12. 前递单元设计要点
  13. 处理 load-use 冒险
  14. 在 Vivado 中落地:那些容易踩的坑
  15. 必须设置的 SDC 约束
  16. 使用 BRAM 模拟内存
  17. 伪代码:asm → machine code → coe
  18. 调试利器:ILA 集成逻辑分析仪
  19. 它真的能跑吗?实测案例
  20. 这只是起点:下一步往哪走?
  21. ✅ 加乘除法单元(MDU)
  22. ✅ 添加一级缓存(Cache)
  23. ✅ 接入外设构成微型 SoC
  24. ✅ 支持 RISC-V 压缩指令(RVC)
  25. ✅ 引入分支目标缓冲(BTB)
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • GitHub 仓库从零开始:上传、更新与维护指南
  • 2026最火的6款免费AI写作软件测评:ai写网文哪个好用?这款ai消痕工具
  • LLM 存储优化实战:解决大量 QA 与长对话问题
  • Spring Boot 中 MultipartFile 转 File 对象的四种实现方案
  • AIGC 中的变分自编码器(VAE)代码与实现
  • 基于 Java 大数据的智能家居能耗预测与节能策略优化实战
  • 从三年前端到 CS 硕士:我在韩国亚大的留学复盘与回归
  • Mac Mini M4 本地 AI 模型实战:从 Ollama 到 Stable Diffusion 配置指南
  • 医疗大模型商业化进程:挑战、方案与落地路径
  • VSCode 中配置与使用 Copilot MCP 快速上手指南
  • JavaScript window.location 对象详解
  • 数字频率计 FPGA 实现中的测频方法比较
  • Spring MVC Web 开发实战:加法计算器、登录与留言板
  • 力扣 Hot 100 链表算法题 Python 实现
  • 非科班转码者 AI 学习路径:从 0 到 1
  • 算法题解:三数之和与四数之和
  • Flutter eip55 库在 OpenHarmony 上的适配与以太坊地址校验
  • GGCNN 机器人抓取检测技术详解
  • ARINC 825:一种航电通信总线标准
  • C++ Core Guidelines 解析:让接口易于使用

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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