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. RTL Simulation

Part 6 · Debug by Stage · Foundation (F)

RTL Simulation

RTL simulation is the stage where debugging is cheapest: every signal, every cycle, any run repeated. What that visibility gives, what it hides (time, X and the world around the design), the method of Parts 1 to 4 in one table, and a real run where zero-delay simulation misses a glitch.

On this page

    Why this matters

    Most bugs are found and fixed in RTL simulation, because there every signal is visible and every run repeats exactly. Engineers who forget that it simulates a model of the chip, with no time, a tidy idea of X and a bench for a world, trust a clean regression that cannot see the bug.

    After this lesson you can: use RTL simulation's full visibility with the whole debug loop, name the three things it hides, and recognise a failure that RTL simulation cannot show and must move to a later stage.

    Note

    Level: Foundation (F) · Debug-loop step: The whole loop

    Before this: The Debug Loop, The Cost of a Bug by Stage

    Every signal visible, and still the wrong answer

    A small block has a 2-bit counter and a status flag. Software sets the flag; the block clears it when the counter reaches 3. The RTL regression passes, and the waveform shows exactly that, cycle after cycle. In the lab the flag is sometimes already clear when software reads it at count 2.

    You had every signal in the design, at every cycle, and nothing was wrong. What could RTL simulation not have shown you?

    What people usually do, and why it fails

    Some engineers treat a clean, fully visible RTL run as the truth about the chip, so a lab failure must be the lab's fault: a bad board, a marginal part, a driver bug. Others go the opposite way and lose faith in simulation altogether, and start debugging on the bench, where they can see almost nothing.

    Both are wrong in the same way. RTL simulation is exact about one thing, the model you wrote, and blind to whatever that model leaves out. Knowing what it leaves out tells you when a clean run proves something and when it cannot.

    What RTL simulation gives you

    No later stage comes close to this, which is why the rest of Part 6 is about moving failures back here:

    • Full visibility. Every signal and every memory word, at every cycle, in any run. Nothing has to be planned in advance; you can dump or probe whatever the question needs.

    • Exact repeats. The same seed, commit and simulator give the same run, cycle for cycle (determinism and seeds). An experiment can be rerun with one change and nothing else.

    • Cheap changes. A probe, an assertion or a fix costs a recompile of seconds, not a re-spin or an FPGA build of hours.

    • Two simulators. Icarus and Verilator read the same RTL differently in places, and a disagreement is itself a clue (differential debugging).

    The method here is the whole loop

    Parts 1 to 4 taught the loop on RTL simulation, because it is the stage where every step is affordable. The short version, with where each step lives:

    The debug loop. In RTL simulation every step is cheap; the highlighted steps are where its visibility pays most.

    Step

    In RTL simulation

    Where it is taught

    Reproduce

    one command with a seed, a commit and a simulator; shrink the test and the design

    The reproduction checklist

    Observe

    probes, logs, assertions at the point of use, X checks

    Reading a waveform with a question

    Localise

    walk back to the first wrong cycle; bisect in time, space, stimulus and history

    The localisation checklist

    Hypothesise and Test

    a prediction you have not seen yet, one experiment, one change at a time

    A hypothesis must predict

    What RTL simulation hides

    RTL simulation runs your model in an idealised world. Three things are missing from that world, and each one has a lesson in this course that shows it with a real run:

    It leaves out

    So it cannot show

    Shown in

    Time. Every wire and gate is instant; registers change exactly on the edge.

    setup and hold failures, multicycle paths the design does not keep, glitches on asynchronous controls

    Timing-only bugs, and the run below

    Real start-up values. Unreset state is X in a four-state simulator, 0 in a two-state one; an if on X takes the else branch.

    a bug that needs a register to power up at 1; a bug hidden by X optimism

    X in a waveform, gate-level simulation

    The world around the design. The bench is a model: ideal clocks, a tidy software sequence, no metastability, no noise, no temperature.

    failures that need real clock relationships, real firmware timing or a marginal supply

    When simulation cannot reproduce it

    A fourth gap sits between the model and the netlist: synthesis may read the RTL differently from the simulator. That is covered in races and simulation-synthesis mismatches.

    The picture: a glitch that zero delay cannot make

    The count edge from 1 to 2. With zero delay, cnt[1] rises as cnt[0] falls and the decode of 3 never fires. With bit 0 arriving 300 ps late, both bits are 1 for 300 ps, clr_n pulses low and the flag clears.

    Read the bottom group: from the clock edge until bit 0 arrives, the decoder sees b1 = 1 and the old b0 = 1, which is the code for 3. The pulse is 300 ps wide, and an asynchronous clear does not care how short it is.

    Do it: the same block, with and without wire delay

    wrap_flag decodes "count is 3" with an AND gate and drives the flag's asynchronous reset with it. With WIRE_PS = 0 it is ordinary RTL. With WIRE_PS = 300, counter bit 0 reaches the decoder 300 ps after bit 1, which stands in for the routing of a real chip. The bench sets the flag while the count is 0 and checks it at count 2, three laps, for both copies.

    wrap_flag.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
    `timescale 1ns/1ps
    // wrap_flag: a 2-bit counter and a sticky flag. The flag must clear only when the counter is 3.
    // The clear is decoded combinationally and drives the flag's asynchronous reset (the bug).
    // WIRE_PS = 0 is RTL simulation: every wire is instant. WIRE_PS > 0 models the wire delays of
    // the real circuit: counter bit 0 reaches the decoder WIRE_PS later than bit 1.
    module wrap_flag #(parameter int WIRE_PS = 0) (
      input  logic       clk, rst_n, set,
      output logic [1:0] cnt,
      output logic       flag
    );
      logic b0, b1, clr_n;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) cnt <= '0;
        else        cnt <= cnt + 2'd1;
      if (WIRE_PS == 0) begin : g_rtl
        assign b0 = cnt[0];
        assign b1 = cnt[1];
      end else begin : g_wire
        assign #(WIRE_PS * 1ps) b0 = cnt[0];             // the slower wire
        assign b1 = cnt[1];
      end
      assign clr_n = rst_n & ~(b1 & b0);                 // bad: async clear from decode logic
      always_ff @(posedge clk or negedge clr_n)
        if (!clr_n)   flag <= 1'b0;
        else if (set) flag <= 1'b1;
    endmodule
    
    // tb: three laps of the counter. The flag is set while cnt is 0, so it is 1 from cnt = 1;
    // at cnt = 2 it must still be 1. It may clear only when cnt reaches 3.
    module tb;
      logic clk = 1'b0, rst_n = 1'b0, set = 1'b0;
      logic [1:0] cnt_r, cnt_w;
      logic flag_r, flag_w;
      int early_r = 0, early_w = 0;
      always #5 clk = ~clk;
      wrap_flag #(.WIRE_PS(0))   u_rtl  (.clk, .rst_n, .set, .cnt(cnt_r), .flag(flag_r));
      wrap_flag #(.WIRE_PS(300)) u_wire (.clk, .rst_n, .set, .cnt(cnt_w), .flag(flag_w));
      always @(negedge flag_r) if (rst_n) $display("%6.1f ns  rtl : flag cleared, cnt=%0d", $realtime, cnt_r);
      always @(negedge flag_w) if (rst_n) $display("%6.1f ns  wire: flag cleared, cnt=%0d", $realtime, cnt_w);
      initial begin
        @(negedge clk) rst_n = 1'b1;
        repeat (12) begin
          set = (cnt_r == 2'd0);
          if (cnt_r == 2'd2) begin
            $display("%6.1f ns  cnt=2: rtl flag=%b  wire flag=%b", $realtime, flag_r, flag_w);
            if (!flag_r) early_r++;
            if (!flag_w) early_w++;
          end
          @(negedge clk);
        end
        if (early_r != 0 || early_w != 3) $fatal(1, "the lesson's claims do not hold (rtl %0d, wire %0d)", early_r, early_w);
        $display("PASS: zero-delay RTL keeps the flag until cnt=3; with 300 ps of wire skew it clears at cnt 1->2 in every lap");
        $finish;
      end
    endmodule
    terminal
    $ iverilog -g2012 -o g.vvp glitch.sv && vvp -n g.vvp
    Terminal
      25.0 ns  wire: flag cleared, cnt=2
      30.0 ns  cnt=2: rtl flag=1  wire flag=0
      35.0 ns  rtl : flag cleared, cnt=3
      65.0 ns  wire: flag cleared, cnt=2
      70.0 ns  cnt=2: rtl flag=1  wire flag=0
      75.0 ns  rtl : flag cleared, cnt=3
     105.0 ns  wire: flag cleared, cnt=2
     110.0 ns  cnt=2: rtl flag=1  wire flag=0
     115.0 ns  rtl : flag cleared, cnt=3
    PASS: zero-delay RTL keeps the flag until cnt=3; with 300 ps of wire skew it clears at cnt 1->2 in every lap

    Read it in pairs of lines. The RTL copy clears the flag at 35 ns, on the edge where the count becomes 3, which is the specification. The wire copy clears it at 25 ns, on the edge where the count goes from 1 to 2, and at 30 ns software would read 0 where the RTL reads 1. It happens in every lap, at the same edge: deterministic, not random. Verilator 5.048 prints the same lines.

    Nothing in the RTL run was hidden from view. There simply was no glitch to see, because zero-delay simulation cannot produce one: b1 and b0 change in the same instant. The bug lives in a gap of the model, not in a signal you forgot to look at.

    The fix, and the check that catches it earlier

    Clear the flag synchronously, on the clock, from the decode of the count before it: if (cnt == 2'd2) flag <= 1'b0; else if (set) flag <= 1'b1; inside an ordinary clocked block with the real reset, so it clears on the edge where the count becomes 3, as specified. The decode can glitch as much as it likes between edges; only its settled value at the edge counts, and static timing checks that it settles in time. The rule that finds the original before any simulation is structural: no asynchronous reset or clock driven from combinational logic. Reset-domain and CDC checkers flag it; the glitchy reset release lesson covers the family.

    What the evidence says

    The RTL model was correct by its own rules, so every RTL test passed and the waveform agreed with the specification. The failure needed time, which the model does not have. On the debug loop, this is the Reproduce step at a later stage: when a lab failure meets a clean RTL regression, the question is not "what did the waveform miss" but "which gap could hold this bug", and the answer decides the next instrument: a delay model, gate-level simulation with timing, or the lab.

    Traps

    Common pitfall

    "The waveform shows it is fine." A waveform shows the model. A clean waveform rules out bugs inside the model, not bugs in time, start-up values or the world around it.

    Common pitfall

    Adding delays to RTL to "see" timing. A #1 on an assignment changes the model, not the chip, and real timing depends on the netlist and its layout. Use delays as a deliberate model of a hypothesis, as above, labelled as such, and move to gate level with a delay file to check the real thing.

    Why this matters

    Senior lens. When a bug report arrives from a later stage, start by writing down which of the three gaps it could live in. A failure that changes with clock frequency is in time; one that changes between power cycles is in start-up values; one that follows a board, a cable or a temperature is in the world. Each points at a different way to bring it back into simulation, and Part 6's last lesson is about doing that on purpose.

    Interview

    Asked: "RTL simulation gives you full visibility. Why do chips still fail with bugs that RTL simulation missed?"

    Weak answer: Because the tests did not cover the case. With better coverage, RTL simulation would find everything.

    Strong answer: Coverage is one reason, but even a perfect test only checks the model. RTL simulation is zero-delay, so it cannot show setup or hold failures or glitches, for example on a decoded asynchronous reset. It has an idealised view of unknown values: four-state simulation lets an if on X take the else branch, two-state simulation starts everything at 0, and synthesis may choose something else again. And the bench is a model of the world, with ideal clocks and no metastability, noise or real firmware timing. So when silicon disagrees with a clean RTL run I ask which of those gaps the bug could be in, and pick the stage that can show it.

    Follow-ups: (1) How would you reproduce a suspected glitch in simulation? (2) Which of these gaps does gate-level simulation close?

    Check yourself A designer argues that the decoded clear is safe because "the counter is a Gray code, so only one bit changes at a time". When is that argument right, and what does it not cover?

    With a Gray-coded counter, each step changes one bit, so the decode of a single value cannot pass through a false code between two steps: that argument is right for the counter's own transitions. It does not cover other inputs to the decoder, a counter that is loaded or reset to a new value (several bits change at once), or a decoder whose own gates glitch. A synchronous clear is safe whatever the encoding.

    Check yourself Which of RTL simulation's three gaps would you suspect for each: (a) a failure only on the first boot after power-up, (b) a failure that disappears at a lower clock frequency, (c) a failure only with one vendor's memory part?

    (a) Start-up values: some register without reset powers up to a value no simulation started from. (b) Time: a path too slow for the clock, so a setup-type failure. (c) The world around the design: the bench models the memory with ideal timing, and the real part differs, perhaps within its datasheet.

    Remember

    • RTL simulation shows everything the model does. It cannot show what the model leaves out: wire and gate delays, real start-up values, the real world around the design.

    • Zero delay means no glitches. Logic that is only safe without glitches, like a decoded asynchronous reset, passes every RTL test.

    • When the chip disagrees with a clean RTL run, ask first which of the three gaps the bug could live in.

    Rule of thumb: debug in RTL simulation whenever it can show the bug, and know the three things it cannot show.

    How this was checked

    glitch.sv as shown, 2026-10-18: iverilog -g2012 -o g.vvp glitch.sv && vvp -n g.vvp (Icarus Verilog 13.0) and verilator --binary --timing --assert -Wno-fatal --top-module tb glitch.sv (Verilator 5.048): identical output; the zero-delay copy clears the flag at 35, 75 and 115 ns (cnt = 3); the copy with WIRE_PS = 300 clears it at 25, 65 and 105 ns (the 1 to 2 edge) and reads flag = 0 at cnt = 2 in all three laps; PASS.

    The other limits are each shown, with real runs, in the lessons linked from the table: X optimism (dbglab B4 and a small design), timing (a multicycle path with a delay model), and the environment (a synchroniser model).

    Limits: the wire delay is a behavioural assign # on one signal, a model of skew, not an extracted netlist with a delay file. No CDC or reset lint tool is installed, so the rule that catches this is described, not run.

    Keep going

    • Practice: D flip-flop with asynchronous reset: glitch-free reset

    • Practice: FSM reset synchronisation and glitch prevention

    • Code lab: DFF with asynchronous reset

    • Code lab: Reset synchroniser

    • Interview: run a mock interview on this topic

    Related topics

    • Part 6 · Debug by Stage

    • When simulation cannot reproduce it

    • Gate-level simulation

    • RTL: debug with waveforms

    Previous

    56 / 107

    Next: Formal Counterexamples as Traces

    Up next

    Up nextFormal Counterexamples as Traces

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Gate-Level Simulation
    • Equivalence Failures
    • Emulation and FPGA Prototyping