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

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
    • Senior Debug Playbook
      • Failure Triage Method
      • Waveform Debug Strategy
      • Debugging the Testbench
      • Hunting X's and Races
    • Reviewing Verification Code
    • Verification Strategy: Spec to Sign-Off
    • SystemVerilog Interview Q&A Bank
    • Capstone: Verify a Parameterized FIFO
  • Principal-Level Verification28
Loading lesson

‹Senior Debug Playbook
  1. Topics
  2. SystemVerilog
  3. Senior & Interview Preparation
  4. Senior Debug Playbook
  5. Hunting X's and Races

Part 8 · Senior & Interview Prep · Intermediate

Hunting X's and Races

First-X back-propagation, reset audits, race protocol, and a gate-level X walkthrough.

On this page

    Back-propagate to the first X

    An X on an output is the last car of a train. Find when the signal first went X, ask which input was X then, and repeat until the origin.

    Diagram
    X BACK-PROPAGATION
    
      output X @ 5,200 ns
         │ when did it FIRST go X?           ◄── jump-to-previous-edge, not scroll
         ▼
      first X @ 3,100 ns ── which input was X then? ── pipe_q was X
         │
         ▼
      pipe_q first X @ 3,000 ns ── input? ── ctrl_reg was X
         │
         ▼
      ctrl_reg X SINCE TIME 0  ◄── ORIGIN
         │
         ▼ origin checklist (one of these, nearly always):
         · flop never reset (missing from reset list / wrong reset domain)
         · RAM read before first write
         · undriven input port (TB never drove it / wrong modport)
         · multi-driver conflict resolving to X on a wire
         · deliberate X injection reached live data

    The origin is nearly always on that short list. The wave below shows the signature: an X present from the very first slot.

    Watch cfg_en, data, gated, and out. The X sits at slot 0, which already names the origin.

    • Slot 0: cfg_en X, data 0, gated 0, out 0. The enable is already X; zero data masks it.

    • Slot 1: data 1 arrives at this edge, so gated goes X. The flop sampled the old 0, so out is still 0.

    • Slot 2: the next edge captures the X: out reads X from here on.

    • Slot 3: unchanged. Nothing downstream can repair an origin this early.

    • Slot 4: unchanged. The fix belongs at cfg_en, not at out.

    • List every state element on the failure path and confirm which reset domain owns it.

    • Check reset duration: a flop on a divided clock may never see an active edge during a short pulse.

    • Memories are usually unreset by design, so verify read-before-write protection instead.

    • Beware two-state variables in the bench masking X values the RTL produces.

    Check yourself An output goes X at 5,200 ns but its driver went X at 3,100 ns. Where do you continue?

    At 3,100 ns, asking which input was X then. Debug the first X, never the one you saw.

    Prove the origin with a tiny bench

    The X listing below is not run in this course's checks. The course runner tries Verilator first, and Verilator models two states, so the X never appears and the checks fail. It passes under Icarus (iverilog -g2012), which models four states. The runnable bench after it shows the same origin-trace idea with validity flags.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // not-run: needs four-state X; Verilator 5.048 (tried first by the runner) ties undriven logic to 0 so $isunknown never fires; passes under Icarus
    module tb_x_origin;
      timeunit 1ns; timeprecision 1ps;
      logic clk = 0;
      always #5 clk = ~clk;
      logic cfg_reset = 0;   // properly reset model
      logic cfg_missing;     // never driven: stands in for the unreset flop
      logic mask = 1;
      wire leak;
      assign leak = cfg_missing & mask;
      int errors = 0;
      initial begin
        #1;                                         // settle time 0
        if (!$isunknown(cfg_missing)) errors++;     // origin is X since time 0
        if (leak !== 1'bx) errors++;                // X fans out through live logic
        if (cfg_reset !== 0) errors++;
        @(posedge clk); #1;
        if (!$isunknown(cfg_missing)) errors++;     // still X after a clock
        if (errors == 0) $display("PASS: unreset flop reads X since time 0 and leaks through live logic");
        else $fatal(1, "FAIL: X-origin check");
        $finish;
      end
    endmodule
    design.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
    module tb_origin_trace;
      timeunit 1ns; timeprecision 1ps;
      logic clk = 0;
      always #5 clk = ~clk;
      // Two-state version of the X hunt: each value carries a validity flag.
      logic [7:0] cfg_good = 8'hA5;
      bit valid_good = 1;        // reset path validates its register
      logic [7:0] cfg_missing = 8'h00;
      bit valid_missing = 0;     // never validated: the unreset flop
      bit data_valid = 0;        // live data arrives later
      wire [7:0] gated;
      assign gated = cfg_missing & {8{cfg_good[0]}};
      // Two pipeline stages. Each flop's flag is the AND of its input flags.
      bit s1_ok = 0, s2_ok = 0;      // path fed by the validated register
      bit s1_bad = 0, s2_bad = 0;    // path fed by the unvalidated origin
      always_ff @(posedge clk) begin
        s1_ok  <= data_valid & valid_good;
        s2_ok  <= s1_ok;
        s1_bad <= data_valid & valid_missing;
        s2_bad <= s1_bad;
      end
      int errors = 0;
      initial begin
        #1;
        if (valid_missing !== 0) errors++;   // origin flagged since time 0
        @(negedge clk);
        data_valid = 1;                      // live data arrives
        @(posedge clk); #1;
        if (s1_ok !== 1 || s2_ok !== 0) errors++;   // clean flag reaches stage 1
        if (s1_bad !== 0) errors++;                 // tainted flag stays invalid
        @(posedge clk); #1;
        if (s2_ok !== 1) errors++;                  // clean flag reaches stage 2
        if (s2_bad !== 0) errors++;                 // invalidity traced to stage 2
        if (gated !== 8'h00) errors++;              // data path stays deterministic
        if (errors == 0) $display("PASS: unvalidated origin flagged since time 0 and traced through two live stages");
        else $fatal(1, "FAIL: origin-trace check");
        $finish;
      end
    endmodule

    The flag version runs on every engine and mirrors the method. The clean path turns valid stage by stage, while the path fed by the unvalidated origin never does: the origin is the stage whose validity was never set. Two-state mirrors in a real bench would mask a true X as zero, which is why the audit checklist above matters.

    Races shift under observation

    A race announces itself when behavior-neutral changes move the failure: a print, a flag, a tool version. Then debug event ordering, not data.

    • Classic bench-side cause: driving inputs with blocking assignments from a clocked block. Same-edge sampling order turns tool-dependent. Fix with nonblocking drives, or a clocking block at that interface, which removes the same-edge bench-to-DUT race there.

    • Clocking blocks sample in Preponed and drive in re-NBA at that interface. Races persist across clock domains, skewed signals, and non-clocked access.

    • Zero-delay loops through a shared variable: order undefined by the standard, stable per tool, broken on upgrade.

    • A print that moves the failure is a detector, not a fix. It reorders Active-region evaluation just enough to flip the race.

    1. Pin everything: seed, tool version, flags, arguments.

    2. Diff two identical-input runs to the first divergent line; that names the racing pair.

    3. Force both orderings deliberately and confirm the failure flips on and off.

    4. Fix structurally, remove the ordering hack, and confirm stability.

    race_dut.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
    module race_dut (
      input logic clk,
      input logic d,
      output logic q
    );
      logic q_r = 0;
      always_ff @(posedge clk) q_r <= d;
      assign q = q_r;
    endmodule
    
    module tb_drive_order;
      timeunit 1ns; timeprecision 1ps;
      logic clk = 0, d = 0, q;
      always #5 clk = ~clk;
      race_dut dut (.*);
      int errors = 0;
      initial begin
        @(negedge clk);
        d <= 1;                    // setup-respecting drive: settled before the edge
        @(posedge clk); #1;
        if (q !== 1) errors++;     // captured cleanly
        @(posedge clk);
        #1 d = 0;                  // blocking change after the edge: too late to sample
        if (q !== 1) errors++;     // flop still holds the sampled 1
        @(posedge clk); #1;
        if (q !== 0) errors++;     // the 0 lands on the next edge
        if (errors == 0) $display("PASS: setup drive captured at the edge, late drive waited a cycle");
        else $fatal(1, "FAIL: drive-ordering check");
        $finish;
      end
    endmodule

    The bench flips the ordering deliberately: a setup-respecting drive captures cleanly, a post-edge change waits a cycle. No scheduler luck is involved.

    Common pitfall

    Treating a print that moves the failure as a fix. It is race evidence, so find the racing pair instead.

    Interview

    Likely question: “How do clocking blocks help race debug?” Answer with Preponed sampling and re-NBA drive at that interface, plus what still races: domains, skews, and non-clocked access.

    Check yourself Two runs with identical inputs diverge at one timestamp. What does that name?

    A racing pair at that timestamp. Diff to the first divergent line, then flip the ordering deliberately to prove it.

    Walkthrough: the X that only appeared at gate level

    A design passed RTL regression, then X-stormed at gate level. Triage said X signature with divergence at the first post-reset cycle: initialization, not logic.

    1. Symptom: gate-level X-storm with a fully green RTL regression.

    2. Back-propagation: output X, datapath enable X, configuration register X since time 0.

    3. Origin check: the register was reset by an initial block, not a reset term.

    4. Fix: a real reset term in the RTL, plus a review item against initial-block resets.

    5. Lesson banked: simulation forgives what gates do not.

    Scope note: this rule targets ASIC gate-level correctness, since FPGAs honor power-up initial values. The hunt method itself is identical at both levels.

    Check yourself Gate-level X-storm, green RTL, divergence at the first post-reset cycle. Initialization or logic?

    Initialization. Divergence from cycle one points at reset or init, and initial-block resets do not survive synthesis for ASIC.

    Related topics

    • Failure Triage Method

    Previous

    214 / 259

    Next: Reviewing Verification Code

    Up next

    Up nextReviewing Verification Code

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Failure Triage Method
    • Waveform Debug Strategy
    clkcfg_endatagatedout
    An unreset enable fans X out one cycle after live data arrives