第16课 · 流水线冒险
冒险数据冒险控制冒险结构冒险
📌 学习目标:识别流水线中的三类冒险(结构、数据、控制),实现数据冒险检测单元,通过 Verilator 验证检测逻辑。
一、什么是流水线冒险?
冒险(Hazard)是阻止下一条指令在预期时钟周期执行的情况。三类冒险:
| 类型 | 原因 | 解决方法 |
| 结构冒险 | 硬件资源冲突 | 分离指令/数据存储器 |
| 数据冒险 | 数据依赖 | 前递(转发) + 停顿 |
| 控制冒险 | 分支改变PC | 分支预测 + 延迟槽 |
二、结构冒险
当两条指令同时需要同一硬件资源时发生。例如:IF 和 MEM 同时访问同一存储器。
解决方案:哈佛架构——指令存储器和数据存储器分离(RISC-V 标准做法)。
三、数据冒险
当一条指令依赖于前一条指令的结果,而结果尚未写回时发生。
3.1 RAW(Read After Write)— 最常见
ADD x1, x2, x3 ← 写 x1 (WB阶段才完成)
SUB x4, x1, x5 ← 读 x1 (ID阶段就需要!)
↑ 数据依赖:SUB 在 ID 读 x1 时,ADD 还没写回
3.2 数据冒险分类
- EX 冒险:前条指令 ALU 结果,后条指令需要 → 可用 EX/MEM 前递
- MEM 冒险:前条指令 ALU 结果,后条指令需要 → 可用 MEM/WB 前递
- Load-Use 冒险:前条 LW 的数据,后条指令需要 → 必须停顿1周期
3.3 冒险检测条件
检测 Load-Use 冒险:
if (ID/EX.MemRead && // 前条是 Load
(ID/EX.rd == IF/ID.rs1 || // 后条需要 rs1
ID/EX.rd == IF/ID.rs2)) // 后条需要 rs2
→ 停顿 + 冲刷
四、控制冒险
分支指令在 EX 阶段才能确定是否跳转,但 IF 已经取了顺序的下一条指令。
4.1 简单方案:停顿
检测到分支就停顿,等结果出来再取指。代价:每个分支损失 2-3 个周期。
4.2 假设不跳转
继续取顺序指令,如果实际跳转则冲刷。代价:跳转时损失 2-3 个周期。
4.3 延迟分支
分支后的指令无论跳不跳转都执行(延迟槽),编译器负责填入有用指令。
五、Verilog 冒险检测
module hazard_unit (
input [4:0] id_rs1, id_rs2,
input [4:0] ex_rd,
input ex_mem_read,
input ex_reg_write,
input [4:0] mem_rd,
input mem_reg_write,
output stall,
output flush,
output [1:0] fwd_a, fwd_b
);
assign stall = ex_mem_read && (ex_rd != 5'b0) &&
((ex_rd == id_rs1) || (ex_rd == id_rs2));
assign flush = stall;
assign fwd_a = (ex_reg_write && ex_rd != 5'b0 && ex_rd == id_rs1) ? 2'b01 :
(mem_reg_write && mem_rd != 5'b0 && mem_rd == id_rs1) ? 2'b10 :
2'b00;
assign fwd_b = (ex_reg_write && ex_rd != 5'b0 && ex_rd == id_rs2) ? 2'b01 :
(mem_reg_write && mem_rd != 5'b0 && mem_rd == id_rs2) ? 2'b10 :
2'b00;
endmodule
5.1 测试台
module tb_hazard;
reg [4:0] id_rs1, id_rs2, ex_rd, mem_rd;
reg ex_mem_read, ex_reg_write, mem_reg_write;
wire stall, flush;
wire [1:0] fwd_a, fwd_b;
hazard_unit uut (.*);
integer pass=0, fail=0;
initial begin
id_rs1=1; id_rs2=2; ex_rd=3; mem_rd=4;
ex_mem_read=0; ex_reg_write=1; mem_reg_write=1; #1;
if (stall||fwd_a!==2'b00||fwd_b!==2'b00) begin $display("FAIL no hazard"); fail=fail+1; end else pass=pass+1;
id_rs1=3; id_rs2=2; ex_rd=3; ex_mem_read=1; #1;
if (!stall) begin $display("FAIL load-use not detected"); fail=fail+1; end else pass=pass+1;
id_rs1=3; id_rs2=4; ex_rd=3; mem_rd=4;
ex_mem_read=0; ex_reg_write=1; mem_reg_write=1; #1;
if (fwd_a!==2'b01||fwd_b!==2'b10) begin
$display("FAIL fwd: a=%b b=%b", fwd_a, fwd_b); fail=fail+1;
end else pass=pass+1;
id_rs1=0; id_rs2=3; ex_rd=0; ex_mem_read=0;
ex_reg_write=1; mem_reg_write=0; mem_rd=0; #1;
if (fwd_a!==2'b00) begin $display("FAIL x0 fwd"); fail=fail+1; end else pass=pass+1;
$display("========================================");
$display("冒险检测测试: PASS=%0d FAIL=%0d", pass, fail);
if (fail == 0) $display("✅ 数据冒险检测全部正确!");
else $display("❌ 存在失败!");
$display("========================================");
$finish;
end
endmodule
六、Verilator 编译命令
verilator --cc hazard_unit.v --exe tb_hazard.v \
--build --top-module tb_hazard
./obj_dir/Vtb_hazard
🤔 思考题:为什么 x0 寄存器不应触发冒险检测和前递?
💡 提示:x0 硬连线为 0,即使有写 x0 的指令也不会改变 x0 的值
🏆 成就解锁:冒险检测专家
✅ Verilator 仿真验证通过
✅ Load-Use 冒险检测正确
✅ EX/MEM 前递检测正确
✅ x0 寄存器特殊处理正确
🎯 下一目标:前递实现 → 第17课:前递