Skip to content

RTL Design Patterns · Chapter 11 · Clock Domain Crossing

Metastability

A flip-flop only latches a clean 0 or 1 when its data input is stable across the setup and hold window around the clock edge. Violate that window and the output can go metastable, hovering at an intermediate voltage for an unbounded time before it resolves randomly. Inside one clock domain, static timing analysis proves the window is always met, so metastability never happens. But a signal from an asynchronous clock has no fixed phase relationship to yours, so sooner or later it changes right at the edge and the sampling flop goes metastable. This is inevitable, not a rare fluke. You learn the setup and hold window, why more settling time buys exponentially better mean time between failures, and how a metastable value corrupts logic when it fans out, in SystemVerilog, Verilog, and VHDL.

Advanced16 min readRTL Design PatternsMetastabilityClock Domain CrossingSetup Hold ViolationMTBFSynchronizer

Chapter 11 · Section 11.1 · Clock Domain Crossing

1. The Engineering Problem

You have two blocks on the same die that do not share a clock. A processor core runs on clk_a; a peripheral runs on clk_b, generated by a different PLL at an unrelated frequency. The core needs to hand the peripheral a single control bit — call it async_in, an enable that goes high when the core wants the peripheral to start. The obvious thing works in simulation and looks completely reasonable: register async_in with one flop clocked by clk_b, and use that registered value in the peripheral's state machine. One flop. It compiles, it lints clean, it passes every directed test. You ship it.

And most of the time it works. But every so often — once an hour, once a day, once a week, at a rate that seems to change when the lab warms up — the peripheral does something impossible: its state machine lands in a state it has no transition into, or its byte counter jumps by two, or it starts on a cycle it should not have. The failure never reproduces on demand. It vanishes when you add a probe. It is worse at high temperature and at low voltage. It looks, for all the world, like cosmic rays or a bad solder joint. It is neither. It is the one flop.

Here is why that flop is not safe, and why no amount of careful RTL around it makes it safe. A flip-flop only guarantees a clean output if its data input is stable across the setup/hold window — a sliver of time bracketing the active clock edge (setup before, hold after). Inside a single clock domain that guarantee is free: every launch-to-capture path is timed, and static timing analysis proves the data settles with margin before every edge (this is the setup/hold analysis from Verilog v1). But async_in is launched by clk_a, which has no timing relationship whatsoever to clk_b. Its transitions land at a uniformly random phase relative to the clk_b edge. Across enough edges, one transition must fall inside the setup/hold window of the capturing flop — and when it does, the flop's output is not guaranteed to be a clean 0 or 1 by the next edge. It can go metastable: hover at an intermediate voltage, neither 0 nor 1, for an unbounded time, and then resolve randomly. That metastable, or late-resolved, value flows straight into the state machine, and the state machine does something illegal. The bug is not rare bad luck; it is the guaranteed consequence of sampling an asynchronous signal with an unprotected flop, and the only question is how often.

the-need.v — one flop sampling an asynchronous bit is a time bomb
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // clk_a and clk_b are ASYNCHRONOUS: unrelated PLLs, no fixed phase.
   // async_in is launched by clk_a; we sample it in the clk_b domain.
 
   // WRONG — a single flop captures an async bit and feeds logic directly:
   always @(posedge clk_b)
       meta <= async_in;          // async_in can change INSIDE clk_b's setup/hold window
   // ...then meta drives the FSM immediately:
   //   next_state = f(meta, ...); // if meta is metastable/late => illegal transition
   //
   // async_in has no timing arc to clk_b, so STA CANNOT prove setup/hold here.
   // Sooner or later a transition lands in the window and 'meta' goes METASTABLE.
   // This is INEVITABLE given enough edges — not a fluke. The fix (11.2) is to give
   // that first flop a full clock period to resolve before anything reads it.

2. Mental Model

3. Pattern Anatomy

Metastability is not a module you instantiate — it is a failure mode with a precise anatomy, and understanding its parts is what lets you engineer around it.

The setup/hold window and its violation. Every flop specifies a setup time (data must be stable this long before the edge) and a hold time (stable this long after). Together they bracket the edge in a small forbidden window. If the data input is stable throughout that window, the flop resolves to a clean 0 or 1 by the next edge, on time. If the data transitions inside the window — a setup/hold violation — the flop enters an indeterminate region: its output may be a clean 0, a clean 1, or, in the worst case, a value stuck between them. In a synchronous domain, STA guarantees the window is never violated; across an asynchronous crossing, nothing does, so a violation is only a matter of edges.

The setup/hold window around the clk_b edge — a clean sample vs an async violation

