第18课 · 停顿
停顿气泡Load-Use
📌 学习目标:实现 Load-Use 冒险的停顿逻辑,插入气泡(bubble)使流水线暂停1个周期,通过 Verilator 验证停顿行为正确。
一、为什么需要停顿?
前递能解决大部分数据冒险,但Load-Use 冒险无法仅靠前递解决:
LW x1, 0(x2) IF ID EX MEM←数据在这里才出来
ADD x3, x1, x4 IF ID EX←需要 x1,但还在 MEM 阶段
↑ 差一个周期!前递也来不及
解决:插入1个气泡(停顿1周期)
LW x1, 0(x2) IF ID EX MEM WB
bubble -- -- -- ← 气泡
ADD x3, x1, x4 IF ID stall EX MEM WB
↑ 现在 MEM/WB 可以前递了
二、停顿的实现
停顿需要做三件事:
- 冻结 PC:不更新 PC(stall IF 阶段)
- 冻结 IF/ID:保持当前指令(stall ID 阶段)
- 冲刷 ID/EX:插入气泡(NOP),清除控制信号
三、Load-Use 检测条件
Stall = ID/EX.MemRead && (ID/EX.rd != 0) &&
((ID/EX.rd == IF/ID.rs1) || (ID/EX.rd == IF/ID.rs2))
↑ 前一条是 Load ↑ 不写 x0 ↑ 后一条用了 Load 的目标寄存器
四、Verilog 停顿控制
module stall_control (
input [4:0] ex_rd,
input ex_mem_read,
input [4:0] id_rs1, id_rs2,
output stall_if,
output stall_id,
output flush_ex
);
wire load_use = ex_mem_read && (ex_rd != 5'b0) &&
((ex_rd == id_rs1) || (ex_rd == id_rs2));
assign stall_if = load_use;
assign stall_id = load_use;
assign flush_ex = load_use;
endmodule
4.1 带停顿的流水线寄存器
module pipe_reg_ifid_stall (
input clk, rst,
input stall,
input [31:0] instr_f, pc_f,
output reg [31:0] instr_d, pc_d
);
always @(posedge clk or posedge rst) begin
if (rst) begin
instr_d <= 32'b0; pc_d <= 32'b0;
end else if (!stall) begin
instr_d <= instr_f;
pc_d <= pc_f;
end
end
endmodule
module pipe_reg_idex_stall (
input clk, rst,
input flush,
input [31:0] pc_d, rs1_d, rs2_d, imm_d,
input [4:0] rd_d,
input reg_write_d, mem_read_d, mem_write_d,
output reg [31:0] pc_e, rs1_e, rs2_e, imm_e,
output reg [4:0] rd_e,
output reg reg_write_e, mem_read_e, mem_write_e
);
always @(posedge clk or posedge rst) begin
if (rst || flush) begin
pc_e <= 32'b0; rs1_e <= 32'b0; rs2_e <= 32'b0;
imm_e <= 32'b0; rd_e <= 5'b0;
reg_write_e <= 0; mem_read_e <= 0; mem_write_e <= 0;
end else begin
pc_e <= pc_d; rs1_e <= rs1_d; rs2_e <= rs2_d;
imm_e <= imm_d; rd_e <= rd_d;
reg_write_e <= reg_write_d; mem_read_e <= mem_read_d;
mem_write_e <= mem_write_d;
end
end
endmodule
4.2 测试台
module tb_stall;
reg [4:0] ex_rd, id_rs1, id_rs2;
reg ex_mem_read;
wire stall_if, stall_id, flush_ex;
stall_control uut (.*);
integer pass=0, fail=0;
initial begin
ex_rd=5; id_rs1=3; id_rs2=4; ex_mem_read=1; #1;
if (stall_if||stall_id||flush_ex) begin $display("FAIL no hazard"); fail=fail+1; end else pass=pass+1;
ex_rd=3; id_rs1=3; id_rs2=4; ex_mem_read=1; #1;
if (!stall_if||!stall_id||!flush_ex) begin $display("FAIL load-use rs1"); fail=fail+1; end else pass=pass+1;
ex_rd=4; id_rs1=3; id_rs2=4; ex_mem_read=1; #1;
if (!stall_if||!stall_id||!flush_ex) begin $display("FAIL load-use rs2"); fail=fail+1; end else pass=pass+1;
ex_rd=3; id_rs1=3; id_rs2=4; ex_mem_read=0; #1;
if (stall_if||stall_id||flush_ex) begin $display("FAIL non-load"); fail=fail+1; end else pass=pass+1;
ex_rd=0; id_rs1=0; id_rs2=0; ex_mem_read=1; #1;
if (stall_if||stall_id||flush_ex) begin $display("FAIL x0"); fail=fail+1; end else pass=pass+1;
$display("========================================");
$display("停顿控制测试: PASS=%0d FAIL=%0d", pass, fail);
if (fail == 0) $display("✅ Load-Use 停顿逻辑验证正确!");
else $display("❌ 存在失败!");
$display("========================================");
$finish;
end
endmodule
五、Verilator 编译命令
verilator --cc stall_control.v pipe_reg_with_stall.v \
--exe tb_stall.v --build --top-module tb_stall
./obj_dir/Vtb_stall
六、停顿的性能影响
每遇到一次 Load-Use 冒险,损失 1 个时钟周期。典型程序中 Load 占约 25%,其中约 20% 会产生 Load-Use 冒险,所以停顿率约 5%。
实际 CPI ≈ 1 + 0.05(Load-Use停顿) + 分支惩罚
🤔 思考题:编译器如何帮助减少 Load-Use 停顿?
💡 提示:指令调度——在 LW 和使用结果之间插入一条不相关的指令
🏆 成就解锁:停顿控制专家
✅ Verilator 仿真验证通过
✅ Load-Use 冒险检测正确
✅ 停顿信号(stall_if/stall_id)验证正确
✅ 气泡插入(flush_ex)验证正确
✅ x0 寄存器特殊处理正确
🎯 下一目标:分支预测 → 第19课:分支预测