fpga mips vivado verilog 五级流水线cpu设计 55条指令
这玩意儿折腾起来真得掉几撮头发。五级流水线CPU的设计就像搭乐高,每个部件都得严丝合缝。先说下我的开发环境:Vivado 2020.2配Artix7板子,Verilog代码量最终撸到了4000行左右。
流水线的五级划分大伙儿都熟------取指、译码、执行、访存、写回。但具体实现时,数据冒险处理绝对是个磨人精。来看这段冒险检测的代码:
verilog
always @(*) begin
// 前推判断逻辑
if (EX_MEM_RegWrite && (EX_MEM_rd != 0) && (EX_MEM_rd == ID_EX_rs))
ForwardA = 2'b10;
else if (MEM_WB_RegWrite && (MEM_WB_rd != 0) && (MEM_WB_rd == ID_EX_rs))
ForwardA = 2'b01;
else
ForwardA = 2'b00;
end
这段前推逻辑处理的是当后续指令要读取的寄存器被前面指令修改时,直接从流水线寄存器抓数据。注意那个rd != 0的条件,MIPS规定$0寄存器永远为0,这个细节漏了会导致各种灵异现象。
指令扩展方面,55条指令覆盖了MIPS32核心集。拿乘法指令MUL举例,得特别注意执行阶段的时序:
verilog
case(ALUControl)
3'b101: begin // 乘法操作
{HI, LO} = $signed(operandA) * $signed(operandB);
ALUResult = LO; // 默认存低32位到目标寄存器
end
endcase
这里用到了Verilog的符号乘法,但实际综合时会调用DSP Slice。Vivado报告显示这个乘法器吃掉了我板子上18%的DSP资源,要是用纯组合逻辑实现,时序立马崩给你看。
测试阶段最抓狂的是分支延迟槽的问题。JAL指令的实现里这个细节特别关键:
verilog
always @(posedge clk) begin
if (Flush) begin
IF_ID_instr <= NOP; // 插入空泡
IF_ID_pcplus4 <= 0;
end else begin
IF_ID_instr <= imem_data; // 正常取指
IF_ID_pcplus4 <= PC + 4;
end
end
当发生分支跳转时,必须立即清空流水线第一级,否则下条指令会错误执行。我在这个坑里卡了两天,用Vivado的ILA抓信号才发现有个周期没冲刷干净。
最后上板跑分,Dhrystone测试跑到1.57 DMIPS/MHz。资源消耗方面,LUT用了23k,FF用了15k,Block RAM吃了36%。有个骚操作是把指令内存拆分成两个32KB的Block RAM,这样能并行取指令和访存操作。
这项目最大的收获是:流水线就像高速公路,数据冲突好比连环追尾,前推和暂停机制就是交警和拖车。下次再搞的话,准备试试加入分支预测,现在这个版本遇到跳转指令还是得乖乖暂停流水线。
