简介:本资源是浙江大学计算机体系结构课程配套的MIPS流水线CPU硬件实现项目,面向高校计算机/电子类专业本科生及数字电路设计初学者,解决从理论指令集架构到可综合Verilog代码落地的关键实践问题。压缩包共56个文件,含24个核心Verilog模块(如IfStage.v、ExStage.v、Cpu.v等)、5个COE内存初始化文件、5个XISE工程配置文件、4个PNG/JPG原理图与调试界面截图,以及Python自动生成脚本、ASM汇编测试用例和详细README文档,整体大小仅685KB,轻量但结构完整。已有230人学习下载,项目采用清晰长命名规范、模块化分阶段设计(取指/译码/执行/访存/写回)及推荐Verilog编码实践,所有逻辑集中于*Stage.v系列文件,Registers.v与Cpu.v由脚本生成,辅以UCF约束与Nexys3开发板适配说明,便于移植与深度理解流水线控制逻辑、Stall处理与Forwarding机制。
1. 这不是教学演示,而是一颗跑在 Nexys3 上的、带 VGA 输出的完整 MIPS 流水线 CPU
你手头那块积灰的 Nexys3 开发板,真能跑起一个有指令解码、寄存器读写、ALU 运算、数据内存访问、分支预测(Stall/Forwarding)、甚至还能把执行过程实时渲染到 VGA 屏幕上的 CPU 吗?浙江大学计算机体系结构课的这份实验包给出了明确答案:能,而且是可综合、可调试、可验证的工业级 Verilog 实现。它不依赖任何黑盒 IP(除基础 PLL 和 Block RAM 初始化文件),所有流水线阶段(IF/ID/EX/MEM/WB)全部手写模块,31 条 MIPS I 类指令覆盖算术、逻辑、移位、跳转、加载/存储全谱系;更关键的是,它把传统“烧进 FPGA 就完事”的实验,升级为可观测系统——通过Debugger.v和Terminal.v模块,CPU 当前 PC、各寄存器值、指令译码结果、ALU 输入输出、MEM 阶段数据通路状态,全部以 ASCII 字符形式实时输出到 VGA 显示器。这不是玩具模型,而是用命名即文档(如IdStage_InstructionRegister_Output)、模块职责隔离(*Stage.v处理逻辑,*Registers.v仅做寄存器堆建模)、Verilog-2001 规范(显式端口声明、always @(posedge clk)标准触发)构建的工程化实践。适合刚学完《计算机组成原理》想动手验证流水线冲突、正在准备 PAT 或考研复试需要硬件项目背书、或想从零理解 RISC CPU 如何落地为真实数字电路的工程师。
2. 从顶层模块 Cpu.v 到五级流水线 Stage.v:结构拆解与信号流闭环
2.1 Cpu.v:顶层设计与时钟域划分
Cpu.v是整个系统的入口,它不实现具体运算逻辑,而是完成三类关键连接:
- 时钟与复位管理:接收外部 100MHz 时钟
clk_100mhz,经Pc.v中的计数器分频生成clk_25mhz供给 VGA 控制器,同时通过Anti_jitter.v消抖后驱动 CPU 主时钟cpu_clk; - 模块实例化与信号粘连:按 IF→ID→EX→MEM→WB 顺序例化
IfStage.v、IdStage.v等,并用IfIdRegisters.v、IdExRegisters.v等寄存器模块连接相邻阶段; - 外设桥接:将
VgaController.v的像素数据总线vga_r/g/b与Terminal.v的字符缓冲区char_buffer关联,使 CPU 内部状态可映射为屏幕坐标。
提示:
Cpu.v中未使用initial块或#delay,所有行为均基于同步时序,符合 FPGA 综合要求。若需移植到其他开发板,只需修改nexys3.ucf中的引脚约束,Cpu.v本身无需改动。
2.2 五级流水线 Stage.v 模块分工与关键信号定义
每个*Stage.v模块遵循统一接口规范:输入为上一级寄存器输出(如IfId_*),输出为本级计算结果(如IdEx_*)。以IdStage.v为例,其核心职责是指令译码与寄存器堆读取,关键信号如下:
// IdStage.v 片段(已简化) module IdStage ( input wire clk, input wire rst_n, // 从 IF 阶段接收的指令和 PC input wire [31:0] IfId_Instruction, input wire [31:0] IfId_PcPlus4, // 输出到 EX 阶段的译码结果 output reg [31:0] IdEx_ReadData1, output reg [31:0] IdEx_ReadData2, output reg [4:0] IdEx_WriteRegister, output reg [31:0] IdEx_Immediate, output reg [2:0] IdEx_AluOp, output reg IdEx_RegWrite, output reg IdEx_MemRead, output reg IdEx_MemWrite, output reg IdEx_Branch, output reg IdEx_Jump );IfId_Instruction是从IfIdRegisters.v输出的 32 位原始指令,IdStage需解析其opcode(6bit)、rs/rt/rd(5bit)、shamt(5bit)、funct(6bit)等字段;IdEx_ReadData1/2来自RegisterFile.v的同步读端口,IdEx_Immediate是符号扩展后的立即数(sign_extend模块处理);IdEx_AluOp是三位控制码,决定 EX 阶段 ALU 执行 ADD/SUB/AND/OR/XOR/SLT 等操作,其值由ControlUnit.v根据opcode和funct查表生成。
2.3 寄存器文件 RegisterFile.v 与前递(Forwarding)实现
RegisterFile.v是双端口同步 RAM,支持同一周期内读两个寄存器(rs,rt)并写一个(rd)。其关键设计在于解决 RAW 冲突:当某条指令I1在 MEM 阶段写入rd,而下一条I2在 ID 阶段需读取同一rd时,I2不能等待I1完成 WB,必须从 MEM 或 EX 阶段的输出中“截获”最新值。项目通过ExMemRegisters.v和MemWbRegisters.v的输出信号,在IdStage.v内部实现前递逻辑:
// IdStage.v 中前递判断逻辑(伪代码) if (IdEx_RegWrite && (IdEx_WriteRegister == IfId_rs)) begin IdEx_ReadData1 = ExMemRegisters_WbData; // 从 MEM 阶段取值 end else if (IdEx_RegWrite && (IdEx_WriteRegister == IfId_rt)) begin IdEx_ReadData2 = ExMemRegisters_WbData; end else begin // 正常从 RegisterFile 读取 IdEx_ReadData1 = RegisterFile_Out1; IdEx_ReadData2 = RegisterFile_Out2; end注意:前递路径仅覆盖 MEM→ID 和 EX→ID 两种情况(因 WB 阶段输出已稳定,无需再前递),且
IdEx_WriteRegister必须非零($zero不参与写回),该逻辑在IdStage.v中硬编码实现,避免额外多路选择器延迟。
2.4 控制单元 ControlUnit.v:指令集完备性的决策中枢
ControlUnit.v是纯组合逻辑模块,输入为 6-bitopcode,输出为RegWrite、MemRead、MemWrite、Branch、Jump、AluSrc、AluOp等 9 个控制信号。其本质是一个 64 项 ROM 查表器,对应 MIPS I 全部 64 种 opcode 编码。项目实现了其中 31 条常用指令,包括:
- R 型指令:
add,sub,and,or,xor,slt,sll,srl(funct字段决定具体操作); - I 型指令:
addi,andi,ori,xori,lw,sw,beq,bne,lui; - J 型指令:
j,jal。
查表逻辑用 case 语句实现,例如lw指令(opcode=100011)的输出为:
100011: begin RegWrite = 1'b1; // 写回目标寄存器 MemRead = 1'b1; // 从内存读取 MemWrite = 1'b0; Branch = 1'b0; Jump = 1'b0; AluSrc = 1'b1; // ALU 第二输入来自立即数 AluOp = 3'b000; // ALU 执行 ADD(基地址+偏移) end3. VGA 实时调试系统:Terminal.v 与 Debugger.v 的协同机制
3.1 Terminal.v:字符缓冲区与 VGA 像素映射
Terminal.v是 VGA 显示的核心中介,它维护一个 60×80 字符的缓冲区char_buffer[4799:0](60 行 × 80 列),每个字节存储 ASCII 码。其工作流程分三步:
- CPU 状态写入:
Cpu.v将Pc.v的当前 PC 值、RegisterFile.v的 32 个寄存器内容、Disassembler.v的反汇编指令字符串,通过write_addr和write_data接口写入缓冲区指定位置(如 PC 显示在第 0 行第 0 列); - VGA 时序生成:内部集成
VgaController.v的时序逻辑,产生hsync、vsync、blank信号,并根据当前扫描位置row/col计算缓冲区索引addr = row * 80 + col; - 字符→像素转换:查
Font.xco(Xilinx Core Generator 生成的 8×16 点阵字体 ROM),将 ASCII 码映射为 16 行 × 8 列的像素位图,最终输出vga_r/g/b三原色信号。
提示:
Font.xco文件定义了标准 ASCII 字符集(0x20–0x7E),若需显示中文需替换为 GB2312 字体 ROM 并修改Terminal.v的地址映射逻辑。
3.2 Debugger.v:指令级单步与断点触发
Debugger.v提供两种调试模式:
- 自动刷新模式:每 10ms 从
Cpu.v采集一次PC、IR(指令寄存器)、Reg[0:31]、ALU_Out、MEM_DataOut等信号,格式化为固定宽度文本(如PC: 0x00000000 | IR: 0x00000000 | R1: 0x00000000)写入Terminal.v缓冲区; - 断点模式:用户通过
debugger-input.txt预设断点地址(如0x00000010),Debugger.v在每个时钟上升沿比对PC值,命中时拉高debug_halt信号,冻结Pc.v的计数器,使 CPU 停在断点处。此时可通过串口(需外接 UART 模块)发送命令继续运行或查看寄存器。
3.3 调试信息生成链:从汇编到屏幕的全流程
项目提供generate-debugger.py脚本,将InstructionMemory.asm中的汇编指令自动转换为InstructionMemory.coe(COE 文件是 Xilinx Block RAM 初始化格式),并生成disassembly.txt反汇编列表。Disassembler.v模块在运行时实时解析IR,调用HexCharacterConverter.v(将 4-bit 十六进制数转 ASCII)和BooleanTextConverter.v(将 1-bit 信号转 '0'/'1' 字符),最终拼接成人类可读的指令字符串(如add $t0,$s0,$s1)。该字符串被Debugger.v采集后,经Terminal.v渲染到 VGA 屏幕第 2 行。
| 调试信号源 | Verilog 模块 | 输出格式 | VGA 显示位置 |
|---|---|---|---|
| 当前 PC | Pc.v | 32-bit hex (0x00000000) | 第 0 行,列 0–9 |
| 指令寄存器 IR | IfStage.v | 32-bit hex +Disassembler.v反汇编 | 第 1 行,列 0–29 |
| 通用寄存器 R0–R31 | RegisterFile.v | 32-bit hex(每行 4 个寄存器) | 第 3–11 行 |
| ALU 运算结果 | Alu.v | 32-bit hex | 第 12 行,列 0–9 |
4. 实验复现:从 Vivado 工程构建到 Nexys3 烧录的完整步骤
4.1 Vivado 工程创建与约束文件配置
使用 Vivado 2018.3(兼容 Nexys3 的最低版本)创建新工程:
- 选择
RTL Project→Do not specify sources at this time; - 在
Default Part中选择xc7a35ticsg324-1L(Nexys3 的 Artix-7 FPGA 型号); - 添加所有
.v文件(Cpu.v,IfStage.v, ...,Terminal.v)和.xco文件(Font.xco,Background.xco); - 关键步骤:在
Constraints下添加nexys3.ucf,该文件已预定义所有引脚:# nexys3.ucf 片段 NET "clk_100mhz" LOC = E3 | IOSTANDARD = LVCMOS33 | PERIOD = 10.000 ns; NET "vga_hsync" LOC = G19 | IOSTANDARD = LVCMOS33; NET "vga_vsync" LOC = H19 | IOSTANDARD = LVCMOS33; NET "vga_r<0>" LOC = D19 | IOSTANDARD = LVCMOS33; NET "vga_g<0>" LOC = D20 | IOSTANDARD = LVCMOS33; NET "vga_b<0>" LOC = E19 | IOSTANDARD = LVCMOS33;
注意:
nexys3.ucf中vga_r/g/b各定义 3 位(RGB333 格式),若需更高色深需修改约束并重写Terminal.v的像素打包逻辑。
4.2 Block RAM 初始化:COE 文件生成与内存加载
InstructionMemory.coe、DataMemory.coe、Background.coe是初始化 FPGA 片上 RAM 的关键文件。项目提供generate-stage-instantiation.py脚本,但实际使用需手动操作:
- 编写
InstructionMemory.asm(MIPS 汇编),例如:.text 0x00000000: lui $t0, 0x1234 0x00000004: addi $t0, $t0, 0x5678 0x00000008: sw $t0, 0($zero) - 运行
python generate-debugger.py InstructionMemory.asm生成InstructionMemory.coe; - 在 Vivado 中右键
InstructionMemory.xo→Edit in IP Packager→Re-customize IP→Port Configuration→Load COE file选择生成的.coe。
4.3 综合、实现与烧录验证
执行标准 Vivado 流程:
- Synthesis:检查
Critical Warning是否为 0(如有,通常是未连接的open端口,可忽略); - Implementation:重点观察
Timing Summary中WNS (Worst Negative Slack)是否 ≥ 0,若为负需优化时序(如在Pc.v中插入寄存器打拍); - Bitstream Generation:成功后生成
archexp.runs/impl_1/archexp.bit; - 烧录:使用 Adept 2.2.1 工具(Nexys3 官方配套),选择
Program Device→Select .bit file→Program。
烧录完成后,VGA 显示器应出现 60×80 字符界面,顶部显示PC,IR,R0–R31等实时值。若屏幕无输出:
- 用示波器测
vga_hsync引脚(G19)是否有 31.5kHz 方波; - 检查
Terminal.v中vga_blank信号是否恒为 0(应为周期性脉冲); - 在
Cpu.v中临时将vga_r/g/b全置为3'b111,确认显示器能否显示白屏。
5. 进阶技巧:指令集扩展与性能瓶颈定位
5.1 添加新指令:以nop和syscall为例
扩展指令需同步修改三处:
- ControlUnit.v:在
case语句中新增opcode分支,设置控制信号。nop(opcode=0x00)的输出全为 0;syscall(opcode=0x00, funct=0x0c)需置RegWrite=0,MemRead=0,MemWrite=0,Branch=0,Jump=0,AluSrc=0,AluOp=3'b000; - Disassembler.v:在
casez语句中添加32'h0000000c对应"syscall"字符串; - Testbench:编写
CpuTest.v的新测试用例,例如:// CpuTest.v 片段 initial begin reset = 1; #100 reset = 0; // 测试 nop:PC 应+4,无寄存器变化 wait (pc == 32'h00000000); #100 assert (pc == 32'h00000004) else $error("nop failed"); end
5.2 Stall 检测与流水线气泡注入
当lw指令后紧跟使用该加载数据的add指令时(RAW冲突),IdStage.v必须插入气泡(stall_id = 1)。项目通过以下逻辑检测:
// IdStage.v 中 stall 判断 assign stall_id = (IdEx_MemRead && (IdEx_WriteRegister != 5'd0) && (IdEx_WriteRegister == IfId_rs || IdEx_WriteRegister == IfId_rt));若stall_id为高,则IdStage输出全为 0,且IfIdRegisters.v的clk_en被拉低,暂停 IF 阶段取指。此时 VGA 屏幕上PC值将停滞 1 个周期,IR显示为0x00000000(气泡指令),这是验证 Stall 机制是否生效的最直观证据。
5.3 性能瓶颈分析:关键路径时序报告解读
在 Vivado 的Reports → Timing → Report Timing Summary中,重点关注:
- Slack:若
WNS = -1.2ns,说明最差路径比时钟周期慢 1.2ns; - Path Group:通常瓶颈在
PC + InstructionMemory Address→IfStage→IdStage的组合逻辑链; - Critical Path:点击
Report Timing Details,查看延时最大的单元,常见为InstructionMemory的 Block RAM 读取(约 2.5ns)或Disassembler.v的多级 case 语句(约 1.8ns)。
优化建议:
- 将
InstructionMemory改为双时钟域(clk_100mhz读,clk_25mhz写),利用 Block RAM 的异步读特性; - 在
Disassembler.v前插入一级寄存器缓存IR,将长组合路径拆分为两级时序。
VGA 调试界面本身即是性能探针——当PC更新频率明显低于 25MHz(即每 40ns 更新一次),说明流水线存在严重阻塞,需优先检查stall_id信号是否被意外拉高。
本文还有配套的精品资源,点击获取