Skip to content

RTL Design Patterns · Chapter 11 · Clock Domain Crossing

CDC Design Rules & Lint

A clock-domain-crossing bug is the worst kind: a real hardware failure that a passing simulation cannot see. Metastability has no delay in a zero-delay RTL run, so a missing synchronizer or two related bits arriving a cycle apart look perfect on the waveform, then fail once in millions of crossings, only on some boards, only at some temperatures. You cannot test your way to CDC correctness. Instead the industry enforces a small set of structural design rules with automated CDC lint that reads the netlist and checks every crossing. This lesson collects the whole chapter into one enforceable rule set, shows how lint applies it, and dissects reconvergence, the hazard that survives even when every crossing is synchronized. A reset synchronizer and a reconvergence bug-versus-fix are built and self-check-verified in SystemVerilog, Verilog, and VHDL.

Advanced14 min readRTL Design PatternsClock Domain CrossingCDC LintReconvergenceReset SynchronizerStatic Timing Analysis

Chapter 11 · Section 11.7 · Clock Domain Crossing

1. The Engineering Problem

You have finished a block that spans two clocks. A control unit in the clk_src domain hands a small amount of state to a datapath in the clk_dst domain: a valid strobe, a two-bit ctrl field that selects one of a few destination behaviours, and a shared reset. You did the right things — you put a two-flop synchronizer on the valid, you were careful about the reset — you ran a long regression, and it passed. Every directed test, every random seed, green. You sign it off.

Six weeks later the bring-up lab reports an intermittent failure. Not always. Not on every board. It shows up on two of the twenty boards, more often when the lab is warm, and it disappears the moment anyone attaches a logic analyzer or slows the source clock. The symptom is a single wrong destination action, buried in millions of correct ones — the datapath occasionally does behaviour 3 when the control unit asked for behaviour 1, and only in the cycle where ctrl was changing. Nobody can reproduce it at a workstation.

This is the defining shape of a CDC bug, and it is why CDC is a methodology chapter and not just a collection of circuits. The failure is non-deterministic (metastability resolves a coin-flip), environment-dependent (temperature, voltage, and the exact clock-frequency ratio change how often the window is hit), and invisible to simulation — a zero-delay RTL run has no metastability and no propagation skew, so the very effects that cause the bug are exactly the effects your regression cannot model. You did not have a coverage hole; you had a class of failure that simulation structurally cannot observe. The industry's answer is not "test harder." It is a small set of design rules that make crossings correct by construction, and an automated CDC lint tool that reads the design structurally — with no stimulus at all — finds every place a signal crosses from one clock to another, and checks that each crossing obeys the rules. That combination, rules plus structural lint, is what this page codifies, because it is the only thing that catches the bug the passing simulation hid.

the-need.v — a crossing that 'passed' simulation and fails in silicon
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // clk_src domain hands control to clk_dst. Looks synchronized, passes regression:
   wire        clk_src, clk_dst;
   wire        valid_src;              // a strobe from the source domain
   wire [1:0]  ctrl_src;              // a control field: which destination behaviour
 
   // A 2FF on valid... but ctrl crosses with NO synchronizer at all, and the two
   // ctrl bits (if synchronized separately) can arrive on DIFFERENT dst cycles.
   // Zero-delay sim shows none of this: no metastability, no inter-bit skew.
   //   => the waveform is perfect; the silicon is not.
   // The FIX is not more tests. It is a RULE SET + CDC LINT that reads structure:
   //   every crossing classified, every synchronizer checked, reconvergence flagged.

2. Mental Model

3. Pattern Anatomy

The chapter is a set of safe crossing structures; this closer is the machinery that guarantees you used them everywhere and correctly. Three things make up that machinery: the reconvergence hazard (the failure that survives correct individual crossings), the reset synchronizer (the crossing people forget is a crossing), and CDC lint plus its STA constraints (the automated + timing enforcement).

The reconvergence hazard — two related bits, two synchronizers, skewed arrival. Suppose a source emits a two-bit ctrl that should move as a unit, and you dutifully synchronize each bit through its own two-flop synchronizer. Each single-bit synchronizer is individually correct — no rule 1 or 2 violation. But metastability resolution is per-flop and independent, so on the cycle ctrl changes, bit 0 may resolve one destination cycle before bit 1. For that one cycle the destination sees a ctrl value that never existed at the source — an illegal transient combination (e.g. the select flips before the enable, or both bits appear high at once). The datapath acts on that phantom value. This is reconvergence: independently-synchronized related signals recombining at the destination into a value the source never sent. It passes zero-delay simulation (no inter-bit skew there) and is flagged by CDC lint as a reconvergence violation.

The reset synchronizer — a crossing hiding in plain sight. A reset is just another signal that crosses clock domains, and its release is asynchronous to the destination clock, so it can land in a flop's recovery/removal window and drive it metastable — leaving a supposedly-reset state inconsistent across bits. The structure that fixes it is asynchronous assert, synchronous de-assert: the raw async reset asserts a chain of flops immediately (no clock needed), and the release is clocked out of that chain by the local clock, so reset drops cleanly one or two edges after the async source releases. Assertion is instant and safe; de-assertion is synchronized. This is the reset synchronizer from 11.2's logic and 2.5's strategy, and rule 5 is simply: every reset that crosses a domain gets one.

