A small pipeline passes its tests in the team's usual simulator after a tidy-up. A colleague runs it in Verilator and half the pipeline outputs come out one cycle early. Gate-level simulation of the synthesised netlist passes.
Which of the three is the chip, and what in the RTL lets them disagree?
They pick the run they like: the passing simulator is "the reference" and the other is "buggy". Each run is right about what it was given: the language lets simulators order same-time events as they choose, and synthesis reads only the parts of the RTL that describe hardware. The bug is RTL that means different things to each of them.
Pattern | Usual cause |
|---|---|
Two simulators disagree, by exactly one cycle, on a register's output | a blocking assignment in a clocked block that another block reads (a race) |
Results change after an edit that should not matter: reordered modules, an added | a race: the order of same-time events moved |
RTL simulation shows a stale combinational output; the netlist is right | an incomplete sensitivity list |
RTL simulation passes from a start value or with a delay; the netlist or the chip does not | an |
The bench drives an input with | a bench race; see the testbench bugs lesson in the part hub |
Before you blame ordering, rule out X: a four-state simulator and a two-state one also disagree on unknown values. A run with random start values, as in X and reset bugs, separates the two. On the debug loop this is Localise by differential debugging, then Hypothesise.
Run it a third way. Simulate the synthesised netlist with the same bench, and the RTL in a second simulator. If the netlist agrees with one RTL run only by luck, the RTL is ambiguous.
Read the lint. verilator --lint-only -Wall reports BLKSEQ for a blocking assignment in a clocked block. Then search the RTL for always @( lists that are not @* or always_comb, and for initial and # outside the bench.
Find the first cycle the runs differ, and how. A register one stage early is a blocking assignment. A combinational output that lags one input and follows the others is a sensitivity list.
The design has a first pipeline stage feeding two second stages, q_b and q_c, and a mux. FIX = 0 writes the first stage with a blocking assignment and leaves the select out of the mux's sensitivity list. FIX = 1 (the default) is the corrected design. The bench checks that both second stages show the input from two edges earlier, and that the mux follows a change on its select alone.
// mismatch: two simulation-synthesis mismatches in one small design. FIX = 0 builds the buggy
// version: a pipeline register written with a blocking assignment, and a mux whose sensitivity
// list leaves out its select. FIX = 1 (the default) is the corrected design.
`timescale 1ns/1ps
module stage #(parameter bit BLOCKING = 0) (input logic clk, input logic [3:0] d, output logic [3:0] q);
if (BLOCKING) begin : g_bad
always @(posedge clk) q = d; // bad: blocking assignment in a clocked block
end else begin : g_good
always @(posedge clk) q <= d;
end
endmodule
module pick #(parameter bit FULL = 1) (input logic a, input logic b, input logic s, output logic y);
if (FULL) begin : g_good
always @* y = s ? b : a;
end else begin : g_bad
always @(a or b) y = s ? b : a; // bad: s is missing from the list
end
endmodule
module dut #(parameter bit FIX = 1) (
input logic clk, input logic [3:0] d, input logic a, b, s,
output logic [3:0] q_b, q_c, output logic y);
logic [3:0] q_a;
stage u_b (.clk, .d(q_a), .q(q_b)); // second stage, path b
stage #(.BLOCKING(!FIX)) u_a (.clk, .d, .q(q_a)); // first stage
stage u_c (.clk, .d(q_a), .q(q_c)); // second stage, path c
pick #(.FULL(FIX)) u_p (.a, .b, .s, .y);
endmodule
module tb;
parameter bit FIX = 1;
logic clk = 1'b0, a = 1'b0, b = 1'b1, s = 1'b0, y;
logic [3:0] d = '0, q_b, q_c, hist [0:1];
int bad_b = 0, bad_c = 0, bad_y = 0, first_b = -1, first_c = -1;
dut #(.FIX(FIX)) u (.clk, .d, .a, .b, .s, .q_b, .q_c, .y);
always #5 clk = ~clk;
initial begin
for (int cyc = 0; cyc < 12; cyc++) begin
@(negedge clk);
if (cyc >= 2) begin // q_b and q_c must be d from two edges ago
if (q_b !== hist[1]) begin bad_b++; if (first_b < 0) first_b = cyc; end
if (q_c !== hist[1]) begin bad_c++; if (first_c < 0) first_c = cyc; end
end
hist[1] = hist[0]; hist[0] = (cyc * 5 + 3) % 16;
d = hist[0];
end
s = 1'b1; #1; // only the select changes: y must follow b
if (y !== b) bad_y++;
s = 1'b0; #1;
if (y !== a) bad_y++;
$display("q_b wrong %0d times (first at cycle %0d), q_c wrong %0d times (first at cycle %0d), y wrong %0d of 2",
bad_b, first_b, bad_c, first_c, bad_y);
if (bad_b || bad_c || bad_y) $fatal(1, "mismatch: the simulation does not match the hardware");
$display("PASS: two-stage pipeline and mux behave as the hardware will (FIX=%0d)", FIX);
$finish;
end
endmodule$ iverilog -g2012 -P tb.FIX=0 -o m0.vvp mismatch.sv && vvp -n m0.vvp
q_b wrong 5 times (first at cycle 2), q_c wrong 5 times (first at cycle 3), y wrong 2 of 2
FATAL: mismatch.sv:55: mismatch: the simulation does not match the hardware
Time: 122000 Scope: tb
$ verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 -Mdir vlm0 mismatch.sv > /dev/null 2>&1 && vlm0/Vtb
q_b wrong 10 times (first at cycle 2), q_c wrong 10 times (first at cycle 2), y wrong 1 of 2
[122000] %Fatal: mismatch.sv:55: Assertion failed in tb: mismatch: the simulation does not match the hardware
...Same design, two different failures. Verilator gets every second-stage value wrong: it ran the blocking first stage before both readers on every edge, so q_b and q_c see the new value and the pipeline is one stage short. Icarus gets half of them wrong. A copy of the bench with one $display per cycle (mm_dbg.sv) shows how:
$ iverilog -g2012 -P tb.FIX=0 -o mdbg.vvp mm_dbg.sv && vvp -n mdbg.vvp
...
cyc 2 d=8 q_b=8 q_c=3 want=3 y=x
cyc 3 d=13 q_b=8 q_c=13 want=8 y=x
cyc 4 d=2 q_b=2 q_c=13 want=13 y=x
cyc 5 d=7 q_b=2 q_c=7 want=2 y=x
...In Icarus the two readers take turns: at cycle 2 q_b has the early value and q_c the right one, at cycle 3 the other way round. Both simulators are allowed to do this, because nothing in the language orders two blocks triggered by the same edge. The mux shows the second bug: y never follows s alone. Icarus never ran the block at all, since a and b never changed, so y is X; Verilator evaluated it once at the start, so it is right only by luck when s returns to 0.
$ sed '/^module tb/,$d' mismatch.sv > design.sv # Yosys reads the design only
$ yosys -q -p "read_verilog -sv design.sv; chparam -set FIX 0 dut; synth -flatten -top dut; rename dut dut_net; write_verilog -noattr dut_net.v"
$ iverilog -g2012 -P tb.FIX=0 -o net.vvp dut_net.v tb_net.sv && vvp -n net.vvpq_b wrong 0 times (first at cycle -1), q_c wrong 0 times (first at cycle -1), y wrong 0 of 2
PASS: two-stage pipeline and mux behave as the hardware will (FIX=0)
...
$ grep -E "assign (y|q_b|q_c) " dut_net.v; grep -A1 "always" dut_net.v | head -4
assign y = s ? b : a;
assign q_b = \u_b.q ;
assign q_c = \u_b.q ;
always @(posedge clk)
\u_a.q [0] <= d[0];
always @(posedge clk)
\u_a.q [1] <= d[1];tb_net.sv is the same bench with dut_net instantiated in place of dut. The buggy RTL synthesises to a correct circuit, and Yosys printed no warning. It made u_a a flip-flop whatever the assignment, built the mux from its expression and ignored the list, and even merged the two identical second stages into one (q_c is u_b.q). The two stages that disagreed in simulation are one wire in hardware. Lint was the only tool that complained: Verilator -Wall reported %Warning-BLKSEQ: design.sv:7:29: Blocking assignment '=' in sequential logic process, and said nothing about the sensitivity list.
Why the blocking assignment is a race: the first stage writes q_a at once, and whether a reader on the same edge runs before or after that write is up to the simulator. With <= every block reads the old value and the order stops mattering. The RTL course's race-aware reasoning has the scheduling rules.
// dly: an initial value and two delays, none of which a chip can have.
module dly(input wire clk, input wire d, output reg q, output wire qd);
initial q = 1'b1; // simulation start value
always @(posedge clk) q <= #2 d; // a 2 ns clock-to-q, in simulation only
assign #3 qd = q; // a 3 ns wire, in simulation only
endmodule$ yosys -q -p "read_verilog dly.v; synth -top dly; write_verilog -noattr dly_net.v" && grep -vE "^/\*|^$" dly_net.v
module dly(clk, d, q, qd);
...
reg q = 1'h1;
...
always @(posedge clk)
q <= d;
assign qd = q;
endmoduleBoth delays are gone, silently. The initial value survives only as an attribute (reg q = 1'h1), which an FPGA can load at configuration and an ASIC flip-flop cannot. RTL that works only because of a delay or a start value works only in simulation.
Clocked blocks use `<=`, combinational blocks use `=`. Use always_ff and always_comb, which make the intent checkable.
*`always_comb` or `@`, never a hand-written list.** The tool builds the list, and it cannot leave out a select.
No `initial` and no `#` in RTL. State that needs a value at power-up gets a reset; timing belongs to the timing constraints.
Lint as a gate with BLKSEQ (and COMBDLY, its combinational twin) as errors. dbglab's RTL passes this: verilator --lint-only -Wall reports no BLKSEQ.
A coding rule a script can check: clocked logic in always_ff, combinational logic in always_comb, no initial or # under rtl/.
Simulate the netlist of each block once, with its own bench, early in the project. It is the one run that cannot be fooled by RTL-only constructs.
Run the regression in two simulators. A disagreement that is not X is a bug to find, not noise to waive.
$display to a module, a test that passed for months fails by one cycle. Nothing else changed. What do you suspect, and what is the first check?A race: an edit that should not change behaviour moved the order of same-time events. Run lint for BLKSEQ, and look for blocking assignments in clocked blocks, and for the bench driving inputs with = at the clock edge. Simulating the netlist, or the RTL in a second simulator, shows whether the RTL is ambiguous.
y wrong once and Icarus twice. Why the difference, and why does it not matter?Icarus never ran always @(a or b), because neither a nor b changed, so y stayed X and both checks failed. Verilator evaluated the block once at the start, so y was 0, wrong while s was 1 and right by luck when s went back to 0. Either way the mux does not follow s. The netlist, which is what synthesis built, passes both checks, so the RTL is what must change.
mismatch.sv as shown, 2026-10-18. FIX=0: iverilog -g2012 -P tb.FIX=0 and vvp -n (Icarus Verilog 13.0): q_b and q_c wrong 5 times each, y wrong 2 of 2; verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 (Verilator 5.048): 10 and 10, y 1 of 2. The per-cycle trace is from a copy with one extra $display. FIX=1 passes in both.
Yosys 0.69+post: the design part only (Yosys cannot parse the bench's timing controls), yosys -q -p "read_verilog -sv design.sv; chparam -set FIX 0 dut; synth -flatten -top dut; rename dut dut_net; write_verilog -noattr dut_net.v", no warning; the netlist simulated with the same bench in Icarus passes. verilator --lint-only -Wall -GFIX=0 --top-module dut design.sv reports BLKSEQ at line 7 only. dly.v through synth -top dly: delays dropped, the initial value kept as reg q = 1'h1.
Limits: two small cases and two simulators. The order each simulator picks is not promised by the language and may change between versions. dbglab has no bug of this class: Verilator lint reports no BLKSEQ on its RTL, and its bugs fail at the same cycle in both simulators.