本项目是一个使用 Verilog 编写的 32 位 RISC-V 五级流水线 CPU,实现了取指、译码、执行、访存、写回五个阶段,并配套了仿真 testbench、指令/数据存储器、分支预测模块、冒险停顿控制以及 FPGA 顶层显示逻辑。
当前根目录下的 Verilog 文件是主版本;Pipeline/ 子目录只包含部分同名模块,更像历史版本或备份版本,除非特别需要对比,否则建议以根目录源码为准。
本项目受 VenciFreeman/RISC-V 启发。该仓库是一个使用 Verilog 编写的简单 RISC-V CPU 项目,包含单周期和流水线等实现思路;本项目在其基础思路上结合当前实验目标进行了扩展和调整。
- 五级流水线结构:IF、ID、EX、MEM、WB。
- 支持寄存器堆同周期读写旁路、EX 阶段前递以及 load/use 等数据冒险停顿。
- 支持条件分支、
jal、jalr的跳转处理,并带预测错误恢复 flush 逻辑。 - 带局部/全局方向预测表的分支预测模块
BRANCH_PRE。 - 支持
lw、sw访存,数据存储器按小端序读写 32 位字。 - 支持 RV32I 常用整数指令,并扩展支持 RV32M 的乘除余指令。
- 提供仿真顶层
riscv_soc_tb.v。 - 提供 FPGA 顶层
fpga_top.v、数码管驱动seg_driver.v和管脚约束fpga_pin.xdc。 - 板级演示可将写入
0x10000000的数据输出到 LED 和 8 位数码管。
.
├── riscv.v # CPU 顶层,连接流水线各阶段、寄存器堆、停顿控制和分支预测
├── if.v # PC/取指地址控制
├── if_id.v # IF/ID 流水线寄存器,支持 stall 和 flush
├── id.v # 指令译码、立即数生成、分支判断、冒险检测、flush 生成
├── id_ex.v # ID/EX 流水线寄存器
├── ex.v # ALU、访存地址计算、EX 阶段前递、RV32M 运算
├── ex_mem.v # EX/MEM 流水线寄存器
├── mem.v # 访存阶段,生成数据存储器控制信号
├── mem_wb.v # MEM/WB 流水线寄存器
├── wb.v # 独立写回模块,当前主顶层未直接实例化
├── register.v # 32 个通用寄存器,x0 恒为 0,带 WB 旁路
├── stall.v # 全局流水线停顿控制
├── bp.v # 分支预测器 BRANCH_PRE
├── inst_mem.v # 指令存储器,使用 $readmemh 加载机器码
├── data_mem.v # 仿真数据存储器,支持内存映射输出
├── riscv_soc_tb.v # 仿真 testbench
├── fpga_top.v # FPGA 顶层,连接 CPU、指令存储器、LED 和数码管
├── seg_driver.v # 8 位七段数码管十进制扫描显示驱动
├── fpga_pin.xdc # FPGA 管脚约束
├── test_all.txt # 当前 inst_mem.v 默认加载的综合指令测试机器码
├── test_all_mem.txt # test_all.txt 的带注释说明版本
├── Fibonacci.txt # Fibonacci 板级演示程序说明
├── sum.txt # 平方和示例机器码
├── sum_mem.txt # sum.txt 的带注释说明版本
├── machinecode.txt # Fibonacci 程序机器码
├── data_mem.txt # 示例数据内存初始化内容,当前 data_mem.v 未直接加载
└── Pipeline/ # 部分模块的历史/参考版本
流水线主路径如下:
PC/IF -> IF/ID -> ID -> ID/EX -> EX -> EX/MEM -> MEM -> MEM/WB -> Registers
核心顶层 riscv.v 对外暴露两组存储器接口:
- 指令存储器接口:
inst_addr_o、inst_i、inst_ce_o - 数据存储器接口:
data_ce_o、data_we_o、data_addr_o、data_i、data_o
riscv_soc_tb.v 将 CPU、inst_mem.v 和 data_mem.v 组合成可仿真的 SoC。fpga_top.v 则将 CPU 接到 FPGA 外设,当前板级顶层只实例化了指令存储器,没有实例化通用数据存储器;当 CPU 对 0x10000000 执行 sw 时,写入值会被锁存到 LED 和数码管显示逻辑中。
当前译码逻辑在 id.v 中通过 casex 匹配指令,执行逻辑在 ex.v 中按 ALUop 完成运算。
| 类型 | 指令 |
|---|---|
| U-type | lui, auipc |
| J-type | jal |
| I-type 跳转 | jalr |
| B-type 分支 | beq, bne, blt, bge, bltu, bgeu |
| Load/Store | lw, sw |
| I-type 算术/逻辑 | addi, slti, sltiu, xori, ori, andi, slli, srli, srai |
| R-type 算术/逻辑 | add, sub, sll, slt, sltu, xor, srl, sra, or, and |
支持:
mulmulhdivdivuremremu
ex.v 中的 RV32M 运算当前是组合/单周期风格实现,StallReq_o 固定为 0。综合到 FPGA 时,乘除法资源和时序压力会比普通 ALU 指令更高。
当前代码没有实现以下 RISC-V 功能:
lb/lh/lbu/lhu/sb/sh等非字宽访存。- CSR、异常、中断、特权级。
- 压缩指令 RV32C。
- 原子指令 RV32A。
- 完整总线协议,如 AXI、Wishbone 等。
项目中与冒险相关的主要模块和逻辑如下:
register.v:寄存器堆在 WB 同周期读写同一寄存器时,读口直接返回写入值。ex.v:EX 阶段支持从 MEM/WB 结果向当前执行指令前递。id.v:当 ID 阶段源寄存器依赖 EX 或 MEM 阶段待写寄存器时,产生StallReqLoad。stall.v:根据 load/use、分支或 EX 阶段请求生成 6 位stall控制信号。if_id.v:在flush时向 ID 阶段注入 NOP。PC模块:Flush优先级高于预测跳转和顺序取指。
bp.v 中的 BRANCH_PRE 提供分支预测:
- 使用全局历史寄存器
GlobalRec。 - 使用全局预测表
GlobalPre。 - 使用局部地址记录/预测表
LocalAddrRec、LocalPre。 - 使用
AllPre在局部/全局预测之间选择。
id.v 在分支实际结果与预测结果不一致时产生 Flush 和 CorrectPC,PC 模块据此恢复正确取指地址。jal 和 jalr 当前也会触发 flush,并将 PC 修正到跳转目标。
inst_mem.v 定义了 1024 个 32 位字的指令内存,总容量 4 KiB。CPU 输出的是字节地址,指令存储器使用 addr[31:2] 转为字地址。
当前代码中机器码加载路径是硬编码绝对路径:
$readmemh("E:/RISC-V/Pipeline/test_all.txt", inst_memory);如果在本项目目录直接运行仿真,建议把它改成当前目录下的相对路径:
$readmemh("test_all.txt", inst_memory);也可以按需要替换为:
sum.txtmachinecode.txt- 其他自行生成的 32 位十六进制机器码文件
data_mem.v 定义了 1025 字节的数据存储器:
lw:从addr起按小端序读取 4 字节。sw:向addr起按小端序写入 4 字节。verify:输出data[15:12]组成的 32 位值,便于仿真观察。
特殊地址:
0x10000000
在仿真数据存储器中,写入该地址不会写 RAM,而是通过 $display 打印:
Fibonacci Output: <decimal> (0x<hex>)
在 fpga_top.v 中,写入该地址会更新 LED 和数码管显示数据。
推荐先确认 inst_mem.v 中的 $readmemh 路径能被仿真器访问。若使用当前目录中的测试程序,请将路径改为相对路径。
如果安装了 Icarus Verilog,可以在项目根目录运行:
iverilog -g2012 -o riscv_soc_tb.vvp `
riscv_soc_tb.v riscv.v if.v if_id.v id.v id_ex.v ex.v ex_mem.v mem.v mem_wb.v `
register.v stall.v bp.v inst_mem.v data_mem.v
vvp riscv_soc_tb.vvp仿真中可以观察:
- 寄存器写回打印:
register.v中的$display("x%d = %h", ...) - Fibonacci 内存映射输出:
data_mem.v中写0x10000000时的$display riscv_soc_tb.v中的内部信号,例如inst_addr、inst、data_addr、wdata、rdata、verify
- 新建 Vivado 工程。
- 添加根目录下所有 Verilog 源码。
- 将
riscv_soc_tb.v设置为仿真顶层。 - 确认
inst_mem.v中$readmemh指向存在的机器码文件。 - 运行 Behavioral Simulation。
上板相关文件:
- 顶层模块:
fpga_top.v - 约束文件:
fpga_pin.xdc - 显示驱动:
seg_driver.v - CPU 和流水线相关全部根目录 Verilog 文件
fpga_top.v 的顶层端口:
| 端口 | 方向 | 说明 |
|---|---|---|
sys_clk |
input | FPGA 系统时钟,约束到 P17 |
sys_rst |
input | 复位按键/输入,约束到 P15,当前顶层未接入 CPU 复位 |
sw_polarity |
input | 拨码输入,已约束但当前逻辑未使用 |
led[7:0] |
output | LED 输出 |
seg_sel[7:0] |
output | 数码管位选 |
seg_led_0[7:0] |
output | 第一组段选 |
seg_led_1[7:0] |
output | 第二组段选 |
板级逻辑说明:
sys_clk通过简单计数器分频生成cpu_clk。- CPU 复位在当前代码中固定接
1'b0。 - 当 CPU 写
0x10000000时,wdata[6:0]显示到led[6:0],完整wdata显示到数码管。 led[7]每个 CPU 时钟翻转一次,可作为运行心跳。TEST_MODE设为 1 时,数码管固定显示12345678;默认 0 时显示 CPU 写出的数据。
上板建议:
- 将
inst_mem.v中$readmemh指向上板要运行的机器码。 - 若使用 Fibonacci 演示,可加载
machinecode.txt,并参考Fibonacci.txt。 - 在 Vivado 中将
fpga_top设置为综合/实现顶层。 - 添加
fpga_pin.xdc。 - 综合、实现、生成 bitstream 并下载。
当前 inst_mem.v 默认加载的综合测试程序。覆盖内容包括:
- 立即数与逻辑运算:
addi,andi,ori - 比较:
slt,sltu - 移位:
sll,srl,sra - 乘除余:
div,rem - 条件分支:
beq,bne,bge - PC 相对与间接跳转:
auipc,jalr - 程序末尾使用
jal x0, 0自循环结束
test_all_mem.txt 是对应的带地址和注释版本,便于核对预期结果。
平方和与访存示例程序,包含循环、mul、sw、lw、blt 等操作。sum_mem.txt 是带注释版本。
Fibonacci 演示程序。程序会通过 lui x3, 0x10000 构造 0x10000000,再把 Fibonacci 数写到该地址:
sw x2, 0(x3)在仿真中会打印 Fibonacci 输出;在 FPGA 顶层中会更新 LED 和数码管显示。
inst_mem.v当前使用绝对路径E:/RISC-V/Pipeline/test_all.txt。如果路径不存在,仿真器会无法加载指令或指令内存保持为 0。data_mem.txt当前没有被data_mem.v自动$readmemh加载,如需初始化数据内存,需要自行添加读取逻辑。fpga_top.v当前没有实例化data_mem.v,只处理写0x10000000的外设显示场景。sys_rst在fpga_top.v中已经声明和约束,但当前 CPU 复位固定为1'b0,如需上板复位功能,需要把sys_rst接入 CPU 和相关模块。- RV32M 乘除余使用组合逻辑实现,上板时建议关注综合资源、时序报告和最大频率。
wb.v在当前riscv.v主顶层中未实例化,写回直接由MEM_WB输出驱动寄存器堆。Pipeline/子目录存在部分同名模块,加入 Vivado 工程时不要同时添加根目录和子目录里的同名模块,否则可能出现模块重复定义或版本混用。
- 新增指令时,通常需要同步修改
id.v的译码、ex.v的执行逻辑,必要时还要修改mem.v和冒险处理逻辑。 - 修改测试程序后,保持机器码文件一行一条 32 位十六进制指令。
- 对分支、跳转、load/use、前递和乘除法指令分别建立小测试程序,便于定位流水线问题。
- 上板前先通过仿真确认寄存器写回、访存地址和分支目标符合预期。