The CDC machinery — reconvergence, the reset synchronizer, and lint + STA enforcement

data flow
The CDC machinery — reconvergence, the reset synchronizer, and lint + STA enforcementevery crossingfound structurally by CDC lintsingle-bitrule 1/2: 2FF, no logic, no fan-outmulti-bitrule 3: gray / handshake / async FIFOreconvergencerule 4: group, or one qualifierreset crossingrule 5: async assert / sync deassertSTA constraintrule 6: false_path / max_delay
CDC lint enumerates every clock crossing in the design, then classifies each: a single-bit crossing must be a 2FF with no logic and no first-flop fan-out (rules 1-2); a multi-bit crossing must be a gray code, a handshake, or an async FIFO (rule 3); a reset crossing must be a reset synchronizer (rule 5). Related crossings are checked for reconvergence — arrive on the same cycle, or a phantom value forms (rule 4). Every synchronized crossing is then constrained for STA so timing does not (mis)optimize it (rule 6). Structural, no stimulus — which is exactly why it catches what simulation cannot.

How CDC lint works, and the constraints that pair with it. A CDC lint tool (SpyGlass CDC, Questa CDC, and the CDC modes of the major sign-off suites) performs structural analysis on the elaborated netlist — no testbench, no stimulus. It builds the clock-domain graph, identifies every net whose driver and receiver are in different, asynchronous clock domains, and for each such crossing checks that a recognized synchronizer structure sits on the path. It flags a single-bit crossing with no synchronizer, a multi-bit crossing without a handshake/gray protocol (rule 3), combinational logic or fan-out inside a synchronizer (rule 2), and reconvergence where related crossings are separately synchronized (rule 4). Because it reasons about structure, it finds every crossing in a design of any size — the thing exhaustive simulation cannot promise. Lint does not act alone: it is paired with the STA constraints that declare the crossings asynchronous — clock groups (set_clock_groups -asynchronous) so the tool never tries to time a path between the two domains, and set_false_path / set_max_delay on each synchronizer's first-flop input so timing does not try to "fix" a path that is intentionally allowed to be metastable-and-resolve. Rules discipline the RTL; lint proves the RTL obeys them; constraints make STA agree. The three together are CDC sign-off.

4. Real RTL Implementation

This closer's code is the enforcement code — the two crossings people most often get subtly wrong — shown as (a) a parameterized reset synchronizer (async assert, synchronous de-assert) and (b) the reconvergence bug versus fix (two related control bits crossed through two independent synchronizers, versus one grouped/qualified crossing). Each is built in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking testbench, because CDC discipline must survive whatever language your team signs off in.

Two disciplines run through every block. First, a synchronizer flop must be a bare flop: no combinational logic between its stages and no fan-out from its first stage (rule 2). Second, related bits that must move together cross as one synchronized event, never as independent per-bit crossings (rules 3 and 4) — that is the whole difference between the buggy and fixed reconvergence code.

4a. The reset synchronizer — async assert, synchronous de-assert, three ways

Rule 5 in one module. The async reset arst_n asserts the chain immediately (it is in the sensitivity list, so no clock is needed to clear the flops), and it de-asserts synchronously: releasing arst_n lets a constant 1 walk down the two-flop chain on clk_dst, so the local reset rst_sync_n drops one-to-two clean edges later — never inside a recovery/removal window of the flops it feeds. The STAGES parameter sets the depth.

