第17课 · 前递

前递转发EX/MEMMEM/WB

📌 学习目标:实现完整的前递(Forwarding/Bypassing)逻辑,将 EX/MEM 和 MEM/WB 阶段的 ALU 结果直接前递到 EX 阶段的 ALU 输入,通过 Verilator 验证前递正确性。

一、前递原理

前递(也称转发/旁路)是解决数据冒险的主要方法。核心思想:不等数据写回寄存器文件,直接从流水线中间阶段获取结果

无前递:
ADD x1, x2, x3   IF  ID  EX  MEM WB
SUB x4, x1, x5       IF  ID  stall stall EX  MEM WB
                        ↑ 等待 x1 写回

有前递:
ADD x1, x2, x3   IF  ID  EX──→MEM WB
SUB x4, x1, x5       IF  ID──→EX  MEM WB
                              ↑ 从 EX/MEM 直接获取 x1

二、前递场景分析

2.1 EX/MEM 前递(1条指令间隔)

ADD x1, x2, x3    ← 结果在 EX/MEM 寄存器中
SUB x4, x1, x5    ← 需要 x1,从 EX/MEM 前递

前递条件:EX/MEM.RegWrite && EX/MEM.rd != 0 &&
          EX/MEM.rd == ID/EX.rs1

2.2 MEM/WB 前递(2条指令间隔)

ADD x1, x2, x3    ← 结果在 MEM/WB 寄存器中
OR  x6, x7, x8    ← 不影响 x1
SUB x4, x1, x5    ← 需要 x1,从 MEM/WB 前递

前递条件:MEM/WB.RegWrite && MEM/WB.rd != 0 &&
          MEM/WB.rd == ID/EX.rs1 &&
          !(EX/MEM.RegWrite && EX/MEM.rd != 0 && EX/MEM.rd == ID/EX.rs1)
          ↑ 注意:EX/MEM 优先级更高!

2.3 前递优先级

当 EX/MEM 和 MEM/WB 都有匹配的前递时,EX/MEM 优先(因为它是最新的结果)。

三、Verilog 前递实现

// forwarding.v — 完整前递逻辑 module forwarding ( input [4:0] ex_rs1, ex_rs2, // EX 阶段的源寄存器 input [4:0] mem_rd, // MEM 阶段目标寄存器 input mem_reg_write, // MEM 阶段写使能 input [31:0] mem_alu_result, // MEM 阶段 ALU 结果 input [4:0] wb_rd, // WB 阶段目标寄存器 input wb_reg_write, // WB 阶段写使能 input [31:0] wb_result, // WB 阶段结果 input [31:0] rs1_data, rs2_data, // 寄存器文件读出值 output [31:0] fwd_a, fwd_b // 前递后的值 ); // 前递 A 路径 (rs1) reg [31:0] fwd_a_r; assign fwd_a = fwd_a_r; always @(*) begin if (mem_reg_write && mem_rd != 5'b0 && mem_rd == ex_rs1) fwd_a_r = mem_alu_result; // EX/MEM 前递(优先) else if (wb_reg_write && wb_rd != 5'b0 && wb_rd == ex_rs1) fwd_a_r = wb_result; // MEM/WB 前递 else fwd_a_r = rs1_data; // 无前递,使用寄存器文件值 end // 前递 B 路径 (rs2) reg [31:0] fwd_b_r; assign fwd_b = fwd_b_r; always @(*) begin if (mem_reg_write && mem_rd != 5'b0 && mem_rd == ex_rs2) fwd_b_r = mem_alu_result; else if (wb_reg_write && wb_rd != 5'b0 && wb_rd == ex_rs2) fwd_b_r = wb_result; else fwd_b_r = rs2_data; end endmodule

3.1 测试台

// tb_forwarding.v module tb_forwarding; reg [4:0] ex_rs1, ex_rs2, mem_rd, wb_rd; reg mem_reg_write, wb_reg_write; reg [31:0] mem_alu_result, wb_result, rs1_data, rs2_data; wire [31:0] fwd_a, fwd_b; forwarding uut (.*); integer pass=0, fail=0; initial begin // 无前递 ex_rs1=1; ex_rs2=2; mem_rd=3; wb_rd=4; mem_reg_write=0; wb_reg_write=0; rs1_data=32'd100; rs2_data=32'd200; #1; if (fwd_a!==32'd100||fwd_b!==32'd200) begin $display("FAIL no fwd"); fail=fail+1; end else pass=pass+1; // EX/MEM 前递到 rs1 ex_rs1=3; mem_rd=3; mem_reg_write=1; mem_alu_result=32'd42; #1; if (fwd_a!==32'd42) begin $display("FAIL EX/MEM fwd_a: got %d", fwd_a); fail=fail+1; end else pass=pass+1; // MEM/WB 前递到 rs2 ex_rs2=4; wb_rd=4; wb_reg_write=1; mem_rd=5; mem_reg_write=0; wb_result=32'd99; #1; if (fwd_b!==32'd99) begin $display("FAIL MEM/WB fwd_b: got %d", fwd_b); fail=fail+1; end else pass=pass+1; // EX/MEM 优先于 MEM/WB ex_rs1=3; mem_rd=3; mem_reg_write=1; wb_rd=3; wb_reg_write=1; mem_alu_result=32'd111; wb_result=32'd222; #1; if (fwd_a!==32'd111) begin $display("FAIL priority: got %d", fwd_a); fail=fail+1; end else pass=pass+1; $display("========================================"); $display("前递测试: PASS=%0d FAIL=%0d", pass, fail); if (fail == 0) $display("✅ EX/MEM 前递逻辑验证正确!"); else $display("❌ 存在失败!"); $display("========================================"); $finish; end endmodule

四、Verilator 编译命令

verilator --cc forwarding.v --exe tb_forwarding.v \ --build --top-module tb_forwarding ./obj_dir/Vtb_forwarding

五、前递 vs 停顿

场景解决方法性能损失
ALU→ALU (1条间隔)EX/MEM前递0 周期
ALU→ALU (2条间隔)MEM/WB前递0 周期
Load→ALU (紧跟)停顿1周期+前递1 周期
Load→Store (紧跟)停顿1周期+前递1 周期

前递消除了大部分数据冒险的停顿,但 Load-Use 冒险仍需1个周期停顿。

🤔 思考题:为什么前递不能解决 Load-Use 冒险?LW 的数据最早在哪个阶段才可用?

💡 提示:LW 在 MEM 阶段末尾才读出数据,但下条指令在 EX 阶段就需要,差一个周期

🏆 成就解锁:前递工程师

✅ Verilator 仿真验证通过

✅ EX/MEM 前递路径正确

✅ MEM/WB 前递路径正确

✅ 前递优先级(EX/MEM > MEM/WB)正确

🎯 下一目标:停顿 → 第18课:停顿