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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
    • Reset Architecture
    • Metastability and Synchronisation
    • Asynchronous FIFO
    • CDC Protocols
    • Reset Domain Crossing
      • RDC Is CDC Plus Reset Asymmetry
      • Reset Isolation Pattern
      • Quiescent State Before Reset Release
      • RDC Signoff Checklist
      • Recovery, Removal and Verification Focus
    • CDC Verification
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

←Reset Domain Crossing
  1. Topics
  2. RTL Design
  3. Reset & CDC
  4. Reset Domain Crossing
  5. Reset Isolation Pattern
On this page

    Part 6 of 9, lesson 24 of 34Part 6 of 9/Reset & CDC/Lesson 24 of 34Intermediate

    Reset Isolation Pattern

    Data retention and stale-request suppression with an explicit reset-recovery contract.

    A stale ack can survive a source reset and complete the next request, so a source that returns to IDLE and accepts a word can deliver the wrong data. Isolation cells and protocol tolerance each leave a gap on their own.

    After this lesson you can: make a handshake tolerate the other side's reset with an armed flag and a link_ready interlock, and place isolation cells with the right control.

    Make every side robust to the other's reset

    Coordinated resets are simplest, but some blocks cannot reset together. Three rules then make a crossing survive the other side's reset: keep the crossing data, arm the receiver, and block new traffic until recovery is complete.

    Data retention and an armed receiver reduce reset damage, but they do not start a new handshake epoch by themselves. A stale ack can survive a source reset: a source that returns to IDLE and accepts another word lets that old ack complete the new request. The source below therefore also needs link_ready_s from the reset controller before it accepts any word.

    Problem

    Rule

    Change

    Source reset puts 0 on the data bus (stale word 0)

    Don't reset data that crosses; reset only control state

    Remove data <= '0 from the source reset

    Destination reset leaves a pending request that gets captured again (duplicate)

    Acknowledge an old request without capturing it

    A destination armed flag: after reset it is 0 until req has been seen low once; while not armed, a request is acknowledged but not delivered

    Stale request/ack history survives a reset

    Block acceptance until recovery completes

    link_ready_s is low throughout reset/recovery, and high only after both sides and synchroniser pipelines are idle

    The reset-controller contract

    link_ready_s is synchronous to clk_s. The controller that drives it follows five steps for every reset:

    1. Clear link_ready_s before a planned reset. An unexpected reset must also clear it before any new acceptance.

    2. Stop new traffic. Hold any destination reset until the pending req has reached its synchroniser (at least three clean destination edges in this model).

    3. Release the resets synchronously, each in its own domain.

    4. Let the old handshake finish or cancel.

    5. Raise link_ready_s only when req, req_sync, ack and ack_sync are low, the source is IDLE and the destination is armed.

    In hardware this needs synchronised recovery status, or an epoch handshake, from an always-on controller. Seeing ack low once, or a fixed timer in the source domain, is not enough. If either clock stops, keep the link closed. The listing below implements only the acceptance interlock; the recovery-controller pilot at the end of the page sketches the controller itself.

    Isolation cells

    An isolation cell is a gate (AND or OR) on each output of a block held in reset or powered down. An isolate signal clamps the outputs to a safe constant.

    Isolate asserts before the block resets and releases after it is stable again.

    Diagram
    assign out_to_neighbour = block_out & ~isolate;     // clamp to 0 while isolated
    // isolate asserted before the block resets, released after it has left reset and is quiescent

    Combine both: isolation at block boundaries (hardware), and protocol tolerance inside crossings (as above). Either alone leaves a gap: isolation does not fix a protocol that was in the middle of a transfer, and tolerance does not remove a glitch.

    Isolation control must itself come from a domain that is not being reset. An isolate signal from the block being isolated defeats the purpose.

    Check yourself Why does the destination acknowledge a stale request instead of just ignoring it?

    The source is still waiting for an acknowledge to complete its handshake. If the destination ignored the request, the source would wait forever (deadlock). Acknowledging without capturing completes the handshake and cancels the word.

    Testbench: the protected handshake

    The protected handshake under the same random reset experiment. The task reopen_link stands in for the controller: it raises link_ready_s only after three qualified idle samples.

    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
    76
    77
    78
    79
    80
    `timescale 1ns/1ps
    
    module meta_ff #(parameter real TCQ = 0.25, TSU = 0.3, THD = 0.15) (input logic clk, d, output logic q);
      realtime t_d = -100.0, t_clk = -100.0;
      initial q = 1'b0;
      always @(d) begin
        t_d = $realtime;
        if ($realtime - t_clk < THD) q <= #TCQ $urandom_range(0, 1);
      end
      always @(posedge clk) begin
        t_clk = $realtime;
        if ($realtime - t_d < TSU) q <= #TCQ $urandom_range(0, 1);
        else                       q <= #TCQ d;
      end
    endmodule
    
    module sync2 (input logic clk, d, output logic q);
      logic q1;
      meta_ff f1 (.clk, .d,      .q(q1));
      meta_ff f2 (.clk, .d(q1),  .q);
    endmodule
    
    // ---------------- 4-phase request/acknowledge transfer of one word ----------------
    module hs_src_recover #(parameter int W = 8) (
      input  logic         clk, rst_n, link_ready_s,
      input  logic         send_valid,
      output logic         send_ready,
      input  logic [W-1:0] send_data,
      output logic         req,
      output logic [W-1:0] data,
      input  logic         ack_sync
    );
      typedef enum logic [1:0] {IDLE, WAIT_ACK, WAIT_NACK} state_t;
      state_t state;
      assign send_ready = rst_n && link_ready_s && (state == IDLE);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin state <= IDLE; req <= 1'b0; end  // retain data during recovery
        else unique case (state)
          IDLE:      if (send_valid && send_ready) begin data <= send_data; req <= 1'b1; state <= WAIT_ACK; end
          WAIT_ACK:  if (ack_sync)  begin req <= 1'b0; state <= WAIT_NACK; end
          WAIT_NACK: if (!ack_sync) state <= IDLE;
          default:   state <= IDLE;
        endcase
    endmodule
    
    module hs_dst #(parameter int W = 8) (
      input  logic         clk, rst_n,
      input  logic         req_sync,
      input  logic [W-1:0] data,
      output logic         ack,
      output logic         out_valid,
      output logic [W-1:0] out_data
    );
      logic armed;       // 0 after reset: a request that was already pending is acknowledged but NOT captured again
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin ack <= 1'b0; out_valid <= 1'b0; out_data <= '0; armed <= 1'b0; end
        else begin
          out_valid <= 1'b0;
          if (req_sync && !ack) begin
            ack <= 1'b1;
            if (armed) begin out_data <= data; out_valid <= 1'b1; end    // normal: capture and deliver
          end else if (!req_sync) begin
            ack   <= 1'b0;
            armed <= 1'b1;                                               // the old handshake has completed: new requests are real
          end
        end
    endmodule
    
    module hs_cdc_recover #(parameter int W = 8) (
      input  logic clk_s, rst_s_n, clk_d, rst_d_n, link_ready_s,
      input  logic send_valid, input logic [W-1:0] send_data, output logic send_ready,
      output logic out_valid, output logic [W-1:0] out_data
    );
      logic req, ack, req_sync, ack_sync;
      logic [W-1:0] data;
      hs_src_recover #(W) s (.clk(clk_s), .rst_n(rst_s_n), .link_ready_s, .send_valid, .send_ready, .send_data, .req, .data, .ack_sync);
      sync2 u_req (.clk(clk_d), .d(req), .q(req_sync));       // request crosses to the destination clock
      hs_dst #(W) d (.clk(clk_d), .rst_n(rst_d_n), .req_sync, .data, .ack, .out_valid, .out_data);
      sync2 u_ack (.clk(clk_s), .d(ack), .q(ack_sync));       // acknowledge crosses back
    endmodule
    

    Directed / random case

    Required result (all PASS)

    Source reset with old ack high

    No acceptance until qualified idle; subsequent traffic resumes

    Destination only (MODE = 0)

    No duplicate or stale word under the controller contract

    Source only (MODE = 1)

    No duplicate or stale word under the controller contract

    Both together (MODE = 2)

    No duplicate or stale word under the controller contract

    Run each MODE separately. MODE 0, 1 and 2 each pass over 1,500 resets (4,276, 4,699 and 4,371 words delivered). This experiment permits cancellation of an interrupted word; it checks no duplication/stale data and eventual progress, not lossless delivery across reset. An exactly-once contract requires transaction identity and replay/deduplication above this link.

    Recovery-controller pilot

    The random bench above uses an oracle for the controller. This separate pilot supplies a small always-on controller, recovery_supervisor, and a clocked reporter in each endpoint, recovery_endpoint. One recovery is a fresh low-high-low flush barrier: close both sides, raise flush and wait for both acknowledgements, drop flush and wait for both to fall, then reopen. Neither a stale low acknowledgement nor a reused timer can open a new epoch.

    The recovery_supervisor phases: a flush barrier from CLOSE through WAIT_HIGH and WAIT_LOW to OPEN, a timeout into FAULT, and recover_i as the only way out of OPEN or FAULT

    Read the diagram from the top: each arrow is one clock of clk_aon on which the condition, seen through the controller's two-flop synchronisers, is true.

    • The barrier needs both edges. WAIT_HIGH proves each endpoint saw this flush; WAIT_LOW proves each has finished it. A low ack left over from before the reset satisfies neither.

    • The timeout counts clk_aon cycles from CLOSE (LIMIT = 100). It covers the whole barrier, so a stopped endpoint clock ends in FAULT.

    • FAULT never reopens by itself: only recover_i leaves it, even after the stopped clock returns.

    • A synchronised fault_hold forces CLOSE from every phase (not drawn), and its unsynchronised copy closes each endpoint at once.

    Phase

    Required evidence

    CLOSE

    Both synchronised local open flags low, both old acknowledgements low

    WAIT_HIGH

    Each endpoint has observed flush and held its protocol block reset for four local edges

    WAIT_LOW

    Flush released; each reporter waits four local edges with idle/armed true before dropping acknowledgement

    OPEN

    Fresh barrier complete; open request synchronised into each endpoint

    FAULT

    Always-on watchdog expired; keep acceptance closed and require explicit retry after clocks recover

    The bench tb_recovery_barrier checks four cases: the first opening, a planned recovery that closes both sides before a fresh barrier, a stopped destination clock that must end in a closed FAULT, and an explicit retry that reopens once the clock is back.

    recovery_endpoint.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
    76
    77
    module recovery_endpoint (
      input logic clk, por_n, fault_hold, open_async, flush_async,
      input logic local_idle, local_armed,
      output logic link_open, block_reset, flush_ack
    );
      timeunit 1ns; timeprecision 1ps;
      (* ASYNC_REG = "TRUE" *) logic [1:0] open_sync, flush_sync;
      int clean_edges;
      // A held, approved fault-control path closes acceptance even with this clock stopped.
      always_ff @(posedge clk or negedge por_n or posedge fault_hold)
        if (!por_n || fault_hold) open_sync <= '0;
        else open_sync <= {open_sync[0], open_async};
      assign link_open = open_sync[1] && !fault_hold;
      always_ff @(posedge clk or negedge por_n)
        if (!por_n) flush_sync <= '0;
        else flush_sync <= {flush_sync[0], flush_async};
    
      // Reporter remains powered/clocked while its protocol block is reset.
      always_ff @(posedge clk or negedge por_n) begin
        if (!por_n) begin block_reset <= 1; flush_ack <= 0; clean_edges <= 0; end
        else if (flush_sync[1]) begin
          block_reset <= 1;
          if (!flush_ack) begin
            if (clean_edges == 3) begin flush_ack <= 1; clean_edges <= 0; end
            else clean_edges <= clean_edges + 1;
          end
        end else begin
          block_reset <= 0;
          if (flush_ack) begin
            if (!local_idle || !local_armed) clean_edges <= 0;
            else if (clean_edges == 3) begin flush_ack <= 0; clean_edges <= 0; end
            else clean_edges <= clean_edges + 1;
          end else clean_edges <= 0;
        end
      end
    endmodule
    
    module recovery_supervisor #(parameter int LIMIT = 100) (
      input logic clk_aon, por_n, fault_hold, recover_i,
      input logic open_s, open_d, ack_s, ack_d,
      output logic flush_req, open_request, faulted
    );
      timeunit 1ns; timeprecision 1ps;
      typedef enum logic [2:0] {CLOSE, WAIT_HIGH, WAIT_LOW, OPEN, FAULT} phase_t;
      phase_t phase;
      (* ASYNC_REG = "TRUE" *) logic [1:0] os, od, as_q, ad_q, fault_q;
      int waited;
      initial if (LIMIT < 1) $fatal(1, "Recovery LIMIT must be positive");
      always_ff @(posedge clk_aon or negedge por_n) begin
        if (!por_n) begin os<=0; od<=0; as_q<=0; ad_q<=0; fault_q<=0; end
        else begin
          os<={os[0],open_s}; od<={od[0],open_d};
          as_q<={as_q[0],ack_s}; ad_q<={ad_q[0],ack_d}; fault_q<={fault_q[0],fault_hold};
        end
      end
      assign flush_req = phase == WAIT_HIGH || phase == FAULT;
      assign open_request = phase == OPEN && !fault_hold;
      assign faulted = phase == FAULT;
      always_ff @(posedge clk_aon or negedge por_n) begin
        if (!por_n) begin phase <= CLOSE; waited <= 0; end
        else if (fault_q[1]) begin phase <= CLOSE; waited <= 0; end
        else if (recover_i && (phase == OPEN || phase == FAULT)) begin phase <= CLOSE; waited <= 0; end
        else if (phase != OPEN && phase != FAULT && waited >= LIMIT) phase <= FAULT;
        else begin
          if (phase == OPEN || phase == FAULT) waited <= 0;
          else waited <= waited + 1;
          case (phase)
            CLOSE: if (!os[1] && !od[1] && !as_q[1] && !ad_q[1]) phase <= WAIT_HIGH;
            WAIT_HIGH: if (as_q[1] && ad_q[1]) phase <= WAIT_LOW;
            WAIT_LOW: if (!as_q[1] && !ad_q[1]) phase <= OPEN;
            OPEN: begin end
            FAULT: begin end // no timer-based reopening
            default: phase <= FAULT;
          endcase
        end
      end
    endmodule
    

    Limits. The pilot assumes that reporters survive local protocol resets, that local idle/armed describe the actual link and its history, that a coordinated flush cancels in-flight transactions, that all clocks normally progress, and that fault_hold is a clean held fault control with an approved asynchronous assertion path. The four-edge flush suits this model's two-stage synchronisers; it is not universal. Unexpected-fault containment still needs a fault-detection latency budget: words accepted before closure must be cancelled and accounted, and synchronised flags do not detect a remote reset immediately. The bench uses abstract idle/armed endpoints, so it does not connect the full handshake or prove cancellation or exactly-once delivery. Integration must derive local status, keep data until cancellation is authorised, synchronise reset release, constrain recovery/removal and isolation, and attach transaction or epoch accounting.

    • Do not reset data that crosses. A destination that is not armed acknowledges an old request without capturing it.

    • Block acceptance until recovery completes, using link_ready_s from an always-on controller.

    • Isolate asserts before the block resets and releases after it is stable. The isolate control must come from a domain that is not being reset.

    Rule of thumb: use both: isolation at block boundaries and tolerance inside the crossing.

    How this was checked

    Protected handshake under the random reset experiment, run with Icarus Verilog 13.0 (2026-10-16): MODE 0, 1 and 2 each print PASS over 1,500 resets (4,276, 4,699 and 4,371 words delivered). The experiment permits cancellation of an interrupted word. It checks no duplication, no stale data and eventual progress, not lossless delivery across reset.

    The recovery-controller pilot is a standalone bench with abstract idle and armed endpoints. It does not connect the full handshake or prove cancellation or exactly-once delivery. No simulation or physical signoff is claimed for this source.

    • Practice: Debugging metastability from asynchronous reset deassertion

    • Interview: run a mock interview on this topic

    Related topics

    • Quiescent state before reset release

    Previous

    178 / 350

    Next: Quiescent State Before Reset Release

    Up next

    Up nextQuiescent State Before Reset Release

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • RDC Signoff Checklist
    • Recovery, Removal and Verification Focus