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

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
    • Reset Architecture
    • Metastability and Synchronisation
    • Asynchronous FIFO
    • CDC Protocols
      • Choose a Protocol by Traffic Shape
      • 4-Phase Request/Acknowledge
      • Pulse Synchronisers and Event Toggles
      • Mux Recirculation for Multi-Bit Capture
      • CDC Handshake Checklist
    • Reset Domain Crossing
    • CDC Verification
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹CDC Protocols
  1. Topics
  2. RTL Design
  3. Reset & CDC
  4. CDC Protocols
  5. Mux Recirculation for Multi-Bit Capture

Part 6 · Reset & CDC · Intermediate

Mux Recirculation for Multi-Bit Capture

Capture a held word with a synchronised load pulse instead of a handshake.

On this page

    Why this matters

    Holding a word still while a pulse crosses is cheaper than a handshake, but the hold time is only an assumption between clocks. If the destination period grows to 50 ns, a 70 ns hold no longer covers a capture near 160 ns.

    After this lesson you can: build a held-word crossing with a toggle pulse, derive the earliest-capture and overwrite budgets, and say when to use a handshake instead.

    Holding the word still

    The 4-phase handshake buys certainty with a round trip. When the source can wait long enough, a cheaper scheme works.

    The source parks the word in a holding register and sends a pulse through a toggle synchroniser. The destination captures the held word when the pulse arrives.

    The destination register is an ordinary enabled flop. It keeps its value unless the pulse arrives, which is the mux recirculation the lesson is named for.

    mux_recirc as a circuit: the word waits in hold_reg, a toggle carries the load pulse across, and pulse_d selects the held word into dout for one destination edge

    Read the top row as the data path and the bottom row as the control path; they meet only at the mux in front of dout.

    • The word never passes through a synchroniser. It is safe because hold_reg does not change while the destination may sample it.

    • Only the toggle crosses through flops. pulse_d is one clk_d cycle wide, so dout loads the held word exactly once.

    • When pulse_d is low the mux feeds dout back to itself. That loop is the recirculation; in RTL it is simply if (pulse_d) dout <= hold_reg;.

    Hold time and measured results

    The testbench moves 3,000 + 3,000 words (HOLD = 6 and HOLD = 20). Derive earliest capture and latest permitted overwrite separately before using this open-loop structure.

    Check

    Result

    Source clock 10 ns, destination 17.06 ns (slower)

    3,000 16-bit words, none corrupted or lost

    Source input bus changes right after each load

    Yes, the holding register protects the data

    Source reuse interval

    HOLD=6 permits overwrite after 7 source periods = 70 ns; see the edge derivation below

    Mutation: hold time of 1 cycle

    2,740 of 3,000 words corrupted

    Launch, capture and overwrite budgets

    Timeline from the accepted load (E0): hold_reg stays stable for 70 ns, the ideal capture window sits at 44.12 to 61.18 ns, and 8.82 ns remain before the next overwrite

    Read the timeline as one axis of time after E0. The top bar is how long the data holds still. The lower bar is when the destination can capture it.

    • The capture window (44.12 to 61.18 ns) lies inside the hold window (0 to 70 ns), so the data is still when it is sampled.

    • The earliest capture is Ts + 2*Td (D2) and the latest is Ts + 3*Td (D3). Each extra destination period a control deferral adds moves the capture later by 17.06 ns.

    • Only 8.82 ns remain after D3, for late control terms, hold and margin. A single extra destination period would push the capture past the overwrite at 70 ns.

    Event

    Edge / bound in this listing

    E0: accept load

    hold_reg and pulse_s update after source edge 0

    E1: launch toggle

    pulse_sync consumes the registered pulse_s one source period later

    D0 / D1 / D2

    First stage can sample at D0, second updates at D1, destination consumes pulse_d at D2; earliest capture is approximately Ts + 2*Td after E0

    Ideal latest capture

    Less than Ts + 3*Td + control clock-to-Q/route terms when D0 is the next sampling edge; extra model deferrals add destination periods

    Next overwrite

    Counter loads HOLD at E0, becomes zero after E_HOLD, next acceptance is E_(HOLD+1)

    Two distinct inequalities must hold. First, source data clock-to-Q + maximum data route + destination setup + margin must be less than the earliest control-driven capture interval. Second, the earliest next data change must occur after the latest permitted capture plus hold/margin. The output consumer observes registered out_valid on the following destination edge; that adds consumer latency, not an extra hold_reg capture edge. State bounded control deferrals explicitly; physical metastability has no deterministic finite ceiling.

    For Ts=10 ns and Td=17.06 ns, the ideal capture envelope is approximately 44.12 to 61.18 ns after E0, plus implementation terms. The next overwrite for HOLD=6 is at 70 ns, leaving less than 8.82 ns for late control terms, hold and margin; an extra destination-cycle deferral can exhaust it. HOLD=20 checks counter range, not an all-corners guarantee. A worked illustrative data budget of 30 ns including clock-to-Q/setup/margin fits before 44.12 ns, but must be constrained and measured per bit.

    Diagram
    # Illustrative only: adapt endpoints and tool delay accounting.
    # The complete data path, including clock-to-Q/setup/margin, must fit 44.12 ns.
    # Reserve 14.12 ns here; constrain the tool-counted datapath to 30 ns.
    set_max_delay 30.0 -datapath_only -from [get_cells dut/hold_reg*] -to [get_cells dut/dout_reg*]
    # Check all W data bits in effective reports; no broad clock-group/false-path override.
    # Derive a separate latest capture/overwrite bound or use acknowledgement.

    The hold time is a timing assumption between clocks, not something the protocol enforces. If the destination clock is slowed (for example by a power mode), the assumption can break.

    When the clock ratio is not fixed, use the 4-phase handshake. It adapts automatically.

    Warning

    The pulse_sync copy in this example has the same coordinated-reset contract as the pulse lesson: suppress load, reset both ends, flush at least three clean destination edges after toggle_s is zero, and release synchronously before resuming. Its behavioural synchroniser history has no reset. For independent resets or stopped clocks use a recovery handshake, not a HOLD timer.

    HOLD is a parameter of at least 1 source cycle; the counter is sized from it with $clog2(HOLD+1), so a hold of 16 or more (as the slow-clock exercise below needs) works, and an elaboration check rejects HOLD below 1. The testbench runs the default HOLD = 6 and a second instance with HOLD = 20, above 15, so a counter truncated to 4 bits would fail it.

    Common pitfall

    Use the ready output: the source may not load again until the hold counter expires. A producer that ignores ready overwrites the holding register while the destination may still be reading it.

    Tip

    This is an asynchronous held-data crossing: constrain each data path from hold_reg to dout against the earliest capture budget with an appropriate datapath-only bound. A same-clock multicycle recipe does not automatically describe unrelated clocks. See the canonical bundled-data contract.

    Check yourself The destination period becomes 50 ns. Is HOLD=6 at Ts=10 ns enough?

    No. Ideal latest capture approaches Ts + 3 × Td = 10 + 3 × 50 = 160 ns (Td is the 50 ns destination period) plus control delays, while overwrite is permitted at 70 ns. Select HOLD from (HOLD+1) × Ts exceeding the justified latest capture plus hold/margin, and also verify earliest data arrival. Use a handshake if clock/resolution bounds cannot be guaranteed.

    Testbench

    The holding register, toggle pulse and load logic with an order-checking testbench.

    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
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    `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
    
    // Pulse crossing by toggling (see the previous lesson)
    module pulse_sync (
      input  logic clk_s, rst_s_n, pulse_s,
      input  logic clk_d, rst_d_n,
      output logic pulse_d
    );
      logic toggle_s, q1, q2, q2_prev;
      always_ff @(posedge clk_s or negedge rst_s_n)
        if (!rst_s_n) toggle_s <= 1'b0; else if (pulse_s) toggle_s <= ~toggle_s;
      meta_ff f1 (.clk(clk_d), .d(toggle_s), .q(q1));
      meta_ff f2 (.clk(clk_d), .d(q1),       .q(q2));
      always_ff @(posedge clk_d or negedge rst_d_n)
        if (!rst_d_n) q2_prev <= 1'b0; else q2_prev <= q2;
      assign pulse_d = rst_d_n && (q2 ^ q2_prev);
    endmodule
    
    // Multi-bit transfer by "recirculating" data: the source holds the word still, a synchronised
    // load pulse tells the destination when to capture it. The data bus itself has no synchroniser.
    module mux_recirc #(parameter int W = 16, parameter int HOLD = 6) (
      input  logic         clk_s, rst_s_n, clk_d, rst_d_n,
      input  logic         load, input logic [W-1:0] din, output logic ready,
      output logic         out_valid, output logic [W-1:0] dout
    );
      logic [W-1:0] hold_reg;
      logic [$clog2(HOLD+1)-1:0] hold_cnt;                       // wide enough to count down from HOLD itself
      logic         pulse_s, pulse_d;
    
      if (HOLD < 1) begin : g_hold_check                        // legal range: HOLD >= 1 source cycle
        $error("mux_recirc: HOLD must be at least 1 source cycle");
      end
    
      assign ready = (hold_cnt == 0);
      always_ff @(posedge clk_s or negedge rst_s_n)
        if (!rst_s_n) begin hold_reg <= '0; hold_cnt <= '0; pulse_s <= 1'b0; end
        else begin
          pulse_s <= 1'b0;
          if (hold_cnt != 0) hold_cnt <= hold_cnt - 1'b1;          // keep the data still for HOLD source cycles
          else if (load)     begin hold_reg <= din; pulse_s <= 1'b1; hold_cnt <= HOLD; end
        end
    
      pulse_sync u_p (.clk_s, .rst_s_n, .pulse_s, .clk_d, .rst_d_n, .pulse_d);
    
      always_ff @(posedge clk_d or negedge rst_d_n)
        if (!rst_d_n) begin dout <= '0; out_valid <= 1'b0; end
        else begin
          out_valid <= pulse_d;
          if (pulse_d) dout <= hold_reg;                           // capture the stable word on the synchronised pulse
        end
    endmodule
    
    // One complete check of mux_recirc for a given HOLD: its own clocks, scoreboard and result.
    module recirc_check #(parameter int W = 16, parameter int HOLD = 6, parameter int WORDS = 3000)
      (output logic done, output int errors, output int got);
      logic clk_s = 0, clk_d = 0, rst_s_n = 0, rst_d_n = 0, load = 0, ready, out_valid;
      logic [W-1:0] din = 0, dout;
      mux_recirc #(W, HOLD) dut (.*);
      always #5 clk_s = ~clk_s;                                   // 10 ns
      always #8.53 clk_d = ~clk_d;                                // 17.06 ns: the destination is the SLOWER clock
    
      logic [W-1:0] expected [$];
      int sent = 0;
      initial begin done = 0; errors = 0; got = 0; end
      initial begin #100 rst_s_n = 1; rst_d_n = 1; end
    
      initial begin
        @(posedge rst_s_n);
        repeat (WORDS) begin
          wait (ready);
          @(negedge clk_s);
          load = 1; din = $urandom; expected.push_back(din); sent++;
          @(negedge clk_s);
          load = 0; din = $urandom;                                // the input bus changes right after the transfer
          repeat ($urandom_range(0, 3)) @(negedge clk_s);
        end
        repeat (20 + HOLD) @(posedge clk_d);
        if (got != sent) errors++;
        done = 1;
      end
    
      always @(posedge clk_d) if (out_valid) begin
        got++;
        if (expected.size() == 0) errors++;
        else if (dout !== expected.pop_front()) errors++;
      end
    endmodule
    
    // Two settings: the default HOLD = 6, and HOLD = 20, above the 15 that a 4-bit counter could hold.
    module tb_recirc;
      logic done_a, done_b;
      int   err_a, err_b, got_a, got_b;
      recirc_check #(.HOLD(6))  u_a (.done(done_a), .errors(err_a), .got(got_a));
      recirc_check #(.HOLD(20)) u_b (.done(done_b), .errors(err_b), .got(got_b));
    
      initial begin
        wait (done_a && done_b);
        if (err_a == 0 && err_b == 0)
          $display("PASS: HOLD = 6 and HOLD = 20 (above 15): %0d + %0d 16-bit words crossed to a slower clock with a synchronised load pulse; none corrupted or lost", got_a, got_b);
        else
          $display("FAIL: HOLD = 6 errors %0d (got %0d); HOLD = 20 errors %0d (got %0d)", err_a, got_a, err_b, got_b);
        $finish;
      end
    endmodule

    Remember

    • Two inequalities must hold: data arrives before the earliest capture (about Ts + 2 × Td), and the next change comes after the latest capture (about Ts + 3 × Td).

    • The protocol does not enforce the hold time. A slower destination clock, for example from a power mode, can break it.

    • The source may not load again until ready returns, after the hold counter expires.

    Rule of thumb: when the clock ratio is not fixed, use the 4-phase handshake.

    How this was checked

    Testbench, run with Icarus Verilog 13.0 (2026-10-16): prints PASS with 3,000 + 3,000 words (HOLD = 6 and HOLD = 20). This digital-model result does not establish worst-case analog resolution or all clock phases.

    Keep going

    • Practice: Debugging multi-bit bus corruption without a handshake

    • Interview: run a mock interview on this topic

    Related topics

    • Handshake checklist

    Previous

    175 / 350

    Next: CDC Handshake Checklist

    Up next

    Up nextCDC Handshake Checklist

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Choose a Protocol by Traffic Shape
    • 4-Phase Request/Acknowledge