data flow
The setup/hold window around the clk_b edge — a clean sample vs an async violationstableasyncno marginsetup windowD must be stable BEFORE the edgeclk_b edgethe instant the flop samples Dhold windowD must be stable AFTER the edgestable in windowresolves to a clean 0 or 1, on timeasync D changestransition inside window => VIOLATIONmetastable outoutput hovers at Vmid, resolves late
The setup and hold windows bracket the active edge in a small forbidden interval. Inside one clock domain, STA proves the data (D) is stable across that window on every path, so the flop always resolves to a clean 0 or 1 on time. A signal from an asynchronous domain changes at a random phase, so eventually a transition lands inside the window: a setup/hold violation with no timing margin. On that edge the flop is not guaranteed to settle by the next edge and can go metastable, hovering at an intermediate voltage. This window is language-independent — the same physics governs SystemVerilog, Verilog, and VHDL RTL once it is placed and timed.

The metastable trajectory and the time constant tau. A metastable node does not stay balanced forever — it resolves, but on its own schedule. The imbalance between the two logic levels grows exponentially in time with a device time constant called tau (set by the flop's regeneration gain, roughly its bandwidth). Starting from a tiny initial offset near the intermediate voltage Vmid, the node's separation from Vmid grows like e^(t/tau), so the probability that it is still unresolved after a settling time t_settle falls off like e^(-t_settle/tau). Longer settling time means exponentially lower odds of remaining metastable. tau is a few tens of picoseconds in a modern process; a full clock period of settling is many tau, which is why one period already resolves almost every event.

The metastable output over time — hover at Vmid, then resolve exponentially

data flow
The metastable output over time — hover at Vmid, then resolve exponentiallyregenrandomrandomsettlingviolating sampleD changes in the window at the edgehover at Vmidoutput stuck near the intermediate levelgrows ~ e^(t/tau)imbalance regenerates with time const tauresolves to 0random outcome — one of two wellsresolves to 1random outcome — the other wellresidual probP(still meta) ~ e^(-t_settle/tau)
On a violating sample the flop output is left near the intermediate voltage Vmid, in neither logic well. It does not stay there: the imbalance regenerates exponentially with the device time constant tau, and the node eventually topples — randomly — to 0 or to 1. The longer you allow it to settle before anything reads it, the lower the chance it is still hovering: the residual probability falls like e^(-t_settle/tau). This is why more settling time (a synchronizer, 11.2) buys exponentially better reliability, and why you can never drive the probability to exactly zero — only make it astronomically small.

MTBF — the reliability number you actually design to. You cannot promise metastability never happens; you promise it is rarer than any failure that matters. That promise is a mean time between failures: MTBF ~ e^(t_settle/tau) / (T0 * f_clk * f_data), where f_clk is the receiving clock rate, f_data is the rate the async input toggles, T0 and tau are device constants, and t_settle is the resolution time you allow before the value is used. The numerator is the exponential you control — every extra bit of settling time multiplies MTBF by a huge factor. The intuition to carry: one flop with a full clock period of settling already resolves the vast majority of events, and a second flop (the two-flop synchronizer of 11.2) gives the first flop's rare residual metastability a whole additional period to die out before any logic sees it, pushing MTBF from seconds to millions of years. That is not eliminating metastability — the first flop still occasionally goes metastable — it is giving it enough time that it never escapes into the design.

Why a metastable value is dangerous — fan-out corruption. The subtle killer is not that a metastable value is wrong; it is that it can be read inconsistently. A node sitting at the intermediate voltage is, to the analog gates that read it, an ambiguous level. If that one node fans out to two downstream gates before it resolves, one gate may interpret it as a logic 1 and the other as a logic 0 — the same node read as opposite values in the same cycle. Now the design holds two contradictory beliefs about one bit: a state register might advance while its own enable logic thinks it should not, or a counter might increment on one path and hold on another. This is worse than a wrong value; it is an incoherent state, and it is why the discipline is not merely "synchronize the crossing" but "let the value resolve to a clean level before it fans out to anything."

4. Real RTL Implementation

This is the core of the page, and it demands unusual honesty about what is real hardware and what is a simulation trick. Metastability itself is not synthesizable behaviour — there is no RTL construct that makes a flop hover at Vmid; that is an analog, timing-level phenomenon that only exists in silicon and in gate-level simulation with real delays and setup/hold checks. So §4 has two distinct kinds of code, and every block is labelled:

  • (i) the synthesizable CDC-capture flop (RTL) — an ordinary destination-domain flop sampling an asynchronous input. This is real hardware, and it is exactly the flop that would go metastable in silicon. It compiles and synthesizes.
  • (ii) a behavioral metastability MODEL (simulation only) — a testbench that runs two asynchronous clocks and, when the source transitions near the destination edge, injects the hazard: it drives the captured bit to X (or a $random 0/1) to stand in for the metastable event a zero-delay RTL sim cannot produce on its own. This lets us demonstrate a single-flop capture corrupting a downstream counter, and prove the corruption is observable. It is not synthesizable and is labelled so in every language.

Each pattern is shown in SystemVerilog, Verilog, and VHDL; the RTL is identical in intent, and the model is the same trick spelled three ways. The point of the model is not to claim you can simulate metastability faithfully — you cannot in RTL — but to make the consequence (downstream corruption) visible and self-checkable, so the value of the synchronizer in 11.2 is concrete rather than asserted.

4a. The synthesizable CDC-capture flop — real hardware, three ways

The capture flop is deliberately unremarkable: a single flop in the clk_b domain sampling async_in. That ordinariness is the point — it is correct-looking RTL that is unsafe by construction, because its data input has no timing relationship to its clock. This is the flop the rest of the page is about.

cdc_capture.sv — RTL (synthesizable): a single dest-domain flop samples an async input
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module cdc_capture #(
       parameter int WIDTH = 1                  // a CDC control is usually a single bit
   )(
       input  logic             clk_b,          // DESTINATION clock (unrelated to the source clock)
       input  logic [WIDTH-1:0] async_in,       // launched by another clock => async to clk_b
       output logic [WIDTH-1:0] meta            // first-stage capture: THIS is the flop that can go metastable
   );
       // Ordinary, synthesizable flop. Nothing here is wrong syntactically or
       // functionally -- but async_in has NO timing arc to clk_b, so STA cannot
       // prove setup/hold, and in silicon 'meta' can be sampled mid-transition
       // and go metastable. The fix is NOT in this flop; it is to give 'meta' a
       // full period to resolve before use (the two-flop synchronizer, 11.2).
       always_ff @(posedge clk_b)
           meta <= async_in;
   endmodule

A quick self-checking functional testbench proves the flop is a perfectly ordinary, correct register when its input is synchronous to clk_b — which is exactly the point: the RTL is not wrong, the usage (an asynchronous input) is. Metastability cannot appear here because the stimulus is driven synchronously, so this passes cleanly; the hazard only exists once the input is genuinely asynchronous (the §4b model).

cdc_capture_tb.sv — self-checking (functional): the flop registers a SYNCHRONOUS input correctly
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module cdc_capture_tb;
       logic clk_b = 1'b0, async_in, meta;
       int   errors = 0;
 
       cdc_capture #(.WIDTH(1)) dut (.clk_b(clk_b), .async_in(async_in), .meta(meta));
 
       always #5 clk_b = ~clk_b;
 
       initial begin
           async_in = 1'b0;
           // Drive the input SYNCHRONOUSLY (away from the edge) -> no violation possible.
           for (int t = 0; t < 16; t++) begin
               @(negedge clk_b); async_in = t[0];   // change data on the inactive edge
               @(posedge clk_b); #1;                // one edge later meta must equal it
               assert (meta === async_in)
                   else begin $error("t=%0d meta=%b exp=%b", t, meta, async_in); errors++; end
           end
           if (errors == 0) $display("PASS: capture flop registers a synchronous input (in-domain, no metastability)");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule
cdc_capture.v — RTL (synthesizable): the same single capture flop in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module cdc_capture #(
       parameter WIDTH = 1
   )(
       input  wire              clk_b,          // destination clock
       input  wire [WIDTH-1:0]  async_in,       // async to clk_b
       output reg  [WIDTH-1:0]  meta            // first-stage capture (can go metastable in silicon)
   );
       // Synthesizable and correct-looking; unsafe because async_in is unrelated
       // to clk_b. This flop is exactly the one a two-flop synchronizer protects.
       always @(posedge clk_b)
           meta <= async_in;
   endmodule
cdc_capture.vhd — RTL (synthesizable): the capture flop in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity cdc_capture is
       generic ( WIDTH : positive := 1 );                      -- CDC control is typically 1 bit
       port (
           clk_b    : in  std_logic;                           -- destination clock
           async_in : in  std_logic_vector(WIDTH-1 downto 0);  -- async to clk_b
           meta     : out std_logic_vector(WIDTH-1 downto 0)   -- first-stage capture flop
       );
   end entity;
 
   architecture rtl of cdc_capture is
   begin
       -- Ordinary rising-edge flop. Synthesizable; unsafe by construction because
       -- async_in has no timing relationship to clk_b, so meta can go metastable.
       process (clk_b)
       begin
           if rising_edge(clk_b) then
               meta <= async_in;
           end if;
       end process;
   end architecture;

The VHDL self-checking functional testbench mirrors the SystemVerilog one: drive the input synchronously (on the falling edge, away from the rising sample) and assert the flop registers it correctly. It passes because there is no violation — the flop is fine in-domain; the hazard is the asynchronous usage, which the §4b model injects.

cdc_capture_tb.vhd — self-checking (functional): registers a SYNCHRONOUS input (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity cdc_capture_tb is
   end entity;
 
   architecture sim of cdc_capture_tb is
       signal clk_b    : std_logic := '0';
       signal async_in : std_logic_vector(0 downto 0) := "0";
       signal meta     : std_logic_vector(0 downto 0);
   begin
       dut : entity work.cdc_capture
           generic map (WIDTH => 1)
           port map (clk_b => clk_b, async_in => async_in, meta => meta);
 
       clk_b <= not clk_b after 5 ns;
 
       stim : process
           variable bit_v : std_logic;
       begin
           for t in 0 to 15 loop
               wait until falling_edge(clk_b);          -- change data away from the sampling edge
               if (t mod 2) = 1 then bit_v := '1'; else bit_v := '0'; end if;
               async_in(0) <= bit_v;
               wait until rising_edge(clk_b); wait for 1 ns;
               assert meta(0) = bit_v
                   report "capture flop mismatch on synchronous input" severity error;
           end loop;
           report "cdc_capture self-check complete (in-domain, no metastability)" severity note;
           wait;
       end process;
   end architecture;

4b. A behavioral metastability MODEL — inject the hazard, show the downstream corruption

Now the simulation-only part. A zero-delay RTL sim will never make meta metastable on its own, so the buggy single-flop capture and a safe one look identical in a plain testbench — which is exactly why this bug ships. To make the hazard visible and self-checkable, the testbench below runs clk_a and clk_b at asymmetric periods (so their edges drift through every phase), toggles async_in on clk_a, and — when a source toggle lands near a clk_b edge — injects a metastable value into the captured bit (an X, or a $random 0/1) instead of the clean sample. It then feeds that captured bit straight into a small counter and checks that the counter diverges from a golden count that used a properly resolved (two-flop) value. The check PASSES when the corruption is observed, proving the single-flop path is unsafe. Everything here is a model, clearly not synthesizable.

meta_model_tb.sv — SIMULATION MODEL (not synthesizable): inject metastability, prove downstream corruption
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module meta_model_tb;
       // Two ASYNCHRONOUS clocks: asymmetric periods so clk_b edges drift through
       // every phase of clk_a -- eventually a source toggle lands near a clk_b edge.
       logic clk_a = 0, clk_b = 0;
       always #5   clk_a = ~clk_a;               // source-domain clock (100 MHz-ish)
       always #7   clk_b = ~clk_b;               // dest-domain clock (unrelated period)
 
       logic async_in = 0;                       // control bit launched by clk_a
       always @(posedge clk_a) async_in <= ~async_in;   // toggles every source edge
 
       // --- The UNSAFE single-flop capture, with a metastability INJECTION model ---
       // In real silicon this flop would go metastable when async_in changes in the
       // setup/hold window. RTL cannot produce that, so we MODEL it: if the source
       // toggled within a small window of this edge, drive 'meta' to a random 0/1
       // (X in a 4-state sim) instead of the clean value. This is a sim trick only.
       logic meta, prev_async;
       time  last_toggle;
       always @(posedge clk_a) last_toggle = $time;      // remember when async_in changed
       always @(posedge clk_b) begin
           if (($time - last_toggle) < 2)                // toggle landed "in the window"
               meta <= $random & 1'b1;                   // MODEL: metastable -> random resolve
           else
               meta <= async_in;                         // clean sample otherwise
       end
 
       // Downstream logic reads the (possibly metastable) bit DIRECTLY -- the bug.
       int cnt_bad;
       always @(posedge clk_b) if (meta) cnt_bad <= cnt_bad + 1;
 
       // GOLDEN path: a two-flop synchronizer gives 'meta' a full period to resolve,
       // so the value the counter sees is always clean (this is the 11.2 fix, modeled).
       logic s1, s2; int cnt_good;
       always @(posedge clk_b) begin s1 <= async_in; s2 <= s1; end
       always @(posedge clk_b) if (s2) cnt_good <= cnt_good + 1;
 
       int errors = 0;
       initial begin
           cnt_bad = 0; cnt_good = 0;
           repeat (2000) @(posedge clk_b);
           // The single-flop path MUST diverge from the clean path -- that divergence
           // IS the corruption a metastable capture causes. Observing it => PASS.
           if (cnt_bad !== cnt_good)
               $display("PASS: single-flop capture corrupted the count (bad=%0d good=%0d) -> metastability is observable", cnt_bad, cnt_good);
           else begin
               $display("FAIL: no divergence observed -- the injection model did not fire"); errors++;
           end
           $finish;
       end
   endmodule
meta_model_tb.v — SIMULATION MODEL (not synthesizable): Verilog metastability injection + self-check
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module meta_model_tb;
       reg clk_a, clk_b;
       initial begin clk_a = 0; clk_b = 0; end
       always #5 clk_a = ~clk_a;                 // source clock
       always #7 clk_b = ~clk_b;                 // async destination clock
 
       reg async_in;
       initial async_in = 0;
       always @(posedge clk_a) async_in <= ~async_in;
 
       // UNSAFE capture with metastability MODEL: if the async toggle landed near
       // this edge, drive a random value instead of a clean sample (sim trick only).
       reg meta;
       time last_toggle; initial last_toggle = 0;
       always @(posedge clk_a) last_toggle = $time;
       always @(posedge clk_b) begin
           if (($time - last_toggle) < 2)
               meta <= $random;                  // MODEL: metastable -> random resolve (LSB used)
           else
               meta <= async_in;
       end
 
       integer cnt_bad;  initial cnt_bad = 0;
       always @(posedge clk_b) if (meta) cnt_bad <= cnt_bad + 1;   // reads meta DIRECTLY -> bug
 
       // GOLDEN two-flop path (the 11.2 fix, modeled): clean value only.
       reg s1, s2; integer cnt_good; initial cnt_good = 0;
       always @(posedge clk_b) begin s1 <= async_in; s2 <= s1; end
       always @(posedge clk_b) if (s2) cnt_good <= cnt_good + 1;
 
       integer errors; integer i;
       initial begin
           errors = 0;
           for (i = 0; i < 2000; i = i + 1) @(posedge clk_b);
           if (cnt_bad !== cnt_good)
               $display("PASS: single-flop capture corrupted the count (bad=%0d good=%0d)", cnt_bad, cnt_good);
           else begin
               $display("FAIL: no divergence -- injection model did not fire"); errors = errors + 1;
           end
           $finish;
       end
   endmodule
meta_model_tb.vhd — SIMULATION MODEL (not synthesizable): VHDL metastability injection + assert self-check
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity meta_model_tb is
   end entity;
 
   architecture sim of meta_model_tb is
       signal clk_a, clk_b : std_logic := '0';
       signal async_in     : std_logic := '0';
       signal meta         : std_logic := '0';
       signal s1, s2       : std_logic := '0';
       signal cnt_bad      : integer := 0;
       signal cnt_good     : integer := 0;
       signal last_toggle  : time := 0 ns;
       signal seed1, seed2 : integer := 42;      -- for uniform() random resolve
   begin
       -- Two ASYNCHRONOUS clocks (asymmetric periods so phases drift).
       clk_a <= not clk_a after 5 ns;
       clk_b <= not clk_b after 7 ns;
 
       -- Source control bit toggles on clk_a; remember when it changed.
       src : process (clk_a)
       begin
           if rising_edge(clk_a) then
               async_in    <= not async_in;
               last_toggle <= now;
           end if;
       end process;
 
       -- UNSAFE capture with a metastability MODEL: if the async toggle landed near
       -- this edge, resolve to a random bit instead of the clean value (sim only).
       cap : process (clk_b)
           variable r : real;
           variable v1, v2 : integer;
       begin
           if rising_edge(clk_b) then
               if (now - last_toggle) < 2 ns then
                   v1 := seed1; v2 := seed2;
                   uniform(v1, v2, r);           -- MODEL: metastable -> random resolve
                   seed1 <= v1; seed2 <= v2;
                   if r > 0.5 then meta <= '1'; else meta <= '0'; end if;
               else
                   meta <= async_in;             -- clean sample otherwise
               end if;
           end if;
       end process;
 
       -- Downstream reads meta DIRECTLY (the bug).
       badc : process (clk_b)
       begin
           if rising_edge(clk_b) and meta = '1' then cnt_bad <= cnt_bad + 1; end if;
       end process;
 
       -- GOLDEN two-flop path (the 11.2 fix, modeled): clean value only.
       goodc : process (clk_b)
       begin
           if rising_edge(clk_b) then
               s1 <= async_in; s2 <= s1;
               if s2 = '1' then cnt_good <= cnt_good + 1; end if;
           end if;
       end process;
 
       chk : process
       begin
           for i in 0 to 1999 loop
               wait until rising_edge(clk_b);
           end loop;
           -- The single-flop path MUST diverge from the clean path -> corruption observed.
           assert cnt_bad /= cnt_good
               report "no divergence -- injection model did not fire" severity error;
           report "meta model self-check complete: single-flop capture corrupted the count" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the split is the same: the capture flop (4a) is real, synthesizable hardware that is unsafe because its input is asynchronous; the injection testbench (4b) is a simulation model only, a deliberate trick to make the silicon-level hazard visible so the downstream corruption — and therefore the need for the 11.2 synchronizer — is concrete and self-checkable rather than merely asserted.

5. Verification Strategy

Metastability is the pattern where "verification" means something unusual, because the failure is a timing/analog event that pure RTL simulation cannot faithfully produce. The correctness argument is therefore three-legged: you reason MTBF, you model the hazard to prove downstream robustness, and you use structural tools to find unsynchronized crossings — and the real "test" is a design rule.

You do not verify metastability away. You argue its probability from the synchronizer depth and the clock/data rates, you model its consequence to prove your logic survives, and you lint every crossing to prove none is left unprotected.

  • MTBF is an argument, not a simulation. The primary "verification" of a crossing is a calculation: given f_clk, f_data, the device tau and T0, and the settling time your synchronizer allows, compute MTBF ~ e^(t_settle/tau) / (T0 * f_clk * f_data) and confirm it exceeds the product's required lifetime by a wide margin. Adding a synchronizer stage multiplies the numerator's exponent; this is where you decide two flops vs three. No amount of RTL simulation substitutes for this number.
  • Model the hazard to prove downstream robustness (self-checking). Because a zero-delay sim will not go metastable, you inject it — exactly the §4b model: drive the captured bit to X or a $random value on near-edge source transitions, feed it into the downstream logic, and assert the logic never reaches an illegal state even so. The self-check is that the properly-synchronized path stays correct while the single-flop path diverges (SV if (cnt_bad !== cnt_good) $display("PASS…"); Verilog the same compare; VHDL assert cnt_bad /= cnt_good … severity error). X-propagation simulation is the standard form of this: let the metastable bit be X and confirm no X reaches a control point that matters.
  • Invariants, stated conceptually. Three properties must hold: (i) every signal crossing from an asynchronous domain passes through a synchronizer before any logic reads it; (ii) a first-stage capture flop's output never fans out to combinational logic before the synchronizer's final stage (no fan-out of an unresolved node); (iii) the computed MTBF for every crossing exceeds the reliability target. These are structural and numeric properties, not waveform checks.
  • CDC / lint tools find the crossings; that is the real gate. A CDC checker (a lint-class static tool) structurally identifies every signal that crosses clock domains and flags any that is captured by a single flop, fans out before resolving, or is a multi-bit bus crossing without proper handling. This is the tool that actually catches an unsynchronized crossing before tape-out — the equivalent of STA for reset recovery/removal. Formal CDC can prove the absence of unsynchronized paths. RTL functional simulation cannot do this; it must be a structural check.
  • Corner cases to exercise (in the model). The source toggling exactly at the destination edge (the violating sample the model exists to create); the source toggling faster than the destination clock (higher f_data, worse MTBF); an async pulse narrower than a destination period (which a plain synchronizer can miss entirely — the CDC-pulse-loss failure, handled by pulse synchronizers later in the chapter); and a metastable bit that fans out to two loads (the fan-out corruption of §7).
  • Expected waveform behaviour. In a correct (synchronized) design, the async bit appears at the synchronizer's output one or two destination cycles late, always as a clean 0 or 1, and downstream logic only ever sees resolved values. The signature of the bug in the injection model is the single-flop path's X/random capture propagating into the downstream counter or FSM and diverging from the clean path — a divergence that appears only occasionally and at a rate that rises with f_data, mirroring the intermittent, rate-dependent silicon failure.

6. Common Mistakes

Capturing an async signal in one flop and using it directly. The signature mistake, and the reason for this whole chapter. A single destination-domain flop sampling an asynchronous input can go metastable, and if its output feeds logic immediately, that metastable or late-resolved value enters the design and corrupts it. One flop is a capture, not a synchronizer: it gives the metastable event no time to resolve before use. Every async single-bit crossing needs at least a two-flop synchronizer, so the first flop's rare metastability has a full period to die out before the second flop — and only the second flop's output — is read.

Fanning out a metastable node to multiple gates. Even with a capture flop, if that flop's still-unresolved output drives two or more loads, the two loads can read the intermediate voltage as opposite logic levels — the same node seen as 1 by one gate and 0 by another. The design then holds contradictory beliefs about one bit and lands in an incoherent state (the §7 fan-out failure). The rule: nothing may fan out from a first-stage capture flop; resolve the value fully through the synchronizer, then fan out the clean output.

Assuming metastability "can't happen because it's rare." It is not rare in the sense of "won't happen" — across an asynchronous crossing it is inevitable given enough edges; only the rate is small, and the rate rises with clock frequency, data toggle rate, and temperature. Treating it as negligible is how the one-in-a-hundred-hours field failure ships. The correct posture is to budget it: compute MTBF and make it astronomically larger than the product lifetime, never assume it away.

Putting combinational logic right after the first capture flop. A synchronizer's whole job is to give the first flop a full clock period of quiet to resolve. Insert combinational logic (a gate, a mux, an adder) between the first and second synchronizer flops and you steal part of that settling period, shortening the resolution time and collapsing the MTBF — you have paid for two flops and gotten the reliability of one. The synchronizer flops must be back-to-back, with no logic between them, and (in the tool flow) marked with ASYNC_REG-style intent so the tool keeps them adjacent and does not optimize the chain apart.

Synchronizing each bit of a multi-bit bus independently. A two-flop synchronizer is safe for one bit. Run several related bits through parallel synchronizers and each resolves on its own coin-flip, so on a transition where two bits change together, one can update a cycle before the other — the receiver sees a value that was never sent. Multi-bit CDC needs a different mechanism (gray coding for pointers, or a handshake / FIFO for data); a bank of independent two-flop synchronizers is not it.

Treating an async reset release as immune. A reset is just another asynchronous signal, and its release is a setup/hold-analogue (recovery/removal) violation that drives flops metastable exactly like a data crossing — the reset-strategy page's whole subject. Metastability is not only a data-CDC concern; any async edge into a clocked flop, including a reset deassert, is subject to it, and the same "give it time to resolve" fix (the reset synchronizer) applies.

7. DebugLab

The peripheral that fails once an hour — an unprotected async capture and a metastable fan-out

The engineering lesson: a flip-flop is only a reliable 0/1 device when its data is stable across the setup/hold window; a signal from an asynchronous domain violates that window sooner or later with certainty, and the flop that samples it goes metastable — hovering at an intermediate voltage and resolving late and randomly. Inside one clock domain this never happens, because STA proves the window is honoured on every path; across domains it is inevitable, and the only defence is time — give the first flop a full clock period to resolve (a synchronizer) before any logic reads it, and never let an unresolved node fan out, or two loads will read it as opposite values and the state goes incoherent. You cannot eliminate metastability; you make its rate astronomically small (a large MTBF) and prove it with a CDC lint plus an MTBF calculation, never with RTL simulation alone. The tell in silicon is always the same: an intermittent, un-reproducible failure whose rate tracks temperature, voltage, and clock/data frequency — the fingerprint of a timing-window violation, not a logic bug.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the setup/hold-window-across-domains reasoning and the "give it time to resolve" habit.

Exercise 1 — Explain the inevitability

A colleague argues that sampling an asynchronous bit with a single flop is fine because 'the odds of hitting the setup/hold window are tiny.' In one paragraph, explain precisely why the crossing is nonetheless guaranteed to fail eventually (not merely unlikely), what the receiving flop physically does on a violating edge, and why the same single flop inside a synchronous domain is perfectly safe. Name the analysis that guarantees the single-domain case.

Exercise 2 — Reason the MTBF

Write the MTBF expression for a metastable crossing and label every term (t_settle, tau, T0, f_clk, f_data). Then answer in one line each: (a) which term you control by adding synchronizer flops, and how it changes; (b) why adding one flop can multiply MTBF by many orders of magnitude; (c) two operating conditions that make MTBF worse and why (hint: what happens to tau when it is hot); (d) why the MTBF can never be infinite.

Exercise 3 — Diagnose the incoherent state

A block occasionally comes up in a state where its counter has advanced but its datapath mux routed as though the counter had not — the two halves disagree about a single control bit. The bit comes from another clock domain. State the single most likely root cause, the precise mechanism (why one node is read as two different values), why it is rarer and stranger than a plain wrong-value bug, the exact structural rule that was violated, and the RTL change that fixes it. Name the tool that would have flagged it.

Exercise 4 — Place the synchronizer, then stress it

You must bring four signals from a 32 kHz always-on domain into a 600 MHz core domain: (a) a single-bit 'sleep request' level, (b) a single-cycle 'wake pulse', (c) a 3-bit mode field that changes as a group, and (d) an async reset release. For each, state what structure you place (and why a plain two-flop synchronizer is or is not sufficient), and name the failure that occurs if you use a bank of independent two-flop synchronizers for the 3-bit field. Then say where combinational logic must not go, and what ASYNC_REG-style intent tells the tool.

Continue in Clock Domain Crossing (sibling and forward links unlock as they ship):

  • Two-Flop Synchronizer — Chapter 11.2; the direct fix this page motivates — giving the first flop a full clock period to resolve before any logic reads its output, and the MTBF math that sets the stage count.
  • The Multi-Bit CDC Problem — Chapter 11.3; why a bank of independent two-flop synchronizers is not safe for a bus — each bit resolves on its own coin-flip and the group skews to a value never sent.
  • Pulse Handshake Synchronizer — Chapter 11.4; the CDC-pulse-loss failure previewed in §5 — an async pulse narrower than a destination period that a plain synchronizer can miss entirely.
  • Gray-Code Pointer Synchronizer — Chapter 11.5; the multi-bit-safe crossing for FIFO pointers, where only one bit ever changes per step so a synchronizer stays coherent.
  • Asynchronous FIFO — Chapter 11.6; where all of this composes — gray pointers synchronized across domains to move data safely between two clocks.
  • CDC Design Rules — Chapter 11.7; the discipline that generalizes this page — every async crossing goes through a synchronizer, nothing fans out unresolved, and CDC lint proves it.

Backward / in-track dependencies:

  • The Register Pattern (D-FF) — Chapter 2.1; the flop whose setup/hold window is violated at a domain crossing — the device that goes metastable.
  • Reset Strategy in Practice — Chapter 2.5; the recovery/removal analogue — a reset release is an async edge that drives flops metastable exactly like a data crossing, fixed by the same 'give it time' synchronizer.
  • Asynchronous Reset — Chapter 2.4; where the async-release hazard is introduced — the reset-domain instance of this page's physics.
  • valid/ready Handshake — Chapter 9.1; the handshake that safely moves multi-bit data across domains when a plain synchronizer cannot — the alternative to gray coding for buses.
  • Synchronous FIFO Architecture — Chapter 7.1; the single-clock FIFO that becomes the asynchronous FIFO once its pointers must cross domains through synchronizers.
  • The RTL Design Mindset — Chapter 0.2; the State and Verification lenses this page applies — metastability is how a design fails to acquire a clean state at a crossing.
  • What Is an RTL Design Pattern? — Chapter 0.1; why metastability handling earns the name 'pattern' — a recurring problem, a reusable form (the synchronizer), a synthesis reality (ASYNC_REG), and a signature failure.

Verilog v1 prerequisites (the grammar and timing this pattern builds on):

  • Timing Checks — the setup/hold specification and check this page violates across domains.
  • Timing-Violation Behaviour — what a flop does when setup/hold is violated — the RTL-level view of the metastable event.
  • Blocking and Non-Blocking Assignments — why the capture flop and the synchronizer chain use non-blocking (<=) in their clocked processes.
  • Initial and Always Blocks — the always @(posedge clk) block template the capture flop is built on, and the two-clock testbench structure the metastability model uses.

11. Summary

  • A flop is only a clean 0/1 device when its data is stable across the setup/hold window. If the data transitions inside that window — a setup/hold violation — the output can go metastable: hover at an intermediate voltage for an unbounded time, then resolve randomly to 0 or 1. Metastability is a physical, analog, timing-level event, not an RTL construct.
  • Single domain: a non-event. Async crossing: inevitable. Inside one clock domain, STA proves setup/hold are met on every path, so the window is never violated and metastability never happens. A signal from an asynchronous domain has no timing relationship to the receiving clock, so its transitions land at a random phase and will, given enough edges, fall inside the window with certainty — the receiving flop will go metastable. Not rare luck; guaranteed eventually.
  • You buy reliability with settling time, and you can never reach zero. A metastable node resolves exponentially with a time constant tau, so the residual probability falls like e^(-t_settle/tau) and the mean time between failures grows like e^(t_settle/tau). One flop with a full period already resolves most events; a second flop (the synchronizer of 11.2) gives the first's residual metastability another whole period, pushing MTBF from seconds to millions of years. You make it improbable; you never eliminate it — and you prove the margin with an MTBF calculation and a CDC lint, never with RTL simulation.
  • A metastable value is dangerous when it fans out. An unresolved node at the intermediate voltage can be read as opposite levels by two different gates, so one bit becomes two contradictory beliefs and the state goes incoherent. The rule is not just 'synchronize the crossing' but 'resolve the value fully before it fans out to anything' — nothing may read a first-stage capture flop except the next synchronizer stage, with no combinational logic between the stages (mark them ASYNC_REG).
  • Metastability is not synthesizable — but its consequence is demonstrable. No RTL makes a flop hover at Vmid; that lives in silicon and gate-level timing. The synthesizable capture flop here is real, unsafe hardware; the behavioral model injects X/$random on near-edge transitions to make the downstream corruption visible and self-checkable — clearly labelled as a simulation trick, not hardware. Built and self-check-verified in SystemVerilog, Verilog, and VHDL, this is the foundation the entire CDC chapter (synchronizers, gray pointers, async FIFOs, CDC rules) is built on.