Skip to content

RTL Design Patterns · Chapter 11 · Clock Domain Crossing

The Two-Flop Synchronizer

When a flip-flop samples a signal from an unrelated clock, sooner or later it catches an edge inside its setup and hold window and goes metastable, sitting between a valid 0 and 1 for an unbounded time before resolving. You cannot design the crossing away or predict when it happens, but you can make the odds of a bad value reaching real logic vanishingly small. The two-flop synchronizer does exactly that for a single-bit level or control signal: two flops back to back, both clocked by the destination clock. The first flop may go metastable, but it gets a full clock period to settle before the second flop samples it, so the output is practically always clean. This lesson builds the structure, states the rules that keep it airtight, and codes it in SystemVerilog, Verilog, and VHDL.

Advanced14 min readRTL Design PatternsClock Domain CrossingSynchronizerMetastabilityMTBFASYNC_REG

Chapter 11 · Section 11.2 · Clock Domain Crossing

1. The Engineering Problem

You have two clocks that share no relationship: a clk_src that produces a control bit — say a ready, an enable, or a mode level — and a clk_dst in an entirely separate part of the chip that must react to that bit. The two oscillators drift, their edges slide past each other continuously, and nothing constrains when the source bit changes relative to the destination clock. That is an asynchronous clock-domain crossing, and 11.1 established what happens the instant a destination flop tries to sample that bit: eventually the source edge lands inside the destination flop's setup/hold window, the flop's timing is violated, and it goes metastable — it hangs at an invalid voltage, neither 0 nor 1, for an unbounded and probabilistic amount of time before it finally resolves to one rail.

The trap is that you cannot engineer this event out of existence. The two clocks are genuinely unrelated, so there is no phase relationship to constrain, no static-timing path to close across the boundary, and no clock frequency low enough to guarantee the edges never coincide — over enough cycles they will. Metastability at the crossing is not a bug you can fix; it is a physical certainty you must tolerate. The only question is what happens next.

And "next" is where a naive design fails catastrophically. If you feed that first, possibly-metastable flop directly into your logic — an enable that gates a write, a mode bit that steers a mux, a ready that a state machine branches on — different downstream gates see the ambiguous voltage at slightly different thresholds and resolve it differently. One path reads it as 1, another as 0, the state machine takes an illegal transition, a word is written twice or not at all. The failure is intermittent (it needs the edges to align just so), unreproducible on the bench, and can sit dormant for weeks before it bites in the field. You do not need to prevent metastability — 11.1 proved you can't — you need to contain it: give the metastable node time to resolve before any real logic looks at it, and make the odds of it still being ambiguous when logic does look astronomically small.

the-need.v — one async bit, sampled once, straight into logic: a time bomb
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // clk_src and clk_dst are UNRELATED. async_in is a single-bit LEVEL from clk_src.
   //   (an enable, a ready, a mode select — one bit of control, not a bus)
 
   // WRONG — sample once in the destination domain and use it directly:
   //   always @(posedge clk_dst) sync_ff1 <= async_in;   // sync_ff1 MAY be metastable
   //   assign wr_en   = sync_ff1;      // load A reads it as 1...
   //   assign do_mode = sync_ff1;      // ...load B reads it as 0  => INCOHERENT
   // sync_ff1 can hover between 0 and 1; every consumer resolves it independently.
 
   // The need: after sampling, give the ambiguous node a FULL clk_dst period to
   // settle before ANY logic uses it — so what logic sees is always resolved. That
   // structure is the two-flop synchronizer, and it is this page.

2. Mental Model

3. Pattern Anatomy

The structure is two flip-flops in a chain, both in the destination domain, with a precise set of connection rules — and the rules are where the whole correctness lives.

The source is a clean single-domain flop. The bit that crosses should come from a registered output in the source domain (a single clk_src flop), so what the crossing sees is a clean level that makes at most one transition per source cycle — not a combinational glitch, not a bus that ripples. The synchronizer assumes a well-behaved single-bit level at its input; a glitchy or multi-driver source violates that assumption before the first flop even samples.

Stage 1 — sync_ff1, the sacrificial sampler. The first destination-domain flop samples async_in on a clk_dst edge. This is the one flop in the whole design that is expected to occasionally violate setup/hold and go metastable, because its input is asynchronous to its clock. Nothing downstream is allowed to read sync_ff1 except stage 2 — that is not a style preference, it is the load-bearing rule (§6, §7).

Stage 2 — sync_ff2, the trusted resolver. The second destination-domain flop samples sync_ff1 exactly one clk_dst period after stage 1 captured the input. That intervening full period is the settling time: if sync_ff1 went metastable, it has had an entire clock period to decay toward a rail, so the probability it is still ambiguous when sync_ff2 samples it is vanishingly small. sync_ff2 drives sync_out — the only output the rest of the design uses.

The two adjacency rules — nothing between, nothing off the middle. Two connection constraints make the settling time real: (1) sync_ff1 and sync_ff2 must be directly adjacent — no combinational logic between them, so the whole period is settling time and not gate-delay; and (2) sync_ff1 must have no fan-out except to sync_ff2 — if any other load taps sync_ff1, that load sees the still-settling node and can resolve it differently, spreading the metastability the synchronizer was built to contain.

The two-flop synchronizer — one source flop, two destination-domain flops, one trusted output

