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
  • Part 6 · Debug by Stage9
    • RTL Simulation
    • Formal Counterexamples as Traces
    • Gate-Level Simulation
    • Equivalence Failures
    • Emulation and FPGA Prototyping
    • The First Failing Test on Silicon
    • Silicon vs Simulation Correlation
    • Marginal and Environmental Failures
    • Moving the Failure to the Cheapest Stage
  • 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 6 · Debug by Stage
  1. Topics
  2. The Art of Hardware Debug
  3. Part 6 · Debug by Stage
  4. Gate-Level Simulation

Part 6 · Debug by Stage · Senior (S)

Gate-Level Simulation

What gate-level simulation adds to RTL simulation: it runs what synthesis built, including the choices synthesis made for X, and real cells handle X differently from RTL. A small block shows a bug visible only in the netlist and an X that is only pessimism, and how to tell them apart.

On this page

    Why this matters

    RTL simulation runs what you wrote; the chip runs what synthesis built, and the two can differ wherever the RTL left a value unknown. Gate-level simulation is the first place that difference shows, but only engineers who can tell a real netlist bug from X pessimism get anything from it.

    After this lesson you can: run the RTL bench on a netlist, explain a gate-level failure from the synthesis log, and decide with random start values whether a gate-level X is a real bug or pessimism.

    Note

    Level: Senior (S) · Debug-loop step: Observe, Localise

    Before this: RTL: Proving an ECO with Equivalence and GLS, X in a Waveform

    The netlist counts before anyone asked it to

    A small timer block passes its RTL regression. Before tape-out the team runs the same bench on the synthesised netlist for the first time. Two things come back: the timer counts from the first cycle after reset, before software has loaded it, and a status read returns 0x0x where the RTL returned 0101.

    The RTL has not changed and its tests still pass. Is one of these a real bug, are both, or is the netlist simulation just being pessimistic?

    What people usually do, and why it fails

    The common reflex is to call every gate-level difference "X pessimism" and make it go away by initialising every flop to 0. The run goes clean, and both findings disappear, including the real one. The opposite reflex is to debug the netlist net by net, through names like _072_, skipping the cheapest answers: the synthesis log and the RTL.

    Gate-level simulation is not a slower RTL simulation. It asks what synthesis built, and how real cells handle unknown values. With a delay file it also checks timing, the subject of timing-only bugs; there is none here, so this lesson is about function and X.

    The method: what GLS adds, and how to read it

    • Run the same bench on both. The RTL run is the reference. The first cycle where the netlist differs is the place to start, exactly as in comparing a good and a bad run.

    • Sort each difference. A wrong known value means synthesis built something the RTL simulation did not show. An X the RTL did not have means the cells propagate unknowns differently. They need different checks.

    • For a wrong value, read the synthesis log for that register. Synthesis is allowed to choose any value for anything the RTL leaves unknown, and the log says what it chose.

    • For an X, find its source flop and try both values. Start it at 0 and at 1 (random start values do this). If an output changes, the X is real; if it never does, it is pessimism. Never blanket-initialise: that simulates one particular chip and hides every bug that needs the other value.

    On the debug loop this is Observe and Localise at a later stage: the same first-difference search, on names and structure chosen by the tool.

    The picture: one mux, three models

    The status read mux with an unknown select and both inputs at 1: the RTL ternary and the mux cell give 1, the simple-gate version gives X

    Verilog's ? : with an unknown select keeps the bits on which both choices agree, so the RTL and the mux cell give 1. Built from gates, each half is X & 1, which is X, and X OR X is X. The chip reads 1 for either value of sel_q; the gate model is right not to know sel_q, and wrong to conclude it does not know rd.

    Do it: one block, one bench, three runs

    blk has two registers without reset. en_q is the timer enable, set by load; leaving it without reset is the bug, and FIX = 1 gives it one. sel_q picks which of two status registers a read returns; it has no reset either, but both registers reset to IDLE, so a read before the first swap is IDLE whatever sel_q is. The bench checks that the timer does not count before load and warns if a read is not IDLE.

    blk.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
    60
    61
    62
    63
    64
    // blk: a small timer block with two registers that have no reset.
    //  en_q  : the timer enable; software sets it with load. FIX = 0 leaves it without reset (the bug).
    //  sel_q : which of two status registers a read returns; no reset. Both status registers reset
    //          to IDLE, so a read before the first swap returns IDLE whatever sel_q powered up as.
    module blk #(parameter bit FIX = 0) (
      input  logic       clk, rst_n,
      input  logic       load, swap,
      input  logic [3:0] wdata,
      output logic [3:0] cnt, rd
    );
      localparam logic [3:0] IDLE = 4'b0101;
      logic       en_q, sel_q;
      logic [3:0] st0_q, st1_q;
      if (FIX) begin : g_en
        always_ff @(posedge clk or negedge rst_n)
          if (!rst_n)    en_q <= 1'b0;
          else if (load) en_q <= 1'b1;
      end else begin : g_en
        always_ff @(posedge clk)
          if (load) en_q <= 1'b1;                       // bug: no reset
      end
      always_ff @(posedge clk)
        if (swap) sel_q <= ~sel_q;                      // no reset: harmless, both choices are IDLE
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          cnt <= '0; st0_q <= IDLE; st1_q <= IDLE;
        end else begin
          if (en_q) cnt <= cnt + 4'd1;
          if (load) begin st0_q <= wdata; st1_q <= ~wdata; end
        end
      assign rd = sel_q ? st1_q : st0_q;
    endmodule
    // tb: the timer must not count before software loads it, and a read before the first swap
    // returns IDLE. The same bench runs the RTL and the netlists (+define+NET selects blk_net).
    `timescale 1ns/1ps
    module tb;
      logic clk = 1'b0, rst_n = 1'b0, load = 1'b0, swap = 1'b0;
      logic [3:0] wdata = 4'h3, cnt, rd;
      int errs = 0;
      always #5 clk = ~clk;
    `ifdef NET
      blk_net dut (.clk, .rst_n, .load, .swap, .wdata, .cnt, .rd);
    `else
      blk #(.FIX(`ifdef FIX 1 `else 0 `endif)) dut (.clk, .rst_n, .load, .swap, .wdata, .cnt, .rd);
    `endif
      initial begin
        repeat (2) @(posedge clk);
        #1 rst_n = 1'b1;
        $display("start: en_q=%b sel_q=%b", dut.en_q, dut.sel_q);
        for (int c = 1; c <= 4; c++) begin               // four cycles before software loads it
          @(posedge clk); #1;
          $display("cycle %0d  cnt=%b  rd=%b", c, cnt, rd);
          if (cnt !== 4'd0) begin errs++; $display("FAIL cycle %0d: cnt=%b before load, expected 0000", c, cnt); end
          if (rd !== 4'b0101) $display("WARN cycle %0d: rd=%b before the first swap, expected 0101", c, rd);
        end
        load = 1'b1; @(posedge clk); #1 load = 1'b0;
        repeat (3) @(posedge clk); #1;
        $display("after load: cnt=%0d", cnt);
        if (cnt !== 4'd3) begin errs++; $display("FAIL: cnt=%0d three cycles after load, expected 3", cnt); end
        if (errs) $fatal(1, "%0d errors", errs);
        $display("PASS: the timer waits for load, then counts");
        $finish;
      end
    endmodule

    The listing is two files, blk.sv and tb.sv. Two netlists are made from the buggy RTL. The first uses Yosys's default generic cells, which include a mux and enable flip-flops. The second is mapped to plain flip-flops and simple gates only, standing in for a library with no mux cell on this path. Both are simulated with Yosys's own cell models, simcells.v, as in the RTL course's ECO lesson.

    terminal
    1
    2
    3
    4
    $ yosys -q -p "read_verilog -sv blk.sv; chparam -set FIX 0 blk; synth -flatten -top blk; rename blk blk_net; write_verilog -noattr -noexpr net_dflt_fix0.v"
    $ yosys -q -p "read_verilog -sv blk.sv; chparam -set FIX 0 blk; synth -flatten -top blk; dfflegalize -cell \$_DFF_P_ x -cell \$_DFF_PN0_ x; abc -g AND,NAND,OR,NOR,XOR,XNOR,ANDNOT,ORNOT; opt_clean; rename blk blk_net; write_verilog -noattr -noexpr net_gates_fix0.v"
    $ iverilog -g2012 -o rtl.vvp tb.sv blk.sv && vvp -n rtl.vvp
    $ iverilog -g2012 -DNET -o n.vvp tb.sv net_dflt_fix0.v $(yosys-config --datdir)/simcells.v && vvp -n n.vvp

    RTL: clean

    Terminal
    start: en_q=x sel_q=x
    cycle 1  cnt=0000  rd=0101
    cycle 2  cnt=0000  rd=0101
    cycle 3  cnt=0000  rd=0101
    cycle 4  cnt=0000  rd=0101
    after load: cnt=3
    PASS: the timer waits for load, then counts

    Both registers start unknown, and the RTL hides both. if (en_q) with an X condition takes the else path, so the counter holds: the X optimism of an if. The ternary gives 0101 because both choices agree.

    Netlist with Yosys's default cells: a wrong value, not an X

    Terminal
    start: en_q=1 sel_q=x
    cycle 1  cnt=0001  rd=0101
    FAIL cycle 1: cnt=0001 before load, expected 0000
    cycle 2  cnt=0010  rd=0101
    FAIL cycle 2: cnt=0010 before load, expected 0000
    ...
    after load: cnt=8
    FAIL: cnt=8 three cycles after load, expected 3
    FATAL: tb.sv:28: 5 errors

    The first line already says it: in the netlist, en_q is 1 before anything has written it. The timer counts from cycle 1 and has reached 8 by the end. This is not X propagation; the netlist simply has no unknown here. The synthesis log says why:

    Terminal
    3.10.6. Executing OPT_DFF pass (perform DFF optimizations).
    ...
    Adding EN signal on $procdff$43 ($dff) from module blk (D = 1'1, Q = \en_q).
    Setting constant 1-bit at position 0 on $auto$ff.cc:337:slice$47 ($dffe) from module blk.
    $ grep "assign en_q" net_dflt_fix0.v
      assign en_q = 1'h1;

    en_q has no reset and no initial value, and the only value ever written to it is 1. Its start value is unknown, so synthesis may take it to be 1, and then it never changes: a constant, with no warning, because nothing illegal happened. The RTL simulation chose "holds" for the unknown; synthesis chose 1, and a chip built from this netlist counts from reset every time. This is a simulation-synthesis mismatch caused by an X, and only the netlist run shows it.

    Yosys's sequential equivalence would not catch it either: equiv_make, equiv_simple and equiv_induct on this RTL and netlist prove all 18 compare points, because a flop with an unknown start value may equal the constant 1. A production LEC flow would list en_q as a register removed as constant, a line in a report that is easy to waive. The netlist is a legal implementation of RTL that is wrong.

    Netlist of simple gates: the same bug, plus an X

    Terminal
    start: en_q=1 sel_q=x
    cycle 1  cnt=0001  rd=0x0x
    FAIL cycle 1: cnt=0001 before load, expected 0000
    WARN cycle 1: rd=0x0x before the first swap, expected 0101
    ...
    FATAL: tb.sv:28: 5 errors

    The timer fails in the same way. The read is new: the bits where IDLE is 1 are X, and the bits where it is 0 are 0. That is the picture above, bit by bit. (X & 0) | (X & 0) is 0, so only the 1 bits go unknown.

    Real or pessimism: try both start values

    Fix the timer (FIX = 1, a reset on en_q) and rebuild both netlists. The default-cell netlist passes with rd=0101. The simple-gate netlist passes its checks, but the warning stays:

    Terminal
    start: en_q=0 sel_q=x
    cycle 1  cnt=0000  rd=0x0x
    WARN cycle 1: rd=0x0x before the first swap, expected 0101
    ...
    after load: cnt=3
    PASS: the timer waits for load, then counts

    Is that X a bug? The test is to run the netlist as real chips would start: sel_q at 0 in some runs, at 1 in others. Verilator with --x-initial unique gives every unreset flop a random value per seed; it is a two-state simulator, so no X can appear. The same treatment on the buggy RTL shows what a real dependence looks like (the bench's start: line prints the start values each seed drew):

    terminal
    1
    2
    3
    4
    5
    6
    7
    8
    $ verilator --binary --timing -Wno-fatal -Wno-lint -Wno-style --x-initial unique -DNET --top-module tb \
        tb.sv net_gates_fix1.v $(yosys-config --datdir)/simcells.v -Mdir obj_g1      # fixed netlist, simple gates
    $ verilator --binary --timing -Wno-fatal -Wno-lint -Wno-style --x-initial unique --top-module tb \
        tb.sv blk.sv -Mdir obj_r0                                                     # buggy RTL
    $ for s in 1 2 3 4 5 6; do echo "net fix seed $s: $(obj_g1/Vtb +verilator+rand+reset+2 +verilator+seed+$s \
        | grep -E "start|cycle 1 |PASS|FAIL" | head -3 | tr '\n' ' ')"; done
    $ for s in 1 2 3 4 5 6; do echo "rtl bug seed $s: $(obj_r0/Vtb +verilator+rand+reset+2 +verilator+seed+$s \
        | grep -E "start|cycle 1 |PASS|FAIL" | head -3 | tr '\n' ' ')"; done
    Terminal
    net fix seed 1: start: en_q=0 sel_q=0 cycle 1  cnt=0000  rd=0101 PASS: the timer waits for load, then counts
    net fix seed 2: start: en_q=0 sel_q=0 cycle 1  cnt=0000  rd=0101 PASS: the timer waits for load, then counts
    net fix seed 3: start: en_q=0 sel_q=1 cycle 1  cnt=0000  rd=0101 PASS: the timer waits for load, then counts
    ...
    net fix seed 6: start: en_q=0 sel_q=1 cycle 1  cnt=0000  rd=0101 PASS: the timer waits for load, then counts
    rtl bug seed 1: start: en_q=0 sel_q=0 cycle 1  cnt=0000  rd=0101 PASS: the timer waits for load, then counts
    rtl bug seed 3: start: en_q=1 sel_q=1 cycle 1  cnt=0001  rd=0101 FAIL cycle 1: cnt=0001 before load, expected 0000
    ...
    rtl bug seed 6: start: en_q=1 sel_q=1 cycle 1  cnt=0001  rd=0101 FAIL cycle 1: cnt=0001 before load, expected 0000

    In the fixed netlist, sel_q starts at 0 in three seeds and at 1 in three, and rd is 0101 in all six: no chip could read anything else. The X was pessimism. In the buggy RTL, the result follows en_q's start value: seeds 3, 4 and 6 start it at 1 and fail. That X was hiding a real bug. One flop has only two start values, so here six seeds cover both; a block with many unreset flops needs many seeds, or a formal check, to cover the combinations that matter.

    For the pessimistic X there are two honest choices: a waiver with this evidence ("rd does not depend on sel_q until the first swap"), or a reset on sel_q so every later run is clean. On a control bit the reset is usually the better trade.

    What the evidence says

    Run

    Timer before load

    Read before swap

    What it means

    RTL, bug

    0 (holds)

    0101

    Optimism hides the missing reset

    Netlist, default cells, bug

    counts 1, 2, 3 ...

    0101

    Synthesis made en_q the constant 1: a real bug

    Netlist, simple gates, bug

    counts 1, 2, 3 ...

    0x0x

    The same bug, plus gate pessimism

    Netlist, simple gates, fix

    0

    0x0x

    Pessimism only: same read for either sel_q

    Netlist, default cells, fix

    0

    0101

    Clean

    Traps

    Common pitfall

    Initialising every flop to clear the X. It would make the simple-gate netlist look clean, and with en_q started at 0 the buggy RTL would pass too. The bug needs en_q = 1, which is the value synthesis chose. Initialise nothing globally; check each X source.

    Common pitfall

    "Equivalence passed, so gate-level must match the RTL run." A sequential equivalence check like Yosys's compares the netlist with what the RTL allows, and an unknown start value allows anything. It passed here while the netlist failed the RTL bench. The RTL lesson's limits of LEC and GLS table lists what each one asks. Read the constant-register and unmapped-point reports, not only the verdict.

    Common pitfall

    Debugging generated names. _072_ means nothing to you. Start from the RTL register the failing output depends on (here en_q), search the synthesis log and the netlist for that name, and only then follow cells.

    Why this matters

    Senior lens. Plan gate-level runs as a small, targeted set: reset and power-up, the start-up of every block with unreset state, mode switches, and anything an ECO touched. Run each with the default X handling and with random start values over a few seeds, and keep the reviewed X waivers next to the netlist version they apply to. Upstream, a lint rule that flags unreset registers feeding an enable, a select or a state machine removes most of the work.

    Interview

    Asked: "Your gate-level simulation shows X on an output where RTL simulation was clean. How do you decide whether to fix it or waive it?"

    Weak answer: Gate-level simulation is known to be pessimistic, so if RTL is clean I would initialise the flops to zero in the netlist and rerun. If it passes, I waive it.

    Strong answer: First I find which flop the X comes from, usually an unreset register, by tracing back from the first cycle the output differs from the RTL run. Then I check whether the output depends on that flop's start value: I run with it at 0 and at 1, for example with random start values over several seeds. If the result changes, it is a real bug the RTL was hiding through X optimism. If it never changes, it is pessimism, and I either add a reset or waive it with that evidence. I also check that the netlist computed known values the same way as the RTL, because synthesis may have turned an unknown into a constant, and I read the synthesis log for that register.

    Follow-ups: (1) Why can gate-level simulation be both more and less pessimistic than RTL? (2) Why did the equivalence check pass on a netlist that fails the bench?

    Check yourself A designer argues that en_q does not need a reset because "software always writes load before using the timer". What does the netlist run say about that argument?

    The argument assumes en_q starts at 0 and waits for software. Without a reset it starts at an unknown value, and synthesis was free to choose 1, which turned the flop into a constant: the timer runs from reset no matter what software does. A register that gates behaviour before software's first write needs a reset; "software writes it first" only works for registers nothing reads before that write.

    Check yourself With mux cells the read was clean; with simple gates it was X. Which netlist tells you more about the chip?

    Neither alone. Both are the same logic and the chip reads 0101 either way, which only the two-value runs proved. The mux-cell model happened to be optimistic in the same way as the RTL; the simple-gate model was pessimistic. What the chip does is settled by trying both start values, not by which cell model you simulated.

    Remember

    • An unknown start value is a free choice for synthesis. It may pick a constant, so the netlist can do something no RTL run showed.

    • Gate-level X depends on the cell models: a mux cell can be as optimistic as RTL, and AND-OR gates more pessimistic than the chip.

    • Real or pessimism: start the X source at 0 and at 1. If the outputs differ, the X is real; if they never do, it is pessimism.

    Rule of thumb: never clear gate-level X by initialising every flop; find each X's source and prove it harmless or fix it.

    How this was checked

    blk.sv and tb.sv as shown, 2026-10-18. RTL: iverilog -g2012 -o rtl.vvp tb.sv blk.sv && vvp -n rtl.vvp (Icarus 13.0): PASS. Netlists (Yosys 0.69+post): yosys -q -p "read_verilog -sv blk.sv; chparam -set FIX 0 blk; synth -flatten -top blk; rename blk blk_net; write_verilog -noattr -noexpr net_dflt_fix0.v", and the simple-gate netlist with dfflegalize -cell $_DFF_P_ x -cell $_DFF_PN0_ x; abc -g AND,NAND,OR,NOR,XOR,XNOR,ANDNOT,ORNOT; opt_clean after synth. Each run as iverilog -g2012 -DNET -o n.vvp tb.sv <netlist> $(yosys-config --datdir)/simcells.v && vvp -n n.vvp.

    Results: both FIX = 0 netlists fail with cnt=0001 at cycle 1 (5 errors), the simple-gate one also shows rd=0x0x; both FIX = 1 netlists pass, the simple-gate one with rd=0x0x warnings. yosys -p "...; synth -flatten -top blk" log, OPT_DFF pass: Setting constant 1-bit at position 0 on $auto$ff.cc:337:slice$47 ($dffe) from module blk.; the netlists contain assign en_q = 1'h1;. equiv_make/equiv_simple/equiv_induct/equiv_status -assert of the FIX = 0 RTL against net_dflt_fix0.v: 18 of 18 proven.

    Random start values (Verilator 5.048): verilator --binary --timing -Wno-fatal -Wno-lint -Wno-style --x-initial unique [-DNET] --top-module tb tb.sv <blk.sv | net_gates_fix1.v simcells.v>, run with +verilator+rand+reset+2 +verilator+seed+N, N = 1 to 6: FIX = 0 RTL fails for seeds 3, 4 and 6 (en_q started at 1) and passes for 1, 2 and 5; FIX = 1 simple-gate netlist passes all six with rd=0101, sel_q starting at 0 in three and 1 in three.

    Limits: zero-delay gate models (no SDF, no timing; see the timing-only bugs lesson), Yosys generic cells instead of a vendor library, and a simple-gate mapping chosen to stand in for a library without a mux cell. Six random seeds test two start values of one flop, which is all there are here; a real block needs every X source checked.

    Keep going

    • Practice: Gate-level simulation: functional-only semantics

    • Practice: Gate-level reset glitching

    • Practice: X-propagation and four-state races

    • Code lab: DFF with asynchronous reset

    • Code lab: Counter with enable

    • Interview: run a mock interview on this topic

    Related topics

    • Part 6 · Debug by Stage

    • RTL: Proving an ECO with equivalence and GLS

    • X and reset bugs

    • Timing-only bugs (gate-level with timing)

    Previous

    58 / 107

    Next: Equivalence Failures

    Up next

    Up nextEquivalence Failures

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • RTL Simulation
    • Emulation and FPGA Prototyping
    • The First Failing Test on Silicon