Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Part 0 · What Debugging Is6
  • Part 1 · Reproduce8
  • Part 2 · Observe9
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
  • Part 5 · Bug Classes16
    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • X and Reset Bugs
    • Races and Simulation-Synthesis Mismatches
    • Latches and Combinational Loops
    • CDC Bugs
    • Reset-Domain Crossing Bugs
    • Timing-Only Bugs
    • Protocol Violations
    • Hangs and Deadlocks
    • Starvation and Livelock
    • Performance Bugs
    • Power-Intent Bugs
    • DFT-Mode Bugs
    • Configuration and Register-Map Bugs
    • Testbench Bugs That Look Like Design Bugs
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 5 · Bug Classes
  1. Topics
  2. The Art of Hardware Debug
  3. Part 5 · Bug Classes
  4. Races and Simulation-Synthesis Mismatches

Part 5 · Bug Classes · Working (W)

Races and Simulation-Synthesis Mismatches

Races and simulation-synthesis mismatches: a pipeline register written with a blocking assignment and a mux with an incomplete sensitivity list give three different answers in Icarus, Verilator and the Yosys netlist. The signature, the first three checks, and the initial values and delays that synthesis drops.

On this page

    Why this matters

    When the simulation and the chip disagree, every passing test is evidence about the wrong design. A blocking assignment or an incomplete sensitivity list can pass a whole regression in one simulator, fail in another, and work correctly in silicon, so nobody knows which result to believe.

    After this lesson you can: recognise a race or a simulation-synthesis mismatch from runs that disagree, confirm it by simulating the synthesised netlist and reading lint, and fix the RTL so that every simulator and the netlist agree.

    Note

    Level: Working (W) · Debug-loop step: Localise, Hypothesise

    Before this: Differential Debugging, X and Reset Bugs

    Three answers to one question

    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?

    What people usually do, and why it fails

    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.

    The signature

    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 $display

    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 initial value or a # delay in RTL, which synthesis drops

    The bench drives an input with = at the clock edge; simulators disagree on whether the flop saw it

    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.

    The first three checks

    1. 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.

    2. 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.

    3. 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.

    One real reproduction: one design, three answers

    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.

    stage.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    // 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

    Icarus and Verilator: two orders

    Terminal
    $ 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:

    Terminal
    $ 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.

    The netlist: the third answer, and the chip's

    terminal
    $ 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.vvp
    Terminal
    q_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.

    What synthesis drops: initial values and delays

    dly.v
    1
    2
    3
    4
    5
    6
    // 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
    Terminal
    $ 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;
    endmodule

    Both 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.

    The fix pattern

    • 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.

    Preventing it

    • 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.

    Traps

    Common pitfall

    "It passes in our simulator." Icarus passed half the second-stage checks and Verilator none; a different design or a tool upgrade can flip either. A race that happens to resolve the right way today is still a race.

    Common pitfall

    Fixing the bench instead of the design. Moving the bench's check by one cycle makes Verilator pass and breaks Icarus, and the netlist then fails. If the fix depends on the simulator, the RTL is still ambiguous.

    Why this matters

    Senior lens. These bugs are cheap to prevent and expensive to find, because each tool tells a consistent story on its own. Gate the merge on lint, and run a second simulator nightly; both cost little compared with one day spent arguing about which simulator is right.

    Interview

    Asked: "Your RTL simulation passes, but gate-level simulation of the netlist fails (or the other way round). What do you look for?"

    Weak answer: Gate-level simulation is often pessimistic, so I would check the timing annotation and maybe waive the failure if the RTL passes.

    Strong answer: First I rule out X pessimism, since gate-level runs show more X. Then I suspect RTL that means something different to synthesis: a blocking assignment in a clocked block that another block reads, an incomplete sensitivity list, or an initial value or delay that synthesis drops. Lint flags the first; I search for the others. I find the first cycle the runs differ: a register one stage early points to blocking, a stale combinational output to the list. The netlist is what the chip does, so I fix the RTL until RTL simulation in two simulators agrees with it.

    Follow-ups: (1) Why does Yosys not warn about the missing sensitivity entry? (2) How would you show that a difference between two simulators is a race and not X?

    Check yourself After adding a $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.

    Check yourself In the reproduction, Verilator reports 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.

    Remember

    • A blocking assignment in a clocked block that another block reads is a race: each simulator may order it differently, and the chip has no order at all.

    • Synthesis ignores sensitivity lists and delays, and an ASIC cannot honour initial values. The netlist is what the chip does; simulate it when RTL runs disagree.

    • Rule out X first: a two-state simulator and a four-state one also disagree on unknown values.

    Rule of thumb: if two simulators or the RTL and the netlist disagree, suspect the RTL's ordering before the tools.

    How this was checked

    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.

    Keep going

    • Practice: Blocking vs non-blocking in sequential logic

    • Practice: = and <=: correct use and the bugs misuse causes

    • Practice: Incomplete case and simulation vs synthesis mismatches

    • Practice: Race conditions without clocking blocks

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • RTL: blocking vs non-blocking, race-aware reasoning

    • Synthesis: simulation-synthesis mismatch

    • Differential debugging

    Previous

    43 / 107

    Next: Latches and Combinational Loops

    Up next

    Up nextLatches and Combinational Loops

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • CDC Bugs