data flow
The two-flop synchronizer — one source flop, two destination-domain flops, one trusted outputlevelsampleadjacent: wireonlysrc flop(clk_src)clean single-bit level; one edge per src cycleasync crossingunrelated clocks; edge may hit setup/holdsync_ff1(clk_dst)sacrificial: MAY go metastable; loads ff2 onlysync_ff2(clk_dst)samples ff1 one period later: resolved valuesync_outclean 0/1 the rest of the logic may use
A clean single-bit level is registered in the source domain, then crosses into the destination domain where two flops both clocked by CLK_DST sample it back to back. SYNC_FF1 takes the asynchronous edge and is the one that may go metastable; the edge between SYNC_FF1 and SYNC_FF2 is pure wire — no combinational logic — so the full CLK_DST period between the two samples is settling time. SYNC_FF2 samples the (by then resolved) value and drives SYNC_OUT, the only node the rest of the logic touches. SYNC_FF1 fans out to nothing but SYNC_FF2. This structure is identical in SystemVerilog, Verilog, and VHDL.

Why a whole period, and why that makes it safe. Metastability decays exponentially with time: the probability a node is still ambiguous after a wait t falls like exp(-t / tau), where tau is a fast device time-constant. The synchronizer's design lever is simply how much settling time it grants — and two flops grant one full destination clock period, which for a real tau drives the failure probability down by many orders of magnitude, giving a mean-time-between-failures (MTBF) typically measured in centuries. That is why "just add a second flop" is not hand-waving: the second flop is the period of settling time, and the period is what buys the MTBF.

Resolution timing — the first flop's metastability decays inside the settling period

data flow
Resolution timing — the first flop's metastability decays inside the settling periodwaitdecaynext edgeedge N: sync_ff1samplesasync edge in setup/hold -> ff1 goes METASTABLEone clk_dstperiodpure settling: no logic taps ff1ff1 decays to arailP(still ambiguous) ~ exp(-period / tau): tinyedge N+1:sync_ff2 samplescaptures the now-resolved ff1 valuesync_out cleanresolved 0/1; latency = 2 dest cycles
On destination edge N, SYNC_FF1 samples the asynchronous input; if the source edge fell in its setup/hold window it goes metastable. Between edge N and edge N+1 nothing reads SYNC_FF1, so the full CLK_DST period is settling time and the metastable voltage decays toward a rail — the probability it is still ambiguous after that period falls like exp(-period / tau) and is astronomically small. On edge N+1, SYNC_FF2 samples the by-now-resolved value and drives SYNC_OUT. A stable level therefore appears at SYNC_OUT exactly two destination-clock cycles after SYNC_FF1 first captured it: latency is 2 dest cycles, the price of the resolution. Adding a third flop grants another period and higher MTBF at the cost of one more cycle of latency.

The exact contract. Two flops (three for very high frequency or very high reliability), both in the destination domain, strictly adjacent, first flop fanning out only to the second, driven by a clean single-bit level from a single source-domain flop. Latency is two destination-clock cycles from input to sync_out. It synchronizes exactly one bit of level/control — not a multi-bit bus (§ 11.3) and not guaranteed to catch a single-cycle pulse (§ 11.4). Hold that contract and the crossing is safe; break any clause and §6/§7 show precisely how it fails.

4. Real RTL Implementation

This is the core of the page. We build two things — (a) a parameterizable STAGES-flop (default 2) single-bit synchronizer in the destination domain (no fan-out from any non-final stage, clean single output) with a two-async-clock testbench that crosses a single-bit level and asserts it arrives clean after STAGES destination cycles; and (b) the fan-out / single-flop bug beside the proper two-flop fix, with a metastability model injected on stage 1 (X or random) that corrupts a consumer in the buggy version and does not in the fixed one — each shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The reasoning is identical in all three; only the syntax differs.

One discipline runs through every RTL block: the synchronizer flops update on clk_dst with non-blocking assignments, reset is explicit (asynchronous, active-high rst here, so the synchronizer comes out of reset in a defined state), the chain is a simple shift of the crossing bit toward the output, and only the last stage drives the output — no intermediate stage is ever read by anything but the next stage.

4a. The parameterizable STAGES-flop synchronizer — three ways

The synchronizer stores the crossing bit through STAGES flops (default 2), all clocked by clk_dst. async_in enters stage 1; each stage feeds the next; only the final stage drives sync_out. Making STAGES a parameter lets a high-frequency design choose 3 without rewriting the module.

synchronizer.sv — parameterizable single-bit level synchronizer (default STAGES=2)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module synchronizer #(
       parameter int STAGES = 2                 // 2 is standard; 3 for high-freq / high-MTBF
   )(
       input  logic clk_dst,                    // DESTINATION clock: both flops live here
       input  logic rst,                        // async, active-high -> defined power-up
       input  logic async_in,                   // clean single-bit LEVEL from the source domain
       output logic sync_out                    // resolved level, after STAGES dest cycles
   );
       // The synchronizer chain. sync[0] is the sacrificial sampler (may go metastable);
       // sync[STAGES-1] is the trusted output. Nothing outside reads the intermediate bits.
       (* ASYNC_REG = "TRUE" *)                 // keep the flops ADJACENT: no retiming, no
       logic [STAGES-1:0] sync;                 // optimization, no fan-out on sync[0].
 
       always_ff @(posedge clk_dst or posedge rst) begin
           if (rst)
               sync <= '0;                      // deassert cleanly on both flops
           else
               // Shift the crossing bit down the chain. The edge between stages is pure
               // wire — no combinational logic — so each stage gets a full period to settle.
               sync <= {sync[STAGES-2:0], async_in};
       end
 
       assign sync_out = sync[STAGES-1];        // ONLY the last stage is used
   endmodule

The self-checking testbench runs two unrelated clocks — a source clock that drives a level and a faster, asynchronous destination clock that samples it — and asserts that a stable level applied in the source domain appears at sync_out after exactly STAGES destination edges, clean and correct. It does not (and cannot) test metastability directly here; it verifies the functional contract — correct value, correct latency — which the metastability model in 4b then stresses.