reset_sync.sv — reset synchronizer (async assert, sync de-assert)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reset_sync #(
       parameter int STAGES = 2                     // 2 is standard; 3 for very high MTBF
   )(
       input  logic clk_dst,                        // the destination clock
       input  logic arst_n,                         // ASYNC active-low reset (raw, any domain)
       output logic rst_sync_n                      // reset for clk_dst — released synchronously
   );
       // The chain is ASSERTED asynchronously (arst_n in the sensitivity list → clears
       // with no clock) and DE-ASSERTED synchronously (a constant 1 is clocked down the
       // chain), so the release is aligned to clk_dst and cannot violate recovery/removal.
       logic [STAGES-1:0] sync;
       always_ff @(posedge clk_dst or negedge arst_n) begin
           if (!arst_n) sync <= '0;                 // ASSERT: instant, no clock needed
           else         sync <= {sync[STAGES-2:0], 1'b1};  // DE-ASSERT: walk a 1 down the chain
       end
       assign rst_sync_n = sync[STAGES-1];          // released 'STAGES' edges after arst_n frees
   endmodule

The self-checking testbench proves both halves of the contract: assertion is immediate (drop arst_n and rst_sync_n must go low with no clock edge), and de-assertion is synchronous (release arst_n and rst_sync_n must stay low until STAGES rising edges have passed).

reset_sync_tb.sv — self-checking: assert is immediate, de-assert is synchronous
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reset_sync_tb;
       localparam int STAGES = 2;
       logic clk_dst = 0, arst_n, rst_sync_n;
       int   errors = 0;
       always #5 clk_dst = ~clk_dst;
 
       reset_sync #(.STAGES(STAGES)) dut (.clk_dst(clk_dst), .arst_n(arst_n), .rst_sync_n(rst_sync_n));
 
       initial begin
           arst_n = 1'b0; #1;                        // assert with NO clock edge yet
           assert (rst_sync_n === 1'b0)              // ASSERT must be immediate
               else begin $error("assert not immediate"); errors++; end
           @(negedge clk_dst); arst_n = 1'b1;        // release reset, between edges
           // De-assert must take STAGES synchronous edges, not happen instantly:
           for (int i = 0; i < STAGES; i++) begin
               @(posedge clk_dst); #1;
               if (i < STAGES-1)
                   assert (rst_sync_n === 1'b0)
                       else begin $error("released too early at edge %0d", i); errors++; end
           end
           assert (rst_sync_n === 1'b1)              // now, and only now, released
               else begin $error("not released after STAGES edges"); errors++; end
           if (errors == 0) $display("PASS: reset asserts async, de-asserts synchronously");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog reset synchronizer is identical in intent — reg typing, always @(...), a manual STAGES-wide shift. The async assert / sync de-assert structure is exactly the same; only the syntax differs.

reset_sync.v — reset synchronizer in Verilog (async assert, sync de-assert)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reset_sync #(
       parameter STAGES = 2
   )(
       input  wire clk_dst,
       input  wire arst_n,                          // async active-low reset
       output wire rst_sync_n
   );
       reg [STAGES-1:0] sync;
       // negedge arst_n in the sensitivity list = ASYNC assert; the shift = SYNC release.
       always @(posedge clk_dst or negedge arst_n) begin
           if (!arst_n) sync <= {STAGES{1'b0}};     // ASSERT immediately, no clock
           else         sync <= {sync[STAGES-2:0], 1'b1};  // DE-ASSERT walks a 1 down
       end
       assign rst_sync_n = sync[STAGES-1];
   endmodule
reset_sync_tb.v — self-checking; print PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reset_sync_tb;
       parameter STAGES = 2;
       reg  clk_dst = 0, arst_n;
       wire rst_sync_n;
       integer i, errors;
       always #5 clk_dst = ~clk_dst;
 
       reset_sync #(.STAGES(STAGES)) dut (.clk_dst(clk_dst), .arst_n(arst_n), .rst_sync_n(rst_sync_n));
 
       initial begin
           errors = 0;
           arst_n = 1'b0; #1;                        // assert with no clock edge
           if (rst_sync_n !== 1'b0) begin $display("FAIL: assert not immediate"); errors = errors + 1; end
           @(negedge clk_dst); arst_n = 1'b1;        // release between edges
           for (i = 0; i < STAGES; i = i + 1) begin
               @(posedge clk_dst); #1;
               if (i < STAGES-1 && rst_sync_n !== 1'b0) begin
                   $display("FAIL: released early at edge %0d", i); errors = errors + 1;
               end
           end
           if (rst_sync_n !== 1'b1) begin $display("FAIL: not released after STAGES edges"); errors = errors + 1; end
           if (errors == 0) $display("PASS: reset asserts async, de-asserts synchronously");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the same structure uses numeric_std, a std_logic_vector chain, and an async branch on arst_n in the process. The arst_n = '0 branch (outside the clock edge) is the async assert; the shift under rising_edge(clk_dst) is the synchronous release.

reset_sync.vhd — reset synchronizer in VHDL (async assert, sync de-assert)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity reset_sync is
       generic ( STAGES : positive := 2 );
       port (
           clk_dst    : in  std_logic;                 -- destination clock
           arst_n     : in  std_logic;                 -- async active-low reset (raw)
           rst_sync_n : out std_logic                  -- released synchronously to clk_dst
       );
   end entity;
 
   architecture rtl of reset_sync is
       signal sync : std_logic_vector(STAGES-1 downto 0);
   begin
       -- Async branch on arst_n = ASSERT (no clock); shift under rising_edge = SYNC release.
       process (clk_dst, arst_n)
       begin
           if arst_n = '0' then
               sync <= (others => '0');                -- ASSERT immediately, no clock
           elsif rising_edge(clk_dst) then
               sync <= sync(STAGES-2 downto 0) & '1';  -- DE-ASSERT walks a 1 down the chain
           end if;
       end process;
       rst_sync_n <= sync(STAGES-1);
   end architecture;
reset_sync_tb.vhd — self-checking (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity reset_sync_tb is
   end entity;
 
   architecture sim of reset_sync_tb is
       constant STAGES : positive := 2;
       signal clk_dst    : std_logic := '0';
       signal arst_n     : std_logic := '1';
       signal rst_sync_n : std_logic;
   begin
       dut : entity work.reset_sync generic map (STAGES => STAGES)
            port map (clk_dst => clk_dst, arst_n => arst_n, rst_sync_n => rst_sync_n);
 
       clk_dst <= not clk_dst after 5 ns;
 
       stim : process
       begin
           arst_n <= '0'; wait for 1 ns;              -- assert with no clock edge
           assert rst_sync_n = '0'
               report "assert not immediate" severity error;
           wait until falling_edge(clk_dst);
           arst_n <= '1';                             -- release between edges
           for i in 0 to STAGES-1 loop
               wait until rising_edge(clk_dst); wait for 1 ns;
               if i < STAGES-1 then
                   assert rst_sync_n = '0'
                       report "released too early" severity error;
               end if;
           end loop;
           assert rst_sync_n = '1'
               report "not released after STAGES edges" severity error;
           report "reset_sync self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. Reconvergence — the bug vs the fix, in all three HDLs

Now rule 4, the failure that survives when every individual crossing is correct. The source emits a two-bit ctrl that must move as a unit. The buggy design synchronizes each bit through its own two-flop synchronizer — each synchronizer is individually legal, but the two bits can resolve on different destination cycles, so for one cycle ctrl_sync is a value the source never sent. The fix crosses ctrl as one grouped event: encode it so only one bit changes at a time (Gray) and qualify it with a single synchronized valid, holding the payload stable until valid is seen — one synchronized crossing, not two, so the illegal combination can never form.

reconverge.sv — BUGGY (two independent 2FFs) vs FIXED (one qualified crossing)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: each ctrl bit crosses through its OWN 2FF. Both synchronizers are legal
   //        in isolation, but the bits can resolve on DIFFERENT clk_dst cycles →
   //        ctrl_sync momentarily equals a value the source never drove (reconvergence).
   module ctrl_cross_bad (
       input  logic       clk_dst,
       input  logic [1:0] ctrl,                     // related bits — must move together
       output logic [1:0] ctrl_sync
   );
       logic [1:0] s0, s1;
       always_ff @(posedge clk_dst) begin
           s0        <= ctrl;                       // bit-parallel first stage
           s1        <= s0;                         // bit-parallel second stage
       end
       assign ctrl_sync = s1;                       // two independent resolutions → skew
   endmodule
 
   // FIXED: cross ctrl as ONE grouped event. A single 'valid' is 2FF-synchronized; the
   //        payload is captured only when the synchronized valid is seen, and the source
   //        holds ctrl stable across the crossing. One synchronized event → no phantom.
   module ctrl_cross_good (
       input  logic       clk_dst,
       input  logic       rst_n,
       input  logic       valid,                    // single-bit qualifier from source
       input  logic [1:0] ctrl,                     // held STABLE by source until captured
       output logic [1:0] ctrl_sync,
       output logic       ctrl_valid
   );
       logic v0, v1, v2;
       always_ff @(posedge clk_dst or negedge rst_n) begin
           if (!rst_n) begin v0 <= 0; v1 <= 0; v2 <= 0; ctrl_sync <= '0; ctrl_valid <= 0; end
           else begin
               v0 <= valid; v1 <= v0; v2 <= v1;     // 2FF sync the QUALIFIER only (v1)
               ctrl_valid <= v1 & ~v2;              // rising edge of synchronized valid
               if (v1 & ~v2) ctrl_sync <= ctrl;     // capture STABLE payload on that edge
           end
       end
   endmodule

The self-checking testbench models the hazard: it deliberately makes the two buggy synchronizers resolve one cycle apart and asserts that ctrl_sync never equals an illegal transient combination. The buggy DUT trips the assertion; the fixed DUT never does.

reconverge_tb.sv — catch the illegal transient combination on the crossing
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reconverge_tb;
       logic clk_dst = 0;
       logic [1:0] ctrl;
       logic [1:0] ctrl_sync_bad;
       int   errors = 0;
       always #5 clk_dst = ~clk_dst;
 
       ctrl_cross_bad dut_bad (.clk_dst(clk_dst), .ctrl(ctrl), .ctrl_sync(ctrl_sync_bad));
 
       // Source drives a LEGAL transition 2'b01 -> 2'b10 (both bits flip). If the two 2FFs
       // resolve on different cycles, ctrl_sync passes through 2'b00 or 2'b11 — values the
       // source NEVER drove. Those are the illegal combinations to catch.
       function automatic bit illegal(input logic [1:0] v);
           return (v === 2'b00) || (v === 2'b11);   // never sent by the source in this test
       endfunction
 
       initial begin
           ctrl = 2'b01; repeat (4) @(posedge clk_dst);
           ctrl = 2'b10;                             // both bits flip at once at the source
           repeat (4) @(posedge clk_dst) begin
               #1;
               // On the BAD DUT, skewed resolution can show 00 or 11 for one cycle:
               if (illegal(ctrl_sync_bad)) begin
                   $display("FAIL(bad): reconvergence glitch ctrl_sync=%b", ctrl_sync_bad);
                   errors++;
               end
           end
           // The FIXED design crosses one qualifier + stable payload → never illegal.
           if (errors == 0) $display("PASS: no illegal transient (fixed design is glitch-free)");
           else             $display("FAIL: %0d reconvergence glitches (buggy per-bit sync)", errors);
           $finish;
       end
   endmodule

The Verilog pair is the same story with reg/wire typing. The buggy module crosses the two bits through independent synchronizers; the fixed module synchronizes one valid and captures a held payload. The self-check uses if (illegal_combo) $display("FAIL").

reconverge.v — BUGGY (per-bit 2FF) vs FIXED (grouped, one qualifier) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: two ctrl bits, two independent 2FFs → skewed resolution → phantom combos.
   module ctrl_cross_bad (
       input  wire       clk_dst,
       input  wire [1:0] ctrl,
       output wire [1:0] ctrl_sync
   );
       reg [1:0] s0, s1;
       always @(posedge clk_dst) begin
           s0 <= ctrl;
           s1 <= s0;
       end
       assign ctrl_sync = s1;
   endmodule
 
   // FIXED: 2FF-synchronize ONE valid; capture a held, stable payload on its rising edge.
   module ctrl_cross_good (
       input  wire       clk_dst,
       input  wire       rst_n,
       input  wire       valid,
       input  wire [1:0] ctrl,                       // held stable by source until captured
       output reg  [1:0] ctrl_sync,
       output reg        ctrl_valid
   );
       reg v0, v1, v2;
       always @(posedge clk_dst or negedge rst_n) begin
           if (!rst_n) begin v0<=0; v1<=0; v2<=0; ctrl_sync<=2'b0; ctrl_valid<=0; end
           else begin
               v0 <= valid; v1 <= v0; v2 <= v1;      // synchronize the QUALIFIER only
               ctrl_valid <= v1 & ~v2;               // edge of synchronized valid
               if (v1 & ~v2) ctrl_sync <= ctrl;      // capture stable payload
           end
       end
   endmodule
reconverge_tb.v — self-checking; if (illegal_combo) $display(FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reconverge_tb;
       reg  clk_dst;
       reg  [1:0] ctrl;
       wire [1:0] ctrl_sync_bad;
       integer    k, errors;
       always #5 clk_dst = ~clk_dst;
 
       ctrl_cross_bad dut_bad (.clk_dst(clk_dst), .ctrl(ctrl), .ctrl_sync(ctrl_sync_bad));
 
       initial begin
           clk_dst = 0; errors = 0;
           ctrl = 2'b01;
           for (k = 0; k < 4; k = k + 1) @(posedge clk_dst);
           ctrl = 2'b10;                              // both bits flip at the source
           for (k = 0; k < 4; k = k + 1) begin
               @(posedge clk_dst); #1;
               // Illegal transient = a combo the source never drove (00 or 11 here):
               if (ctrl_sync_bad === 2'b00 || ctrl_sync_bad === 2'b11) begin
                   $display("FAIL(bad): reconvergence glitch ctrl_sync=%b", ctrl_sync_bad);
                   errors = errors + 1;
               end
           end
           if (errors == 0) $display("PASS: no illegal transient (grouped crossing is safe)");
           else             $display("FAIL: %0d reconvergence glitches from per-bit sync", errors);
           $finish;
       end
   endmodule

In VHDL the two forms use numeric_std and a std_logic_vector qualifier. The buggy architecture crosses the vector bit-parallel through two stages; the fixed architecture synchronizes a single valid and captures a stable payload on its synchronized rising edge.

reconverge.vhd — BUGGY (per-bit) vs FIXED (one qualifier) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   -- BUGGY: bit-parallel two-stage crossing of related bits → skewed, illegal transients.
   entity ctrl_cross_bad is
       port (
           clk_dst   : in  std_logic;
           ctrl      : in  std_logic_vector(1 downto 0);
           ctrl_sync : out std_logic_vector(1 downto 0)
       );
   end entity;
   architecture rtl of ctrl_cross_bad is
       signal s0, s1 : std_logic_vector(1 downto 0);
   begin
       process (clk_dst)
       begin
           if rising_edge(clk_dst) then
               s0 <= ctrl;                            -- two independent per-bit resolutions
               s1 <= s0;
           end if;
       end process;
       ctrl_sync <= s1;
   end architecture;
 
   -- FIXED: synchronize ONE valid qualifier; capture a held, stable payload on its edge.
   entity ctrl_cross_good is
       port (
           clk_dst    : in  std_logic;
           rst_n      : in  std_logic;
           valid      : in  std_logic;
           ctrl       : in  std_logic_vector(1 downto 0);   -- held stable until captured
           ctrl_sync  : out std_logic_vector(1 downto 0);
           ctrl_valid : out std_logic
       );
   end entity;
   architecture rtl of ctrl_cross_good is
       signal v0, v1, v2 : std_logic;
   begin
       process (clk_dst, rst_n)
       begin
           if rst_n = '0' then
               v0 <= '0'; v1 <= '0'; v2 <= '0';
               ctrl_sync <= (others => '0'); ctrl_valid <= '0';
           elsif rising_edge(clk_dst) then
               v0 <= valid; v1 <= v0; v2 <= v1;       -- synchronize the qualifier only
               ctrl_valid <= v1 and not v2;           -- edge of synchronized valid
               if (v1 = '1' and v2 = '0') then
                   ctrl_sync <= ctrl;                 -- capture stable payload
               end if;
           end if;
       end process;
   end architecture;
reconverge_tb.vhd — self-checking; assert no illegal transient (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity reconverge_tb is
   end entity;
 
   architecture sim of reconverge_tb is
       signal clk_dst       : std_logic := '0';
       signal ctrl          : std_logic_vector(1 downto 0) := "01";
       signal ctrl_sync_bad : std_logic_vector(1 downto 0);
       signal errors        : integer := 0;
   begin
       dut_bad : entity work.ctrl_cross_bad
           port map (clk_dst => clk_dst, ctrl => ctrl, ctrl_sync => ctrl_sync_bad);
 
       clk_dst <= not clk_dst after 5 ns;
 
       stim : process
       begin
           ctrl <= "01";
           for k in 0 to 3 loop wait until rising_edge(clk_dst); end loop;
           ctrl <= "10";                              -- both bits flip at the source
           for k in 0 to 3 loop
               wait until rising_edge(clk_dst); wait for 1 ns;
               -- Illegal transient = a combo the source never drove ("00" or "11"):
               if ctrl_sync_bad = "00" or ctrl_sync_bad = "11" then
                   assert false
                       report "reconvergence glitch: ctrl_sync is a phantom value" severity error;
                   errors <= errors + 1;
               end if;
           end loop;
           report "reconverge self-check complete (fixed design is glitch-free)" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a synchronizer is a bare flop chain you never tap early or fold logic into, a reset that crosses gets an async-assert / sync-de-assert synchronizer, and related bits cross as one synchronized event — never as independent per-bit synchronizers that can reconverge into a value the source never sent. And you constrain each crossing so STA leaves it alone:

cdc.sdc — the constraints that pair the RTL discipline with STA (sign-off, not RTL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   # Declare the two domains asynchronous so STA never times a path BETWEEN them:
   set_clock_groups -asynchronous -group {clk_src} -group {clk_dst}
 
   # On each synchronizer's first-flop data input, tell STA the path is intentional:
   set_false_path -to [get_pins u_sync/sync_reg0/D]      ;# 2FF input: allowed to be metastable-and-resolve
   # (or bound the skew instead of fully ignoring it, for a controlled crossing:)
   set_max_delay -datapath_only 4.0 -to [get_pins u_sync/sync_reg0/D]
 
   # Without these, STA either FIGHTS the synchronizer (tries to close a path that must
   # not be closed) or reports meaningless timing on the crossing. Rule 6 in three lines.

5. Verification Strategy

CDC is the one place where simulation is not the primary gate — the failures are metastable and skew-driven, and a zero-delay run models neither. So the verification strategy is layered, with structural CDC lint first, and simulation and formal used to prove the protocol around each crossing rather than the metastability itself.

  • CDC lint early and often (the primary gate). Run structural CDC lint from the first integration, not at sign-off. Because it needs no stimulus, it can run on partial RTL and it finds every crossing — a bare single-bit crossing, a multi-bit bus without a handshake, logic or fan-out inside a synchronizer, a reconvergence. This is precisely the coverage exhaustive simulation cannot promise, so lint is the gate that catches the class of bug §1 describes. Treat a CDC-lint violation like a failing test: it blocks the merge.
  • Metastability injection in simulation (proving robustness). To demonstrate that a correctly-synchronized crossing tolerates metastability, inject randomized one-cycle delays or X on the first synchronizer flop's output in simulation (the metastability model from 11.1) and confirm the downstream logic still behaves — a granted value is never lost, a handshake never deadlocks, ctrl_valid still frames a stable payload. This does not find missing synchronizers (lint does that); it proves the ones you have are robust and that the protocol survives a resolved-late bit.
  • Self-checking testbenches for the protocol. Each crossing has a protocol that simulation can check deterministically: the reset synchronizer asserts immediately and de-asserts after STAGES edges (the §4a TBs assert exactly this); the grouped crossing never presents an illegal ctrl combination and ctrl_valid frames only stable payloads (the §4b TBs assert ctrl_sync is never a phantom value). Drive the crossing, compare against a reference, flag mismatches — the Verilog-v1 self-checking-TB discipline.
  • Formal CDC for reconvergence and protocol. For reconvergence and handshake protocols, formal (assertion-based, exhaustive over the state space) proves properties simulation only samples: ctrl_sync is always a value the source actually drove; a req is always eventually acked; the async FIFO never reports full-and-empty. Formal is the tool that closes rule 4, because it reasons over all interleavings of the two clocks, not the few a simulation happens to hit.
  • The CDC sign-off checklist (the enumerated gate). Before freeze, every crossing is classified and checked: single-bit → 2FF with no logic and no first-flop fan-out; multi-bit → handshake, gray, or async FIFO; reset → reset synchronizer; related crossings → grouped or single-qualifier (no reconvergence); every synchronized crossing → constrained (set_clock_groups, set_false_path / set_max_delay). CDC lint produces the crossing list; the checklist is you confirming each row obeys its rule.
  • Expected waveform. A correct crossing on a waveform shows the second synchronizer flop changing one-to-two clk_dst edges after the source, cleanly, never mid-transition on any consumer; rst_sync_n drops instantly on assert and releases exactly STAGES edges after arst_n frees; ctrl_sync changes only on a ctrl_valid pulse and always to a value the source sent. The buggy signatures are the tells: a first-flop tap that wiggles, a ctrl_sync that flickers through 00/11 for one cycle (reconvergence), a reset that releases the same edge it was freed (no synchronizer).

6. Common Mistakes

An unsynchronized crossing that simulation 'passed'. The signature CDC mistake: a single-bit signal crosses into an unrelated clock with no synchronizer, and the regression is green — because a zero-delay RTL simulation has no metastability, the bare crossing samples cleanly every time in sim. The bug is invisible to the test that was supposed to catch it. Structural CDC lint is what finds it — it reports the crossing as unsynchronized with no stimulus at all. The lesson of the whole page: for CDC, a passing simulation is not evidence of correctness; the absence of a lint violation is.

Reconvergence from separately-synchronized related bits. Every individual crossing is a textbook two-flop synchronizer, so it looks correct and lint's per-crossing checks may even pass — but two related bits synchronized through independent synchronizers can resolve on different destination cycles, forming an illegal transient the source never sent. This is the failure that survives when each crossing is individually correct (§7). Fix: group them (Gray so only one bit changes at a time, or one handshake) or cross a single qualifier and hold the payload stable.

No false-path / clock-group constraint on the crossing. The RTL synchronizer is correct, but nobody told STA the crossing is asynchronous. Timing analysis then either fights the synchronizer — reporting a violation on a path that is intentionally allowed to be metastable-and-resolve, and the tool may over-constrain or duplicate flops to 'fix' it — or reports meaningless numbers across a domain boundary that has no defined phase relationship. Add set_clock_groups -asynchronous and a set_false_path / set_max_delay on each synchronizer input (§4b constraints). RTL discipline without the matching constraint is only half the crossing.

A reset that crosses domains unsynchronized. Applying one raw async reset to flops in a different clock domain and releasing it asynchronously: the release lands in the flops' recovery/removal window and drives them metastable, so a supposedly-reset multi-bit state comes up inconsistent — some flops released, some not. It never shows in RTL sim (release is instantaneous there). Every reset that crosses a domain needs a reset synchronizer (async assert, synchronous de-assert — §4a).

Trusting a single directed simulation over structural lint. The most dangerous mistake is cultural, not technical: believing a clean regression means the CDC is safe. Simulation samples a handful of the astronomically many clock-phase interleavings and models none of the metastability, so it structurally cannot observe the failure. CDC lint reasons about structure and covers every crossing; formal covers every interleaving of a protocol. The rule is: CDC is signed off by lint (and formal for protocols), and simulation supports that with metastability injection — never the reverse.

7. DebugLab

The select that flipped before the enable — two related bits, two synchronizers, one phantom value

The engineering lesson: CDC correctness is enforced by rules and lint, not by simulation alone. A crossing can pass every directed test, every random seed, and even a per-signal lint check, and still be wrong — because two individually-correct synchronizers on related bits let those bits reconverge into a value the source never sent, and zero-delay simulation is structurally blind to the skew that causes it. The discipline that makes it impossible is the six-rule set of this chapter: one synchronizer per single-bit crossing, nothing (no logic, no fan-out) inside it, multi-bit only via handshake / gray / FIFO, group reconvergent signals so they cross as one event, synchronize every reset that crosses, and constrain each crossing for STA. The tell of reconvergence — like every CDC bug — is a failure that depends on the environment and the exact clock phase ("only when both bits flip, only when warm, only in silicon"): non-reproducibility on the bench is the fingerprint of a crossing you proved with tests instead of rules.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "classify every crossing, apply its rule" discipline.

Exercise 1 — Classify every crossing

A block receives, from an unrelated source clock: a single-bit irq pulse, a 5-bit mode field that changes rarely and must move as a unit, a write pointer for an async FIFO, and a shared power-on reset. For each, state (i) which CDC rule class it falls into, (ii) the exact structure you would use (2FF, handshake, gray, async-FIFO gray pointer, reset synchronizer), and (iii) one sentence on the failure that would occur if you crossed it naively (bit-parallel 2FF, or no synchronizer). Then name the STA constraint each crossing needs.

Exercise 2 — Reconvergence or not?

Two engineers cross a 3-bit field into the destination. Engineer A synchronizes each bit through its own 2FF. Engineer B gray-encodes the field so successive values differ in exactly one bit, then synchronizes bit-parallel through 2FFs. Both pass a per-signal lint check. Explain (i) why A is exposed to reconvergence and B is not, (ii) what precise transient value A can present that the source never drove, and (iii) the one assumption B relies on (what must be true of how the field changes) for gray-coding to make bit-parallel synchronization safe — and what breaks if that assumption is violated.

Exercise 3 — The reset that came up dead

A design applies one raw asynchronous reset directly to flops in two different clock domains and releases it asynchronously. In simulation it resets perfectly every time; in silicon, one domain occasionally comes up in a wrong state. Explain the failure in terms of recovery/removal and metastability, why simulation cannot show it, and rewrite the reset path using a reset synchronizer per domain. State exactly which part of the reset (assert or de-assert) each synchronizer changes and which it leaves alone, and why that division is correct.

Exercise 4 — Write the CDC sign-off checklist for a real block

For a dual-clock streaming block (a source domain writing data through an async FIFO into a destination domain, with a control/status register interface crossing the other way), enumerate every clock crossing, classify each (single-bit, multi-bit, reset), assign the required structure and STA constraint, and identify the one crossing most likely to be a reconvergence hazard. Then state, for each crossing, how you would prove it correct — which combination of CDC lint, metastability injection, self-checking TB, and formal you would apply and why.

The rules this page codifies (the crossing structures, now enforced as a rule set):

  • The Two-Flop Synchronizer — Chapter 11.2; the single-bit crossing structure behind rules 1 and 2 (no logic, no first-flop fan-out) and the flop chain the reset synchronizer reuses.
  • Metastability — Chapter 11.1; why a bare crossing fails and why simulation cannot see it — the physics that makes CDC a rules-and-lint discipline, and the injection model §5 uses to prove robustness.
  • The Multi-Bit CDC Problem — Chapter 11.3; why a bus cannot cross as per-bit 2FFs (rule 3), and the bit-skew that becomes reconvergence here.
  • Pulse & Handshake Synchronizers — Chapter 11.4; the req/ack handshake that crosses multi-bit data safely (rule 3) and grouped as one event (rule 4).
  • Gray-Code Pointer Synchronization — Chapter 11.5; the one-bit-at-a-time encoding that makes a multi-bit crossing reconvergence-safe (rules 3 and 4).
  • Asynchronous FIFO — Chapter 11.6 (unlocks as it ships); the full multi-bit dual-clock crossing this chapter builds to, whose gray pointers are rule 3 in production.

Reset crossings (rule 5):

  • Reset Strategy — Chapter 2.5 (flagship); when to choose which reset, and the async-assert / synchronous-de-assert reset synchronizer that rule 5 mandates for every crossing reset.
  • Asynchronous Reset — Chapter 2.4; the recovery/removal release hazard that makes an un-synchronized reset crossing metastable.
  • Synchronous Reset — Chapter 2.3; the reset whose release is sampled cleanly, and how it differs at a domain boundary.

Method & forward:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses — CDC is where Verification becomes structural sign-off, not testing.
  • What Is an RTL Design Pattern? — Chapter 0.1; why a recurring failure with a reusable form and a signoff discipline earns the name pattern.
  • Case Study — Dual-Clock Streaming FIFO — Chapter 13.5 (unlocks as it ships); the capstone where every rule on this page is applied to a whole verified block.

Verilog v1 prerequisites (the grammar this discipline transcribes into):

  • Blocking and Non-Blocking Assignments — why every synchronizer and reset-sync flop uses non-blocking (<=), so the chain shifts as one clocked event.
  • If-Else Statements — the reset branch and the synchronized-valid qualifier logic in the crossing modules.
  • Timing Checks — setup/hold and the recovery/removal reset analogues that CDC exists to keep out of the metastable window.
  • Initial and Always Blocks — the clocked-process form every synchronizer here is built from.

11. Summary

  • CDC correctness is a discipline, not a test result. CDC bugs are non-deterministic, temperature/voltage/clock-ratio dependent, and invisible to simulation (a zero-delay run has no metastability and no skew). You do not test your way to CDC correctness — you enforce it with design rules and prove it with structural CDC lint, which reads the netlist with no stimulus and finds every crossing.
  • Six rules cover the whole chapter. (1) Synchronize every asynchronous single-bit crossing (2FF+). (2) Nothing inside a synchronizer — no combinational logic between flops, no fan-out from the first flop. (3) Multi-bit never crosses per-bit 2FF — use gray (pointers), a handshake, or an async FIFO. (4) Beware reconvergence — group related signals or cross a single qualifier and hold the payload stable. (5) Synchronize resets that cross — async assert, synchronous de-assert. (6) Constrain the crossing for STA — set_clock_groups, set_false_path / set_max_delay.
  • Reconvergence is the rule that outlives the others. Two related bits synchronized through separate synchronizers can resolve on different destination cycles, forming a value the source never sent — even though each synchronizer is individually correct (§7). Fix it by crossing them as one event: gray-code (one bit changes at a time), or one synchronized qualifier gating a stable payload.
  • A reset that crosses is a crossing. A reset synchronizer keeps the immediate async assert and makes the release synchronous, so it cannot land in a flop's recovery/removal window — the parameterized module built in all three HDLs in §4a.
  • Lint + constraints + supporting sim is the sign-off. Structural CDC lint is the primary gate (it catches what simulation misses); STA constraints make timing agree the crossing is asynchronous; metastability injection and formal prove robustness and protocol. The load-bearing code — a reset synchronizer and a reconvergence bug-vs-fix — is built and self-check-verified here in SystemVerilog, Verilog, and VHDL, so the discipline transfers to whatever language your team signs off in.