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. Reset-Domain Crossing Bugs

Part 5 · Bug Classes · Senior (S)

Reset-Domain Crossing Bugs

Reset-domain crossing bugs: an asynchronous reset in one domain corrupting a register in another, and resets released in the wrong order. Both happen on a single clock, pass a bench that resets everything together, and are reproduced here with hostile reset timing, then fixed with reset isolation and release sequencing.

On this page

    Why this matters

    Reset bugs show up at boot, after a watchdog or a soft reset, as a hang or a corrupted register that nobody can reproduce, because benches reset every domain at once. They cost lab weeks and are invisible to a CDC check when the domains share a clock.

    After this lesson you can: recognise a reset-domain crossing from a failure tied to a reset event, reproduce an asynchronous-assertion corruption and a release-order hang in simulation, and fix them with isolation and sequencing.

    Note

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

    Before this: CDC Bugs, Making a Flaky Test Fail

    The board that hangs on one boot in two

    A subsystem has two reset domains on one clock: a responder that firmware can reset alone, and an always-on block that mirrors some of its registers. Two reports arrive in one week. After a soft reset of the responder, the always-on block sometimes holds a value nobody wrote. On a new board revision, nearly half of all cold boots hang waiting for the responder's first answer.

    The CDC report is clean: there is only one clock. Every simulation passes. What do both reports have in common?

    What people usually do, and why it fails

    They look for a logic bug in the mirror and the boot handshake, find none, and run more boot simulations, which pass because the bench releases all resets in the same cycle. Then they blame the new board. Nobody asks what each domain is doing while the other one's reset changes.

    The signature

    Bug

    What you see

    When

    Asynchronous assertion into another domain

    a register outside the reset holds a value that is neither the old value nor the reset value, or an FSM jumps; X at gate level

    only around a reset of the other domain, at a rate that depends on where in the cycle the reset lands

    Release out of order

    a hang or a lost first transaction right after boot; one side waits for a response the other never saw

    only for some boots: those where the requester leaves reset first

    Release not synchronised

    flops in one domain leave reset in different cycles: a partial reset state

    boot only; follows reset timing and temperature on silicon

    The common clue: the failure is tied to a reset event, not to traffic. On the debug loop this is Localise and Hypothesise: if you can line the failure up with a reset, the suspect list is the reset tree. The third row is the reset synchroniser's job, covered in reset deassertion; this lesson reproduces the first two.

    The first three checks

    1. Draw the reset domains. For every reset, list the flops it resets and every flop outside it that samples one of them. Each such path is a reset-domain crossing, whether or not the clocks differ.

    2. Line the failure up with reset events. Log every reset assertion and release with its time. Does the corruption appear within a cycle of a reset of the other domain? Does the hang follow one release order?

    3. Make reset hostile in the bench. Assert each reset at random points in the cycle with a metastability model on the receiving flops, and release resets in random order. A bug that appears only then is a reset-domain bug.

    The picture: the release that came too early

    Bug 2. B leaves reset in cycle 5 and pulses its boot request; A is still in reset until cycle 6, so its pending flag never sets and B waits for ever.

    One real reproduction: both bugs on one clock

    Everything runs on one 10 ns clock. In bug 1, mirror has a register in domain A with an asynchronous reset and a copy in domain B, built from the CDC lesson's meta_ff. A soft reset of A arrives at a random point in the cycle. In bug 2, req_resp has a requester in B that sends one request pulse after reset and a responder in A; the two resets are released by their own synchronisers at clock edges, 1 to 8 cycles after the start, in either order.

    meta_ff.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
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    `timescale 1ns/1ps
    // meta_ff: a flop with a hostile-simulation model of metastability (as in the CDC lesson):
    // a bit of D that changed less than WIN ns before the edge resolves to old or new at random.
    module meta_ff #(parameter int W = 1, parameter int SALT = 0, parameter real WIN = 0.5) (
      input  logic         clk, meta_en,
      input  logic [31:0]  seed,
      input  logic [W-1:0] d,
      output logic [W-1:0] q
    );
      logic [W-1:0] d_q = '0, old_v = '0, cap;
      real          t_chg [W];
      logic [31:0]  rng;
      bit           seeded = 1'b0;
      initial begin
        q = '0;
        for (int i = 0; i < W; i++) t_chg[i] = -100.0;
      end
      always @(d) begin
        for (int i = 0; i < W; i++)
          if (d[i] !== d_q[i]) begin t_chg[i] = $realtime; old_v[i] = d_q[i]; end
        d_q = d;
      end
      always @(posedge clk) begin
        cap = d;
        for (int i = 0; i < W; i++)
          if (meta_en && $realtime - t_chg[i] < WIN) begin
            if (!seeded) begin rng = seed * 32'h9E37_79B9 + 32'(SALT) * 32'h85EB_CA6B + 32'h7F4A_7C16; seeded = 1'b1; end
            rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
            cap[i] = rng[0] ? d[i] : old_v[i];
          end
        q <= cap;
      end
    endmodule
    
    // Bug 1: a register in reset domain A (async reset rst_a_n) feeds a register in domain B,
    // which is never reset by rst_a_n. Same clock: no clock-domain crossing anywhere.
    // FIX = 0: B samples A every cycle. FIX = 1: B holds while iso_b is high (reset isolation).
    module mirror #(parameter bit FIX = 0, parameter int SALT = 0) (
      input  logic       clk, rst_a_n, meta_en, iso_b, we,
      input  logic [31:0] seed,
      input  logic [7:0] wdata,
      output logic [7:0] cfg_a, cfg_b
    );
      logic [7:0] d_b;
      always_ff @(posedge clk or negedge rst_a_n)
        if (!rst_a_n) cfg_a <= 8'h00;
        else if (we)  cfg_a <= wdata;
      assign d_b = (FIX && iso_b) ? cfg_b : cfg_a;
      meta_ff #(.W(8), .SALT(SALT)) u_b (.clk, .meta_en, .seed, .d(d_b), .q(cfg_b));
    endmodule
    
    // Bug 2: a requester in domain B sends one request pulse right after its reset ends; a responder
    // in domain A must answer it. FIX = 0: nothing orders the two releases.
    // FIX = 1: B also waits until A says it is out of reset (a_ready), so B can never start first.
    module req_resp #(parameter bit FIX = 0) (
      input  logic clk, rst_a_n, rst_b_n,
      output logic done, waiting
    );
      logic req, ack, pend, sent, a_ready;
      always_ff @(posedge clk or negedge rst_a_n)         // responder, domain A
        if (!rst_a_n) begin pend <= 1'b0; ack <= 1'b0; a_ready <= 1'b0; end
        else begin
          a_ready <= 1'b1;
          ack     <= pend;
          pend    <= req ? 1'b1 : (ack ? 1'b0 : pend);
        end
      always_ff @(posedge clk or negedge rst_b_n)         // requester, domain B
        if (!rst_b_n) begin sent <= 1'b0; done <= 1'b0; end
        else begin
          if (!sent && (!FIX || a_ready)) sent <= 1'b1;   // one pulse: the boot request
          if (ack) done <= 1'b1;
        end
      assign req     = rst_b_n && !sent && (!FIX || a_ready);
      assign waiting = sent && !done;
    endmodule
    

    Bug 1: the soft reset that corrupts its neighbour

    Terminal
    $ iverilog -g2012 -o r.vvp rdc_tb.sv
    $ vvp -n r.vvp
    bug 1, model off: 0 of 400 resets corrupt B (bad), 0 (fix)
    bug 1, model on:  15 of 400 resets corrupt B (bad), 0 (fix)
    bug 2, resets together:  0 of 200 boots hang (bad), 0 (fix)
    bug 2, random order:     90 of 200 boots hang (bad), 0 (fix)
    PASS: both reset-domain bugs need hostile reset timing to show; both fixes pass
    rdc_tb.sv:200: $finish called at 368005000 (1ps)
    $ vvp -n r.vvp +BAD=1
    FAIL time=3685.1 ns: domain B holds 06; A held 47, and its reset value is 00
    FAIL time=7585.1 ns: domain B holds 05; A held 75, and its reset value is 00
    ...
    FATAL: rdc_tb.sv:174: bug 1: 13 of 400 runs failed
           Time: 60005000  Scope: rdc_tb.$unm_blk_14

    Zero-delay simulation: 400 soft resets, B always holds either A's value or 00, a clean pass. With the model on, 15 of 400 resets leave B holding something else. Read the first failure: A held 47 (0100 0111) and was reset to 00. B holds 06 (0000 0110): bits 6 and 0 took the new value, bits 2 and 1 kept the old one. The reset hit A's flops just before B's clock edge, and B captured a mix. That is one reset in 27 here; a real flop's window is far narrower, so on silicon it would be rarer and much harder to catch. The fixed mirror holds its value while iso_b is high, so A's reset never reaches a D pin that B is sampling: 0 of 400.

    Bug 2: the boot that hangs

    Terminal
    $ vvp -n r.vvp +BAD=2 +SAME
    PASS: buggy design, bug 2 did not show
    rdc_tb.sv:180: $finish called at 123995000 (1ps)
    $ vvp -n r.vvp +BAD=2
    FAIL time=1235.0 ns: boot request unanswered after 50 cycles (A released at cycle 6, B at cycle 5)
    FAIL time=3715.0 ns: boot request unanswered after 50 cycles (A released at cycle 8, B at cycle 7)
    FAIL time=4955.0 ns: boot request unanswered after 50 cycles (A released at cycle 4, B at cycle 1)
    ...
    FATAL: rdc_tb.sv:174: bug 2: 83 of 200 runs failed
           Time: 123995000  Scope: rdc_tb.$unm_blk_14

    With +SAME, the way most benches reset, all 200 boots pass. With independent release times, 83 of 200 hang, and in every one of the 83 B was released before A. That is a prediction you can check before reading the RTL: with each release drawn from 1 to 8, B goes first in 7/16 of boots, 87.5 of 200, and the runs gave 83 and 90. The requester sent its only pulse while the responder was held in reset. The fixed requester waits for a_ready, a flag A sets when it leaves reset, and none of the 200 boots hang. Verilator 5.048 prints the same lines for both bugs.

    dbglab: how its bench resets

    Terminal
    $ sed -n 312,314p tb/dbglab_tb.sv
        repeat (4) tick();
        rst_n = 1'b1;
        @(posedge pclk) #1 prst_n = 1'b1;
    $ sed -n 19p rtl/periph_pd.sv
      periph_sink u_sink (.pclk(pclk), .prst_n(prst_n && pwr_on), .en(en), .rdata(rdata), .rempty(rempty),

    dbglab's bench releases its two resets, rst_n and prst_n, once and in a fixed order, so its regression cannot see this class. Check 1 does flag one structure: the sink's reset is prst_n && pwr_on, released by a bus-clock register write rather than on a pclk edge; it models a supply switch, and in silicon it would need a reset synchroniser. None of B1 to B5 is a reset-domain bug.

    What the evidence says

    Both failures need reset timing the default bench never produces: an assertion mid-cycle, or releases in different cycles. Each then fails on demand at a rate the mechanism predicts, and each fix passes the same hostile runs. The board's two reports are now two named crossings with fixes.

    The fix pattern

    • Asynchronous assertion: reset isolation. Stop B sampling A (here iso_b makes B hold), then assert and release A's reset, let A settle, and remove isolation (reset isolation). Or reset B too, if its state need not survive.

    • Release order: sequence the releases (the responder first) or make the requester wait for a ready flag from the responder, as here. See release order across domains.

    • Unsynchronised release: release every asynchronous reset through a reset synchroniser clocked by its domain (asynchronous assert, synchronous deassert).

    Preventing it

    • A structural RDC check, which lists every path from a flop with one reset to a flop with another, same clock or not; each needs isolation, sequencing or a written reason. Reading RDC findings covers the report.

    • Reset-aware tests: random reset assertion times during traffic, random release order, and a soft reset of each domain alone (reset sequences and random resets).

    • Assertions: "while A is in reset, B's copy does not change"; "no request is issued while the responder's ready is low".

    • Review question: "When this reset asserts, who is still running? When it releases, who might already be talking to it?"

    Traps

    Common pitfall

    A fix that is itself a crossing. The fixed requester reads a_ready, which A's reset clears asynchronously. At boot both domains are in reset, so it is safe. If A can be soft-reset alone, a_ready dropping is one more asynchronous change into B and needs the same isolation as bug 1.

    Common pitfall

    "Same clock, so no crossing." The CDC report was clean because there is one clock. Reset-domain crossings need their own check. Bug 1 above is a crossing between two flops on the same clock.

    Why this matters

    Senior lens. Write the reset architecture down as a table before RTL: every reset, its source, its synchroniser, which domains it resets, who must be isolated before it asserts, and the release order. Then the RDC check has something to compare with, and the bench knows which orders to randomise. And when a board revision moves the rate of a boot hang, suspect the reset tree: a new board changes reset timing, not logic.

    Interview

    Asked: "After a soft reset of one block, a neighbouring block sometimes holds a corrupt register value. Both are on the same clock. What is going on?"

    Weak answer: Probably a glitch on the reset line, or the neighbour is not handling the reset properly. I would add a reset to the neighbour as well.

    Strong answer: That is a reset-domain crossing. The soft reset is asynchronous, so it changes the first block's flops at an arbitrary time in the cycle, and a neighbour flop that samples them, outside that reset, can capture a mix or go metastable, even on the same clock. I would confirm by lining the corruption up with the reset events and by simulating random reset assertion times with a metastability model on the neighbour's flops. The fix is reset isolation: stop the neighbour sampling before the reset asserts, and resume after release. Adding the reset to the neighbour also works, if losing its state is acceptable.

    Follow-ups: (1) Why does a CDC tool not report it? (2) What changes if the reset is released in a different order from the one the bench uses?

    Check yourself A bench resets all domains with one signal and has never failed. A new test resets only the DMA engine in the middle of a transfer, and the memory controller hangs. Which check do you run first?

    Line the hang up with the reset event: if it always follows the DMA reset, the suspect is a crossing from the DMA's reset domain into the memory controller, such as a request or a pending count that the controller samples and that the DMA reset clears asynchronously, or a handshake left half-finished. Then draw the reset domains and list every path from DMA-reset flops into the controller, and check each one for isolation.

    Check yourself In bug 2, someone suggests making the request a level instead of a pulse, held until it is answered. Does that fix the release-order bug?

    Yes for this case: a level held until acknowledged is still there when A leaves reset, which is why four-phase handshakes survive release-order differences that one-cycle pulses do not. Still check that A does not act on a request in its first cycle out of reset, before its own state is initialised.

    Remember

    • An asynchronous reset changes its flops at any time in the cycle. Any flop outside that reset that samples them is a crossing, even on the same clock.

    • Isolate before you reset: hold the receiving side, then assert, release, wait, and only then let it sample again.

    • Benches that release every reset together cannot see release-order bugs. Randomise which domain leaves reset first.

    Rule of thumb: for every reset in the design, ask who is still running when it asserts, and who is ready when it releases.

    How this was checked

    rdc_tb.sv as shown, 2026-10-18: iverilog -g2012 -o r.vvp rdc_tb.sv and vvp -n r.vvp (Icarus Verilog 13.0, 0.26 s): bug 1, 0 of 400 resets corrupt B with the model off and 15 of 400 with it on (fix 0); bug 2, 0 of 200 boots hang with the resets released together and 90 of 200 in random order (fix 0); PASS. +BAD=1: 13 of 400 fail; +BAD=2: 83 of 200, all with B released before A; +BAD=2 +SAME passes; +FIX=1 and +FIX=2 pass. Seeds 1 to 5: bug 1, 6 to 17 of 400; bug 2, 78 to 105 of 200; fixes 0.

    verilator --binary --timing --assert -Wno-fatal --top-module rdc_tb (Verilator 5.048) prints the same lines for the default run, +BAD=1 and +BAD=2. dbglab at HEAD 49c5f04: sed -n 312,314p tb/dbglab_tb.sv and sed -n 19p rtl/periph_pd.sv, output as shown.

    Limits: the metastability model's 0.5 ns window is exaggerated and says nothing about silicon rates; reset release here is modelled at clock edges, as from a reset synchroniser. No RDC tool is installed, so no structural report is shown.

    Keep going

    • Practice: Reset domain crossing: synchronous deassert and orchestration

    • Practice: Reset release races across domains

    • Practice: Reset sequencing: randomised stagger between resets

    • Code lab: Reset domain crossing

    • Code lab: Reset synchronizer

    • Code lab: RDC reset check

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • CDC and RDC: reset domain crossing

    • CDC and RDC: reset debug playbook

    • RTL: reset domain crossing

    • CDC and RDC: the reset release bug

    Previous

    46 / 107

    Next: Timing-Only Bugs

    Up next

    Up nextTiming-Only Bugs

    Sign in to save progress

    Practice this lessonReport an error

    See also

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