第一次在实验课听到"五段流水线"这个词时,我盯着幻灯片上复杂的数据通路图发愣。作为计算机组成原理课程的期末设计,我们需要在FPGA上实现一个支持38条MIPS指令的模型机,从单周期开始,最终升级到完整的流水线架构。当时的我完全没想到,这个看似简单的课程设计会让我经历如此多的调试不眠夜。
在开始流水线冒险之前,必须先理解单周期CPU的工作原理。我的开发板是Xilinx Artix-7系列,使用Vivado 2019.2作为开发环境。单周期设计看似简单,但魔鬼藏在细节里。
我负责实现的38条MIPS指令包括:
遇到的第一个坑是指令译码。Verilog代码中,我最初用简单的case语句处理opcode:
verilog复制always @(*) begin
case(opcode)
6'b000000: // R-type
case(funct)
6'b100000: alu_op = ADD;
// 其他功能码...
endcase
6'b100011: // LW
mem_read = 1;
// 其他操作码...
endcase
end
但很快发现BEQ和BNE的判断逻辑有问题——需要同时比较两个寄存器值并计算跳转地址。修正后的控制单元增加了专门的比较器模块。
单周期CPU需要统一编址的存储系统。我的设计采用了Block Memory Generator IP核实现指令存储器,用分布式RAM实现数据存储器。关键配置参数:
| 参数 | 指令存储器 | 数据存储器 |
|---|---|---|
| 位宽 | 32-bit | 32-bit |
| 深度 | 1024 | 512 |
| 初始化文件 | inst.mem | 无 |
| 读写延迟 | 1周期 | 1周期 |
注意:Vivado中Block Memory的初始化文件需要纯二进制格式,我写了个Python脚本将汇编代码转换为合适的格式。
单周期CPU通过验收后,真正的挑战才开始。五段流水线将指令执行分为:
每个阶段之间需要插入流水线寄存器保存中间结果。我的Verilog实现:
verilog复制// IF/ID流水线寄存器
always @(posedge clk or posedge reset) begin
if(reset) begin
id_inst <= 32'b0;
id_pc_plus_4 <= 32'b0;
end else if(!stall) begin // 处理暂停信号
id_inst <= if_inst;
id_pc_plus_4 <= if_pc_plus_4;
end
end
每个寄存器需要传递的控制信号和数据超过30个,手动连接极易出错。后来我改用SystemVerilog的struct来组织这些信号:
verilog复制typedef struct packed {
logic [31:0] pc_plus_4;
logic [31:0] inst;
// 其他信号...
} if_id_reg_t;
if_id_reg_t if_id_reg;
当我在测试程序中写下以下序列时,问题出现了:
code复制ADD $t0, $t1, $t2
SUB $t3, $t0, $t4 // $t0依赖上条指令结果
这就是经典的数据冲突(Data Hazard)。解决方法是通过旁路(Forwarding)将ALU结果提前反馈:
verilog复制// 旁路控制逻辑
always @(*) begin
if (ex_mem_reg_write && (ex_mem_rd != 0) && (ex_mem_rd == id_ex_rs))
forward_a = 2'b10; // 使用EX阶段结果
else if (mem_wb_reg_write && (mem_wb_rd != 0) && (mem_wb_rd == id_ex_rs))
forward_a = 2'b01; // 使用MEM阶段结果
else
forward_a = 2'b00; // 无旁路
end
比数据冲突更棘手的是控制冒险(Control Hazard)。当遇到分支指令时,流水线可能需要清空后续已取指令。我最初采用简单的"冻结流水线"方案:
verilog复制// 分支处理逻辑
assign stall = id_branch & id_branch_taken; // 分支成功时暂停
但这种方案性能损失严重。后来改进为"延迟槽"设计,在分支指令后总是执行下一条指令。这需要编译器支持,在课程设计中我手动调整了测试程序。
整个开发过程中,Vivado的仿真工具成为我最亲密的战友。几个实用技巧:
信号分组:将相关信号放入同一波形组
条件触发:设置复杂的触发条件
tcl复制when {/tb_cpu/id_stage/inst[31:26] == 6'b000100} # 当遇到BEQ指令时触发
为了评估流水线效果,我添加了简单的性能计数器:
verilog复制reg [31:0] cycle_count;
reg [31:0] inst_count;
always @(posedge clk) begin
cycle_count <= cycle_count + 1;
if (!stall && !flush)
inst_count <= inst_count + 1;
end
实测显示,五段流水线比单周期CPU性能提升约3.5倍(测试程序:计算斐波那契数列前20项)。
当仿真一切正常后,真正的噩梦才开始——FPGA板级调试。遇到的主要问题:
时钟问题:开发板100MHz时钟导致建立时间违规
verilog复制wire clk_bufg;
BUFG bufg_inst (.I(clk), .O(clk_bufg));
按键消抖:手动复位信号不稳定
verilog复制reg [15:0] debounce_cnt;
always @(posedge clk) begin
if (btn != btn_sync)
debounce_cnt <= 0;
else if (debounce_cnt != 16'hFFFF)
debounce_cnt <= debounce_cnt + 1;
if (debounce_cnt == 16'hFFFE)
btn_sync <= btn;
end
IO显示:七段数码管显示乱码
完成基本功能后,还需要考虑一些增强功能:
简单的异常处理流程:
有效的测试方案应该包括:
我编写了一个Python脚本自动生成随机测试用例并验证结果:
python复制def gen_arith_test():
op = random.choice(['add', 'sub', 'and', 'or'])
rd = random.randint(1, 31)
rs = random.randint(1, 31)
rt = random.randint(1, 31)
return f"{op} ${rd}, ${rs}, ${rt}"
随着设计复杂度的提升,良好的代码结构变得至关重要。我的项目目录结构:
code复制/mips_cpu
/rtl
control_unit.v # 控制单元
datapath.v # 数据通路
hazard_unit.v # 冒险处理
mips_cpu.v # 顶层模块
/sim
testbench.v # 测试平台
test_programs # 测试程序
/constraints
xdc_files # 约束文件
/scripts
mem_gen.py # 存储器初始化脚本
使用Git进行版本控制,关键提交节点:
回顾整个项目,以下经验值得分享:
几个典型的"坑"及解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 仿真波形全红 | 组合逻辑环路 | 检查always块敏感列表 |
| 流水线结果滞后 | 旁路逻辑缺失 | 完善转发条件判断 |
| FPGA资源占用过高 | 未使用Block RAM | 配置存储器使用专用RAM资源 |
| 时序违规 | 关键路径过长 | 插入流水线寄存器分级处理 |
在最终验收演示时,我的流水线MIPS模型机成功运行了一个小型排序算法程序。那一刻,所有调试的痛苦都化作了成就感。这个课程设计不仅让我深入理解了计算机体系结构,更锻炼了工程实践能力——从Verilog编码、仿真调试到FPGA实现的完整流程。