Without bypass, a read in decode can observe old data while writeback updates the same register. This creates silent arithmetic bugs.
Cycle N: writeback writes x5 <= result; decode reads rs1 = x5 for the next instruction.
No bypass: decode sees the old x5 and computes with a stale operand.
With bypass: decode sees the new result in the same cycle.The bypass from the previous lesson is the fix for the same-cycle case. A writeback that lands a cycle after the decode read is a different hazard, handled by forwarding in the pipeline, not by this mux.
The bypass contract is one property: a same-cycle read of the written register must see the write data. The SystemVerilog form below needs a clocked context, so it is shown partial; the testbench checks the same property procedurally.
// snippet: the hazard property in SVA shape (needs a clocked module around it)
// If the same register is read and written in one cycle, the read data must match the write data.
// assert property (@(posedge clk)
// we_i && (waddr_i == raddr0_i) && (waddr_i != '0) |-> (rdata0_o == wdata_i));The nonzero guard is load-bearing. Without it, any write while reading x0 would demand that zero equal the write data, which never holds.
// Same-cycle hazard checker: raises a flag while the contract is broken.
// err_byp0/1 mirror the SVA property above for both read ports; err_x0 is the
// second check, demanding that x0 always read zero on both ports.
module rf_hazard_check #(
parameter int DW = 32,
parameter int AW = 5
) (
input logic we_i,
input logic [AW-1:0] waddr_i,
input logic [AW-1:0] raddr0_i,
input logic [AW-1:0] raddr1_i,
input logic [DW-1:0] wdata_i,
input logic [DW-1:0] rdata0_o,
input logic [DW-1:0] rdata1_o,
output logic err_byp0,
output logic err_byp1,
output logic err_x0
);
assign err_byp0 = we_i && (waddr_i != '0) && (waddr_i == raddr0_i) && (rdata0_o != wdata_i);
assign err_byp1 = we_i && (waddr_i != '0) && (waddr_i == raddr1_i) && (rdata1_o != wdata_i);
assign err_x0 = ((raddr0_i == '0) && (rdata0_o != '0)) ||
((raddr1_i == '0) && (rdata1_o != '0));
endmoduleThe new result. The bypass forwards the write data to the read port in the same cycle, so no stale operand reaches the next instruction.
Reads of x0 always return zero, so demanding rdata == wdata there would fail on every write. The exclusion keeps the property to registers the bypass actually serves.
The register file under a decode-versus-writeback collision, watched by the hazard checker on every cycle.
module regfile_2r1w #(
parameter int DW = 32,
parameter int AW = 5
) (
input logic clk,
input logic rst_n,
input logic we_i,
input logic [AW-1:0] waddr_i,
input logic [DW-1:0] wdata_i,
input logic [AW-1:0] raddr0_i,
input logic [AW-1:0] raddr1_i,
output logic [DW-1:0] rdata0_o,
output logic [DW-1:0] rdata1_o
);
logic [DW-1:0] rf [2**AW];
logic bypass0, bypass1;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
integer i;
for (i = 0; i < 2**AW; i++) rf[i] <= '0;
end else if (we_i && (waddr_i != '0)) begin
rf[waddr_i] <= wdata_i;
end
end
assign bypass0 = we_i && (waddr_i != '0) && (waddr_i == raddr0_i);
assign bypass1 = we_i && (waddr_i != '0) && (waddr_i == raddr1_i);
always_comb begin
rdata0_o = (raddr0_i == '0) ? '0 : rf[raddr0_i];
rdata1_o = (raddr1_i == '0) ? '0 : rf[raddr1_i];
if (bypass0) rdata0_o = wdata_i;
if (bypass1) rdata1_o = wdata_i;
end
endmodule
module rf_hazard_check #(
parameter int DW = 32,
parameter int AW = 5
) (
input logic we_i,
input logic [AW-1:0] waddr_i,
input logic [AW-1:0] raddr0_i,
input logic [AW-1:0] raddr1_i,
input logic [DW-1:0] wdata_i,
input logic [DW-1:0] rdata0_o,
input logic [DW-1:0] rdata1_o,
output logic err_byp0,
output logic err_byp1,
output logic err_x0
);
assign err_byp0 = we_i && (waddr_i != '0) && (waddr_i == raddr0_i) && (rdata0_o != wdata_i);
assign err_byp1 = we_i && (waddr_i != '0) && (waddr_i == raddr1_i) && (rdata1_o != wdata_i);
assign err_x0 = ((raddr0_i == '0) && (rdata0_o != '0)) ||
((raddr1_i == '0) && (rdata1_o != '0));
endmodule