synchronizer_tb.sv — two async clocks; stable level appears after STAGES dest edges
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module synchronizer_tb;
       localparam int STAGES = 2;
       logic clk_dst = 1'b0, clk_src = 1'b0, rst, async_in, sync_out;
       int   errors = 0;
 
       synchronizer #(.STAGES(STAGES)) dut (
           .clk_dst(clk_dst), .rst(rst), .async_in(async_in), .sync_out(sync_out));
 
       // Two UNRELATED clocks (co-prime half-periods -> edges drift, as in a real crossing).
       always #5  clk_dst = ~clk_dst;           // ~100 MHz destination
       always #7  clk_src = ~clk_src;           // ~71  MHz source (async to clk_dst)
 
       // Apply a stable level, then count destination edges until sync_out follows it.
       task automatic drive_and_check(input logic level);
           int edges;
           @(negedge clk_src); async_in = level;   // change the level in the source domain
           edges = 0;
           // After STAGES rising dest edges that all saw the stable level, sync_out == level.
           repeat (STAGES) @(posedge clk_dst);
           #1;
           assert (sync_out === level)
               else begin $error("level=%b not synchronized after %0d edges (got %b)",
                                 level, STAGES, sync_out); errors++; end
       endtask
 
       initial begin
           rst = 1'b1; async_in = 1'b0;
           @(posedge clk_dst); #1; rst = 1'b0;
           assert (sync_out === 1'b0) else begin $error("reset: sync_out not 0"); errors++; end
 
           drive_and_check(1'b1);               // 0 -> 1 crossing
           drive_and_check(1'b0);               // 1 -> 0 crossing
           drive_and_check(1'b1);               // 0 -> 1 again
 
           if (errors == 0) $display("PASS: stable level synchronized cleanly after %0d dest cycles", STAGES);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog form is the same chain with reg/wire typing. The ASYNC_REG intent is carried as a synthesis attribute in the same place; the shift {sync[STAGES-2:0], async_in} is identical.

synchronizer.v — parameterizable single-bit synchronizer in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module synchronizer #(
       parameter STAGES = 2
   )(
       input  wire clk_dst,                     // destination clock
       input  wire rst,                         // async, active-high
       input  wire async_in,                    // clean single-bit level from source domain
       output wire sync_out
   );
       (* ASYNC_REG = "TRUE" *)                 // keep the chain adjacent; block retiming/fan-out
       reg [STAGES-1:0] sync;
 
       always @(posedge clk_dst or posedge rst) begin
           if (rst)
               sync <= {STAGES{1'b0}};
           else
               sync <= {sync[STAGES-2:0], async_in};   // shift the crossing bit down the chain
       end
 
       assign sync_out = sync[STAGES-1];        // only the final stage is used
   endmodule
synchronizer_tb.v — two async clocks; self-checking, PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module synchronizer_tb;
       parameter STAGES = 2;
       reg  clk_dst, clk_src, rst, async_in;
       wire sync_out;
       integer errors, k;
 
       synchronizer #(.STAGES(STAGES)) dut (
           .clk_dst(clk_dst), .rst(rst), .async_in(async_in), .sync_out(sync_out));
 
       always #5 clk_dst = ~clk_dst;            // ~100 MHz
       always #7 clk_src = ~clk_src;            // ~71 MHz, async to clk_dst
 
       task drive_and_check;
           input level;
           begin
               @(negedge clk_src); async_in = level;
               for (k = 0; k < STAGES; k = k + 1) @(posedge clk_dst);
               #1;
               if (sync_out !== level) begin
                   $display("FAIL: level=%b not synced after %0d edges (got %b)",
                            level, STAGES, sync_out);
                   errors = errors + 1;
               end
           end
       endtask
 
       initial begin
           errors = 0; clk_dst = 1'b0; clk_src = 1'b0; rst = 1'b1; async_in = 1'b0;
           @(posedge clk_dst); #1; rst = 1'b0;
           if (sync_out !== 1'b0) begin $display("FAIL: reset sync_out not 0"); errors = errors + 1; end
 
           drive_and_check(1'b1);
           drive_and_check(1'b0);
           drive_and_check(1'b1);
 
           if (errors == 0) $display("PASS: stable level synchronized after %0d dest cycles", STAGES);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the chain is a std_logic_vector shifted on clk_dst; reset is asynchronous, and the ASYNC_REG-equivalent intent is expressed with a synthesis attribute (vendor attribute names vary, so it is shown as the standard ASYNC_REG).

synchronizer.vhd — parameterizable single-bit synchronizer in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity synchronizer is
       generic ( STAGES : positive := 2 );          -- 2 standard; 3 for high-freq/high-MTBF
       port (
           clk_dst  : in  std_logic;                -- destination clock: both flops here
           rst      : in  std_logic;                -- async, active-high
           async_in : in  std_logic;                -- clean single-bit level from source domain
           sync_out : out std_logic
       );
   end entity;
 
   architecture rtl of synchronizer is
       signal sync : std_logic_vector(STAGES-1 downto 0);
       attribute ASYNC_REG : string;
       attribute ASYNC_REG of sync : signal is "TRUE";   -- keep flops adjacent; no retime/fan-out
   begin
       process (clk_dst, rst)
       begin
           if rst = '1' then
               sync <= (others => '0');
           elsif rising_edge(clk_dst) then
               -- shift the crossing bit down the chain: sync(0) is the sacrificial sampler
               sync <= sync(STAGES-2 downto 0) & async_in;
           end if;
       end process;
 
       sync_out <= sync(STAGES-1);                   -- only the last stage is used
   end architecture;
synchronizer_tb.vhd — two async clocks; self-checking assert severity
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity synchronizer_tb is
   end entity;
 
   architecture sim of synchronizer_tb is
       constant STAGES : positive := 2;
       signal clk_dst  : std_logic := '0';
       signal clk_src  : std_logic := '0';
       signal rst      : std_logic := '1';
       signal async_in : std_logic := '0';
       signal sync_out : std_logic;
   begin
       dut : entity work.synchronizer
           generic map (STAGES => STAGES)
           port map (clk_dst => clk_dst, rst => rst, async_in => async_in, sync_out => sync_out);
 
       -- Two UNRELATED clocks: half-periods 5 ns and 7 ns drift against each other.
       clk_dst <= not clk_dst after 5 ns;
       clk_src <= not clk_src after 7 ns;
 
       stim : process
           -- Apply a stable level, wait STAGES destination edges, check sync_out follows.
           procedure drive_and_check(level : in std_logic) is
           begin
               wait until falling_edge(clk_src);
               async_in <= level;
               for k in 1 to STAGES loop
                   wait until rising_edge(clk_dst);
               end loop;
               wait for 1 ns;
               assert sync_out = level
                   report "level not synchronized after STAGES dest edges" severity error;
           end procedure;
       begin
           wait until rising_edge(clk_dst); wait for 1 ns; rst <= '0';
           assert sync_out = '0' report "reset: sync_out not 0" severity error;
 
           drive_and_check('1');
           drive_and_check('0');
           drive_and_check('1');
 
           report "synchronizer self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. The fan-out / single-flop bug beside the fix — with a metastability model

Pattern (b) is the signature failure shown as buggy vs fixed RTL, then dramatized in §7. The buggy version samples the async bit in one flop and lets more than one consumer read that first flop directly; the fixed version adds the second flop and lets only the second flop's output reach consumers. To make the difference visible in simulation we inject a metastability model on the first flop — a X/random value forced onto sync_ff1 on the sampling edge — and show that in the buggy design two consumers latch different resolved values while in the fixed design both consumers see the single, clean sync_out.

sync_bug.sv — BUGGY (1 flop, fan-out) vs FIXED (2 flops, single output)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: one flop, and TWO consumers tap it directly. If sync_ff1 is metastable,
   //        consumer A and consumer B can resolve it to DIFFERENT values -> incoherent.
   module sync_bad (
       input  logic clk_dst, rst, async_in,
       output logic consumer_a, consumer_b      // two loads, both reading the FIRST flop
   );
       logic sync_ff1;
       always_ff @(posedge clk_dst or posedge rst)
           if (rst) sync_ff1 <= 1'b0;
           else     sync_ff1 <= async_in;       // sacrificial sampler...
       assign consumer_a = sync_ff1;            // ...fanned out to A...
       assign consumer_b = sync_ff1;            // ...and B. Both see the UNSETTLED node.
   endmodule
 
   // FIXED: two adjacent flops; consumers read ONLY the second, resolved flop.
   module sync_good (
       input  logic clk_dst, rst, async_in,
       output logic consumer_a, consumer_b
   );
       (* ASYNC_REG = "TRUE" *)
       logic sync_ff1, sync_ff2;
       always_ff @(posedge clk_dst or posedge rst)
           if (rst) begin sync_ff1 <= 1'b0; sync_ff2 <= 1'b0; end
           else     begin sync_ff1 <= async_in; sync_ff2 <= sync_ff1; end  // adjacent, wire only
       assign consumer_a = sync_ff2;            // both consumers read the SAME resolved output
       assign consumer_b = sync_ff2;
   endmodule
sync_bug_tb.sv — inject metastability on ff1; buggy consumers diverge, fixed agree
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_bug_tb;
       logic clk_dst = 1'b0, rst, async_in;
       logic bad_a, bad_b, good_a, good_b;
       int   errors = 0;
 
       sync_bad  ubad  (.clk_dst(clk_dst), .rst(rst), .async_in(async_in),
                        .consumer_a(bad_a),  .consumer_b(bad_b));
       sync_good ugood (.clk_dst(clk_dst), .rst(rst), .async_in(async_in),
                        .consumer_a(good_a), .consumer_b(good_b));
 
       always #5 clk_dst = ~clk_dst;
 
       // METASTABILITY MODEL (sim-only): on the edge that samples async_in, force the
       // first flop to X so downstream loads must resolve it themselves. In the buggy
       // design both consumers read that first flop; in the fixed design they read ff2,
       // which sampled the (settled) value a period earlier.
       initial begin
           rst = 1'b1; async_in = 1'b0;
           @(posedge clk_dst); #1; rst = 1'b0;
 
           async_in = 1'b1;                     // async bit rises near the edge -> metastable ff1
           @(posedge clk_dst);
           force ubad.sync_ff1  = 1'bx;         // model the metastable first flop (buggy)
           force ugood.sync_ff1 = 1'bx;         // same event in the fixed design
           #1;
           // BUGGY: both consumers read the metastable ff1 -> they can disagree (X here).
           if (bad_a === bad_b && bad_a !== 1'bx)
               ;                                // (a lucky agree; the danger is they CAN differ)
           // FIXED: consumers read ff2, which is NOT the forced node -> they always AGREE
           //        and equal each other. That coherence is the property we assert.
           assert (good_a === good_b)
               else begin $error("FIXED consumers disagree: a=%b b=%b", good_a, good_b); errors++; end
           release ubad.sync_ff1;
           release ugood.sync_ff1;
 
           // Now let the value settle and confirm the FIXED output becomes the clean level.
           repeat (2) @(posedge clk_dst); #1;
           assert (good_a === 1'b1 && good_b === 1'b1)
               else begin $error("FIXED did not resolve to 1"); errors++; end
 
           if (errors == 0) $display("PASS: fixed synchronizer keeps consumers coherent; buggy can diverge");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg/wire; the metastability model uses force/release identically, and the self-check prints PASS/FAIL on whether the fixed consumers stay coherent.

sync_bug.v — BUGGY (1 flop, fan-out) vs FIXED (2 flops) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: one flop, two consumers tap it directly -> can resolve differently.
   module sync_bad (
       input  wire clk_dst, rst, async_in,
       output wire consumer_a, consumer_b
   );
       reg sync_ff1;
       always @(posedge clk_dst or posedge rst)
           if (rst) sync_ff1 <= 1'b0;
           else     sync_ff1 <= async_in;
       assign consumer_a = sync_ff1;            // fan-out on the FIRST flop
       assign consumer_b = sync_ff1;
   endmodule
 
   // FIXED: two adjacent flops; consumers read only the second.
   module sync_good (
       input  wire clk_dst, rst, async_in,
       output wire consumer_a, consumer_b
   );
       (* ASYNC_REG = "TRUE" *)
       reg sync_ff1, sync_ff2;
       always @(posedge clk_dst or posedge rst)
           if (rst) begin sync_ff1 <= 1'b0; sync_ff2 <= 1'b0; end
           else     begin sync_ff1 <= async_in; sync_ff2 <= sync_ff1; end
       assign consumer_a = sync_ff2;
       assign consumer_b = sync_ff2;
   endmodule
sync_bug_tb.v — inject metastability; fixed consumers stay coherent (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_bug_tb;
       reg  clk_dst, rst, async_in;
       wire bad_a, bad_b, good_a, good_b;
       integer errors;
 
       sync_bad  ubad  (.clk_dst(clk_dst), .rst(rst), .async_in(async_in),
                        .consumer_a(bad_a),  .consumer_b(bad_b));
       sync_good ugood (.clk_dst(clk_dst), .rst(rst), .async_in(async_in),
                        .consumer_a(good_a), .consumer_b(good_b));
 
       always #5 clk_dst = ~clk_dst;
 
       initial begin
           errors = 0; clk_dst = 1'b0; rst = 1'b1; async_in = 1'b0;
           @(posedge clk_dst); #1; rst = 1'b0;
 
           async_in = 1'b1;                     // rises near the edge -> metastable ff1
           @(posedge clk_dst);
           force ubad.sync_ff1  = 1'bx;         // model metastable first flop (sim-only)
           force ugood.sync_ff1 = 1'bx;
           #1;
           // FIXED consumers read ff2 (not the forced node) -> must stay equal.
           if (good_a !== good_b) begin
               $display("FAIL: fixed consumers disagree a=%b b=%b", good_a, good_b);
               errors = errors + 1;
           end
           release ubad.sync_ff1;
           release ugood.sync_ff1;
 
           repeat (2) @(posedge clk_dst); #1;
           if (!(good_a === 1'b1 && good_b === 1'b1)) begin
               $display("FAIL: fixed did not resolve to 1"); errors = errors + 1;
           end
 
           if (errors == 0) $display("PASS: fixed keeps consumers coherent; buggy can diverge");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the metastability model forces 'X' onto the first flop through the DUT's signal; the fixed design's consumers read the second flop, so the assertion that they remain equal holds regardless of the forced first-flop value.

sync_bug.vhd — BUGGY (1 flop, fan-out) vs FIXED (2 flops) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   -- BUGGY: one flop, two consumers read it directly.
   entity sync_bad is
       port ( clk_dst, rst, async_in : in  std_logic;
              consumer_a, consumer_b  : out std_logic );
   end entity;
   architecture rtl of sync_bad is
       signal sync_ff1 : std_logic;
   begin
       process (clk_dst, rst) begin
           if rst = '1' then sync_ff1 <= '0';
           elsif rising_edge(clk_dst) then sync_ff1 <= async_in; end if;
       end process;
       consumer_a <= sync_ff1;                   -- fan-out on the FIRST flop
       consumer_b <= sync_ff1;
   end architecture;
 
   -- FIXED: two adjacent flops; consumers read only the second.
   library ieee;
   use ieee.std_logic_1164.all;
   entity sync_good is
       port ( clk_dst, rst, async_in : in  std_logic;
              consumer_a, consumer_b  : out std_logic );
   end entity;
   architecture rtl of sync_good is
       signal sync_ff1, sync_ff2 : std_logic;
       attribute ASYNC_REG : string;
       attribute ASYNC_REG of sync_ff1 : signal is "TRUE";
       attribute ASYNC_REG of sync_ff2 : signal is "TRUE";
   begin
       process (clk_dst, rst) begin
           if rst = '1' then sync_ff1 <= '0'; sync_ff2 <= '0';
           elsif rising_edge(clk_dst) then
               sync_ff1 <= async_in; sync_ff2 <= sync_ff1;   -- adjacent, wire only
           end if;
       end process;
       consumer_a <= sync_ff2;                   -- both read the SAME resolved output
       consumer_b <= sync_ff2;
   end architecture;
sync_bug_tb.vhd — inject metastability; fixed consumers stay coherent (assert severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity sync_bug_tb is
   end entity;
 
   architecture sim of sync_bug_tb is
       signal clk_dst  : std_logic := '0';
       signal rst      : std_logic := '1';
       signal async_in : std_logic := '0';
       signal good_a, good_b : std_logic;
   begin
       -- Only the FIXED design is instantiated here; its coherence is the property under test.
       ugood : entity work.sync_good
           port map (clk_dst => clk_dst, rst => rst, async_in => async_in,
                     consumer_a => good_a, consumer_b => good_b);
 
       clk_dst <= not clk_dst after 5 ns;
 
       stim : process
       begin
           wait until rising_edge(clk_dst); wait for 1 ns; rst <= '0';
           async_in <= '1';                      -- rises near the edge
           wait until rising_edge(clk_dst); wait for 1 ns;
           -- The fixed consumers both read sync_ff2; even if ff1 were metastable, they
           -- read the SAME node and therefore agree with each other.
           assert good_a = good_b
               report "fixed consumers disagree (should read same ff2)" severity error;
 
           -- Let it settle and confirm the fixed output resolves to the applied level.
           for k in 1 to 2 loop wait until rising_edge(clk_dst); end loop;
           wait for 1 ns;
           assert good_a = '1' and good_b = '1'
               report "fixed did not resolve to 1" severity error;
 
           report "sync_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: sample the async bit in one sacrificial flop, then let a second destination-domain flop — and only the second flop — resolve it, with nothing tapping the first flop and nothing between the two, so a metastable event has a full period to decay before any logic reads a coherent value.

Synthesis & Trade-offs

The synchronizer's correctness lives as much in what the tool does to it as in the RTL, so the synthesis story is not optional here — it is the pattern.

  • What it infers, and the ASYNC_REG attribute that protects it. Each stage is a single flip-flop; the chain is STAGES flops in series on clk_dst. Left alone, a synthesis or place-and-route tool may do exactly the wrong things to it: retime logic across the flops, duplicate (fan-out-optimize) the first flop to drive multiple loads, or absorb it into surrounding logic — any of which reintroduces combinational delay into the settling window or spreads the metastable node. The ASYNC_REG = "TRUE" attribute (ASYNC_REG on Xilinx/AMD, async_reg/preserve and equivalents elsewhere) tells the tool: these are synchronizer flops — do not retime, optimize, or duplicate them, and place them physically adjacent so the wire between stages is short and the full period is settling time. On an FPGA this also asks the placer to pack the flops in the same slice; on an ASIC the equivalent is a dont_touch/size_only plus a placement keep-together. Marking the synchronizer is not cosmetic — an un-attributed chain that the tool retimes or fans out is a silent CDC hole.
  • Two flops vs three — the MTBF/latency trade-off. Two flops grant one destination clock period of settling and give an MTBF typically in centuries — ample for most designs. But MTBF falls as clock frequency rises (less absolute settling time per period) and as the crossing toggles more often (more chances to hit the window). For a high-frequency clock or a high-reliability part (aerospace, medical, safety), add a third flop: it grants a second full settling period, and because the failure probability is exponential in settling time, one more flop multiplies the MTBF dramatically. The cost is exactly one more cycle of latency: a 3-flop synchronizer delivers sync_out three destination cycles after the input instead of two. So the knob is MTBF-vs-latency: buy reliability by the flop, pay for it by the cycle.
  • The single-bit-level limitation — why this is not a general CDC tool. Every stage resolves its bit independently, which is exactly why the structure is safe for one bit of level: there is no cross-bit relationship to break. Feed it a multi-bit bus synchronized bit-by-bit and that independence becomes the bug — different bits can resolve on different cycles, so a downstream reader can sample a value that was never a valid source word (the multi-bit CDC problem, 11.3, solved with gray coding or a handshake). Feed it a single-cycle pulse and the destination may simply never have an edge while the pulse is high, missing it entirely (11.4, solved with a toggle/pulse synchronizer). Its contract is precise: one bit, level/control, from a clean source flop. Anything wider or narrower needs a different structure — which is exactly why the rest of Chapter 11 exists.
  • Reset and power-up. The synchronizer here uses an asynchronous, active-high reset so it powers up in a known state (sync_out = 0); if your reset itself crosses domains, the reset deassertion must itself be synchronized (a reset synchronizer) so it does not release the flops metastably — a direct application of this same pattern to the reset net.

5. Verification Strategy

A synchronizer's correctness is a timing-and-coherence property, not a truth table, so verification must run two unrelated clocks and check both the functional contract (right value, right latency) and the invariants that keep the metastability contained. The single sentence that captures a correct synchronizer is:

A stable single-bit level applied in the source domain appears at sync_out, clean and correct, exactly STAGES destination-clock cycles later; no logic ever reads any stage but the last; and even when the first flop is modelled as metastable, every consumer of the output sees the same resolved value.

Everything below makes that checkable and maps onto the testbenches in §4.

  • Two-async-clock, self-checking testbench. Generate clk_dst and clk_src with co-prime (unrelated) periods so their edges drift, exactly as a real crossing does. Drive async_in from the source domain, then assert sync_out follows a stable level after STAGES destination edges — the §4a testbenches do this in all three HDLs (SV assert (sync_out === level), Verilog if (sync_out !== level) $display("FAIL…"), VHDL assert sync_out = level … severity error). This proves the functional path and the two-cycle latency.
  • Model metastability on stage 1, then check the output stays coherent. You cannot simulate analog metastability directly, but you can model its consequence: on the sampling edge, force sync_ff1 to X (or a random 0/1) and confirm that (i) in the correct design nothing but sync_ff2 reads sync_ff1, so every consumer of sync_out still sees a single coherent value, and (ii) once the level settles, sync_out resolves to the applied level. The §4b testbenches inject exactly this and assert the fixed design's two consumers never disagree — the property the buggy, fanned-out design violates.
  • Invariants, stated conceptually. Three properties must hold: (i) only the final stage drives logic — no net outside the synchronizer connects to sync[0..STAGES-2] (a structural/lint check, not a simulation one); (ii) no combinational logic between stages — the path from each stage to the next is a single flop-to-flop connection, so the full period is settling time; (iii) output coherence — every reader of sync_out sees the same value on the same cycle (guaranteed because they all read one flop). These are the invariants a CDC tool enforces automatically.
  • Lint / CDC-tool intent. A structural CDC checker (Spyglass-CDC, Questa-CDC, or equivalent) is the primary verification for this pattern, above any testbench: it identifies every net that crosses from one clock domain to another and flags any single-bit crossing that is not a registered synchronizer — a direct combinational crossing, a synchronizer with fan-out on stage 1, or logic inserted between stages. The rule the tool encodes is "every async single-bit crossing must be a two-(or-more-)flop synchronizer, and stage 1 must have exactly one load." Treat a CDC-clean report as a gating sign-off criterion, not an optional nicety.
  • Corner cases to exercise. A level that changes right at a destination edge (the metastability-inducing case, modelled by the force); back-to-back level changes faster than STAGES cycles (confirm the output simply tracks the most recent stable level with STAGES-cycle latency, since a level has no per-transition obligation); reset asserted and deasserted during operation (output returns to its reset value cleanly); and STAGES = 3 (confirm the latency becomes three cycles and the contract is otherwise unchanged).
  • Expected waveform. On a correct run, a source-domain level change appears at sync_ff1 on the next clk_dst edge, propagates to sync_ff2/sync_out one edge later, and holds — a clean two-edge shift with no glitch. The visual signature of the fan-out bug is two nominally-identical consumer signals momentarily showing different values on the sampling edge; the signature of logic-between-flops is sync_out occasionally glitching or resolving late because the settling period was spent in gates.

6. Common Mistakes

Fan-out from the first flop. The centerpiece failure. If any load other than sync_ff2 taps sync_ff1 — an "early" enable, a status bit, a second consumer — that load reads the still-settling node, and because two gates can resolve a metastable voltage at different thresholds, the taps disagree: one branch sees 1, another sees 0, and the design goes incoherent. The metastability the synchronizer was built to contain is now spread to every fan-out load. The rule is absolute: sync_ff1 drives exactly one thing, sync_ff2, and every consumer reads sync_out.

Combinational logic between the two flops. Putting an AND, a mux, or any gate on the path from sync_ff1 to sync_ff2 steals the settling time. The whole value of the second flop is that it samples a full period after the first — but if the metastable value must also propagate through combinational logic, it arrives at the second flop later and still unsettled, so sync_ff2 can itself go metastable. Between the two flops there is nothing but wire; do any required logic on sync_out, after the chain, never inside it.

Synchronizing in the wrong (source) domain. Placing the flops on clk_src instead of clk_dst buys nothing: the resolution time that matters is a destination clock period, because it is the destination flop that samples the async edge and can go metastable. A synchronizer clocked by the source domain resolves against the wrong clock and leaves the true crossing — into the destination domain — completely unprotected. Both flops must be clocked by the destination clock, on the receiving side of the boundary.

Synchronizing a multi-bit bus bit-by-bit. Running each bit of a bus through its own two-flop synchronizer looks like a natural generalization and is a classic disaster: each bit resolves independently, so on a transition the bits can update on different destination cycles and the receiver latches an intermediate value that was never a valid source word. A single-bit synchronizer is for one bit of level; a bus needs gray coding (so only one bit changes per step) or a handshake with a data hold — that is the multi-bit CDC problem of 11.3, not this pattern.

Missing the ASYNC_REG attribute (letting the tool retime, duplicate, or absorb the flops). Without the synchronizer flops being marked, a synthesis/PnR tool is free to retime logic across them, duplicate the first flop to reduce fan-out delay (spreading the metastable node — the exact thing you must prevent), or optimize the "redundant" second flop away. Any of these silently reopens the CDC hole while the RTL still looks correct. Mark the chain (ASYNC_REG/dont_touch/preserve) so the tool keeps the flops adjacent and intact, and rely on a CDC tool to confirm it.

Treating a synchronizer as latency-free. sync_out is valid two destination cycles after the input (three for a 3-flop chain). Logic that assumes the synchronized bit is available the same cycle it changed — a handshake that expects an immediate ack, a counter that expects the enable "now" — will be off by the synchronizer's latency. Budget the two-cycle (or three-cycle) delay into the protocol around every crossing.

7. DebugLab

The enable that was two enables — metastability spread by a first-flop tap and by logic between the flops

The engineering lesson: the two-flop synchronizer works only because the whole period between the flops is pure, uninterrupted settling time and the still-metastable first flop drives nothing but the second flop — steal that time with logic, or leak that node with fan-out, and you spread the very metastability the structure exists to contain. The tell in both cases is a bug that bites once in millions of crossings, only when the async signal is changing, never reproducibly, and gets worse at higher clock frequency — the fingerprint of a CDC resolution failure rather than an ordinary data-path bug. Two habits make both failures impossible, and they are identical across SystemVerilog, Verilog, and VHDL: let only the last stage drive logic — no fan-out from any earlier stage — and keep the stages strictly adjacent with nothing but wire between them, doing every gate on the synchronized output; then mark the flops ASYNC_REG and run a CDC checker so the tool cannot recreate the fan-out and the reviewer cannot miss the inserted logic.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "settling time is sacred, only the last stage is trusted" discipline.

Exercise 1 — Trace the clean crossing

For a two-flop synchronizer on clk_dst, an async single-bit level rises to 1 and stays there. Draw the values of async_in, sync_ff1, sync_ff2, and sync_out across the next four clk_dst edges (assume the input is stable and the first flop happens to resolve cleanly). State the exact number of destination-clock cycles of latency from the input change to sync_out, and explain in one line why the second flop, not the first, is the one the rest of the logic may read.

Exercise 2 — Find the metastability leak

Two engineers synchronize an async ready. Engineer A writes sync_ff1 <= ready; sync_ff2 <= sync_ff1; and drives everything from sync_ff2. Engineer B writes the same two flops but taps sync_ff1 for a fast "pre-ready" and sync_ff2 for the main path. Both simulate identically in RTL. Explain (i) which design has a CDC hole and precisely where, (ii) what the failure looks like in silicon and why it is intermittent and frequency-dependent, and (iii) how B should get an "early" indication without tapping sync_ff1.

Exercise 3 — Size the chain

A crossing that currently uses two flops is being ported to a part where clk_dst runs three times faster and the reliability target is ten times stricter. State qualitatively what happens to the MTBF of the existing two-flop synchronizer, whether you would move to three flops, and the exact latency cost of doing so. Then explain why adding a third flop helps so much more than, say, widening the flops or slowing only the source clock.

Exercise 4 — Reject the wrong structures

For each signal, say whether a plain two-flop synchronizer is correct, and if not, what structure you would use instead and why: (a) a single-bit async mode level; (b) an 8-bit async configuration bus that changes occasionally; (c) a single-cycle async start pulse that must not be lost; (d) an async reset deassertion. (Hint: level vs bus vs pulse vs reset each has a different CDC contract.)

Continue in Chapter 11 — Clock Domain Crossing (prerequisite and forward links unlock as they ship):

  • Metastability — Chapter 11.1; the direct prerequisite — why an async crossing inevitably drives a flop metastable, the setup/hold window, and the exp(-t / tau) decay this synchronizer exploits by granting a full period of settling time.
  • The Multi-Bit CDC Problem — Chapter 11.3; why this single-bit structure cannot cross a bus (independent per-bit resolution), and the gray-code / handshake structures that can.
  • Pulse / Handshake Synchronization — Chapter 11.4; how to cross a single-cycle pulse (which a level synchronizer can miss) with a toggle or a full req/ack handshake.
  • Gray-Coded Pointer Synchronization — Chapter 11.5; synchronizing a multi-bit FIFO pointer by making only one bit change per step, so a two-flop synchronizer can carry each bit safely.
  • Asynchronous FIFO — Chapter 11.6; the dual-clock FIFO whose gray-coded pointers are carried across the boundary by exactly this two-flop synchronizer.
  • CDC Design Rules — the checklist that makes "every async single-bit crossing is a registered synchronizer with no stage-1 fan-out" an enforced sign-off rule.

Backward / in-track dependencies:

  • The Register Pattern (D-FF) — Chapter 2.1; the single flop this whole structure is two of — the clocked-update, reset-first discipline the synchronizer chain is built from.
  • Shift Registers — Chapter 3.1; the synchronizer chain is a short shift register on the crossing bit, which is why {sync[STAGES-2:0], async_in} builds an N-stage synchronizer.
  • Reset Strategy — Chapter 2.5; how the synchronizer is reset into a defined state, and why a reset that itself crosses domains must be deasserted through this same synchronizer pattern.
  • Asynchronous Reset — the async-reset flop the synchronizer uses so it powers up defined, and the reset-synchronizer application of this pattern.
  • valid/ready Handshake — Chapter 9.1; the handshake that carries a multi-bit payload across a crossing while only a single-bit request/ack goes through a synchronizer like this one.
  • Synchronous FIFO Architecture — Chapter 7.1; the single-clock FIFO whose pointers become gray-coded and synchronized (via this pattern) to build the async FIFO.

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

  • Blocking and Non-Blocking Assignments — why the synchronizer flops update with non-blocking (<=) so the chain shifts one stage per edge rather than collapsing.
  • Initial and Always Blocks — the clocked always @(posedge clk_dst) process the synchronizer is built in, and the reset branch it opens with.
  • If-Else Statements — the reset-then-update structure of the clocked block, and the conditional gating of consumers on the synchronized output.
  • Timing Checks — the setup/hold checks whose violation is metastability, the phenomenon the two flops are built to tolerate.

11. Summary

  • Metastability at an async crossing is inevitable; the synchronizer makes its consequences astronomically improbable. 11.1 proved you cannot prevent a destination flop from going metastable when it samples an unrelated-clock signal. The two-flop synchronizer does not prevent it — it contains it: sample in a sacrificial first flop, then give that flop a full destination clock period to settle before a second flop re-samples it, so the value the rest of the logic reads (sync_out) is practically always a clean, resolved 0 or 1. MTBF is set by that settling period.
  • Two flops, both in the destination domain, strictly adjacent, first flop fanning out only to the second. Both flops are clocked by clk_dst (a source-domain flop buys no settling time); there is nothing but wire between sync_ff1 and sync_ff2 (any logic steals the settling window and re-metastabilizes the second flop); and only sync_ff2 loads sync_ff1 (any other tap reads the still-settling node and spreads the metastability). The source is a clean single-bit level from one source-domain flop.
  • It synchronizes one bit of level, and costs two cycles of latency. The structure is safe only for a single-bit level/control signal, because each bit resolves independently — a multi-bit bus synchronized bit-by-bit can present a word that was never sent (11.3, gray code / handshake) and a one-cycle pulse can be missed entirely (11.4). sync_out is valid two destination-clock cycles after the input.
  • Two vs three flops is an MTBF/latency knob, and ASYNC_REG protects the structure. Three flops grant a second settling period for a much higher MTBF (needed at high frequency or high reliability) at the cost of one more cycle of latency. Mark the chain ASYNC_REG/dont_touch so synthesis and place-and-route cannot retime, duplicate, or absorb the flops — which would silently reopen the CDC hole — and treat a clean CDC-tool report as a gating sign-off.
  • The signature failures are fan-out from the first flop and logic between the flops. Both spread or re-introduce the metastability the structure exists to contain; both bite once in millions of crossings, only while the signal is changing, unreproducibly, and worse at higher clock frequency — the fingerprint of a CDC bug. Let only the last stage drive logic, keep the stages adjacent, do every gate on sync_out, and verify with a two-async-clock self-checking testbench plus a CDC checker. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL.