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.
// 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 flowThe 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 flowMTBF — 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$random0/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.
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;
endmoduleA 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).
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 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 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.
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.
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 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 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, computeMTBF ~ 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
Xor a$randomvalue 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 (SVif (cnt_bad !== cnt_good) $display("PASS…"); Verilog the same compare; VHDLassert cnt_bad /= cnt_good … severity error). X-propagation simulation is the standard form of this: let the metastable bit beXand confirm noXreaches 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 withf_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.
10. Related Tutorials
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 likee^(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/$randomon 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.