Back to ChipVerify AI

Learn / Concept

Clock Domain Crossing (CDC): Synchronizers, Metastability, and Common Pitfalls

Clock domain crossing is the set of techniques used when a signal generated by one clock has to be sampled by a flop running on a different, asynchronous clock. CDC matters because the standard digital abstraction of a flop, deterministic input-to-output on a clock edge, breaks at the boundary between asynchronous domains. The result is metastability, which left untreated produces silicon that simulates correctly, passes most tests, and then fails intermittently on a customer's bench. This guide is for designers and verification engineers who need a working understanding of asynchronous clocks, synchronizer cells, single-bit and multi-bit crossings, reset and pulse crossings, the bugs that survive simulation, and how CDC is actually verified. For the device-level physics of why a flop can hang at an invalid voltage, see the deep dive on metastability and MTBF.

Clocks, asynchronous domains, and why CDC exists

A clock domain is the set of flops driven by the same clock signal, or by a derived clock with a known fixed phase relationship to it (a divided clock from the same root, for example). Two clocks are asynchronous when their edges have no fixed phase relationship: separate PLLs, separate oscillators, a system clock and a USB clock, a core clock and an SRAM clock from a different IP. Even two clocks at the nominally same frequency from different sources are asynchronous, because they drift. The number of CDC paths in a modern SoC routinely runs into the thousands. Each one is an opportunity for metastability if left unprotected.

Metastability and MTBF

Every flop has a setup and hold window around its capture edge during which the input must be stable. If a signal changes inside that window, the flop's output can hover at an invalid voltage instead of resolving to a clean 0 or 1. This is metastability. The hovering state is not a third logic value; it is a real analog condition that resolves to 0 or 1 eventually, with an exponentially decaying probability of still being unresolved t seconds later. The standard metric is mean time between failures (MTBF):

MTBF = exp(t_res / tau) / (T0 * f_clk * f_data)

  t_res  = resolution time available before the next flop samples
  tau    = process-dependent settling time constant (ps-scale)
  T0     = process-dependent metastability window (ps-scale)
  f_clk  = destination (sampling) clock frequency
  f_data = effective rate of asynchronous transitions on the input

The dependence on t_res is exponential, which is the whole game. Adding one synchronizer flop gives the metastable node roughly one extra destination clock period to settle, and because t_res sits inside an exponential, that single extra period can move MTBF from seconds to centuries. You do not engineer metastability away; you engineer the probability of it propagating downstream low enough that it will not happen in the lifetime of every chip you ship. The full derivation of each term, how tau and T0 are characterized, and worked MTBF numbers live in the dedicated metastability guide.

The two-flop synchronizer (single-bit)

The default treatment for a single-bit asynchronous signal is a two-flop synchronizer in the destination clock domain. The first flop absorbs the metastability event; the second flop sees a stable input on the next edge with overwhelming probability. Most libraries provide a hardened synchronizer cell with a tighter flop pair, no scan path between them, and a synthesis attribute that prevents tools from optimizing or rebalancing the chain.

// Two-flop synchronizer for a single-bit async signal.
// async_in is launched in some other clock domain.
// dst_clk is the destination domain. ASYNC_REG keeps the pair
// placed close together and stops synthesis from rebalancing them.
module sync_2ff #(parameter logic RESET_VAL = 1'b0) (
  input  logic dst_clk,
  input  logic dst_rst_n,
  input  logic async_in,    // already flopped in its own domain
  output logic sync_out
);
  (* ASYNC_REG = "TRUE" *) logic meta_q;
  (* ASYNC_REG = "TRUE" *) logic sync_q;

  always_ff @(posedge dst_clk or negedge dst_rst_n) begin
    if (!dst_rst_n) begin
      meta_q <= RESET_VAL;
      sync_q <= RESET_VAL;
    end else begin
      meta_q <= async_in;   // may go metastable; given a full cycle to settle
      sync_q <= meta_q;     // samples a (very probably) settled value
    end
  end

  assign sync_out = sync_q;
endmodule

Three rules: the source must be flopped in its own domain (never synchronize a combinational expression, which can glitch), there must be no combinational logic between meta_q and sync_q (it eats into resolution time), and the synchronizer must carry an ASYNC_REG / dont_touch attribute so synthesis cannot move logic between the flops.

Why two flops, and when three?

Two flops are enough for the large majority of crossings: the second flop's full clock period of resolution time pushes MTBF well past the service life of the product. You move to a three-flop synchronizer when the destination clock is very fast (so each period of resolution time is short), when the process has a large tau, or when the safety budget has to cover a huge fleet over many years. Each additional flop adds one more clock period to t_res and multiplies MTBF by the same exponential factor again. The cost is one extra cycle of latency, which is usually free on a control signal.

The four CDC styles

Almost every CDC path on a real chip falls into one of four patterns. Pick the right pattern by what is crossing the boundary, not by what is convenient.

StyleUse whenMechanism
Single-bitA 1-bit control or status signal crosses2-flop synchronizer
Pulse / handshakeAn event must be transferred reliably exactly onceToggle in source, sync + edge-detect in destination, ack back
Multi-bit / FIFOA data word must cross every cycle at high rateAsync FIFO with gray-coded read and write pointers
MUX-recircA multi-bit value is updated rarely and the receiver does not need every updateSync a 1-bit valid; gate a held bus into the destination flop with that synced valid

Why you cannot just two-flop a multi-bit bus

If you put a 2-flop synchronizer on each bit of a 4-bit bus, the four bits resolve metastability independently. On a source transition from 0111 to 1000, some bits can be captured on one edge and the rest on the next, so the destination can briefly observe 0000, 1111, or any other mix of old and new bits, none of which ever existed at the source. The fixes are: synchronize a single-bit valid and gate the held bus into the destination with it (MUX-recirculation), gray-code the value so only one bit changes per legal transition (works for monotonic counters), use a request/acknowledge handshake, or push the data through an async FIFO. A bare per-bit 2-FF is never correct for multi-bit data.

Pulse / event crossing

A common bug is sending a one-cycle pulse from a fast domain to a slow domain and expecting the slow domain to see it. If the pulse is narrower than a destination clock period, the synchronizer can miss it entirely. The standard fix is a toggle: the source flips a level on each event, the destination synchronizes the level with a normal 2-flop synchronizer and detects the edge to recover one clean pulse. For guaranteed exactly-once delivery (no events lost when they arrive faster than the round trip), add an acknowledge toggle back to the source and gate the next event on it, i.e. a full handshake. This toggle pattern is what sits inside nearly every interrupt-cross and AXI domain bridge.

// Pulse crossing via a toggle. The source flips a level on each event;
// the destination synchronizes the level and edge-detects it. This survives
// a slow destination clock that a bare one-cycle pulse would miss.
module pulse_sync (
  input  logic src_clk,
  input  logic src_rst_n,
  input  logic src_pulse,     // 1-cycle pulse in the source domain
  input  logic dst_clk,
  input  logic dst_rst_n,
  output logic dst_pulse      // 1-cycle pulse in the destination domain
);
  logic toggle_q;
  always_ff @(posedge src_clk or negedge src_rst_n)
    if (!src_rst_n) toggle_q <= 1'b0;
    else if (src_pulse) toggle_q <= ~toggle_q;   // one toggle per event

  // Synchronize the level (single bit) into the destination domain.
  logic [2:0] sync_q;
  always_ff @(posedge dst_clk or negedge dst_rst_n)
    if (!dst_rst_n) sync_q <= 3'b0;
    else sync_q <= {sync_q[1:0], toggle_q};

  // Edge-detect the synchronized toggle to recover one clean pulse.
  assign dst_pulse = sync_q[2] ^ sync_q[1];
endmodule

Async FIFO: the workhorse for streaming data

For continuous high-rate data, an async FIFO is the canonical structure. The write and read pointers live in different domains. Each pointer is kept in binary locally (for addressing and the full/empty comparison) and converted to gray code only to cross the boundary, where a 2-flop synchronizer samples it. Gray code is the key trick: consecutive values differ in exactly one bit, so the synchronizer never has to resolve more than one changing bit, and a partially-resolved pointer is always either the old or the new value, never a corrupt one. That single-bit-change guarantee holds only when the pointer is source-registered and increments by exactly one step per source clock, and when bus-skew / max-delay constraints are applied at the crossing; a pointer that jumps multiple steps or an unconstrained path can still present several changing bits, defeating the trick. The dual-port memory in the middle is written and read from different clocks but needs no synchronization itself, because access is gated by the synchronized pointers.

// Gray-code helpers for asynchronous-FIFO pointers. Consecutive binary
// values differ in exactly one gray bit, so PROVIDED the pointer is
// source-registered and advances by exactly one step per source clock (a
// single-bit change), and bus-skew / max-delay constraints are applied, the
// cross-domain synchronizer sees at most one bit changing per cycle and a
// partially-resolved pointer is always the old or the new value, never a
// corrupt one. Skip those conditions (multi-step jumps, no skew constraint)
// and the destination can still observe several changing bits.
// Only the gray pointer is synchronized across the boundary.
function automatic logic [PTR_W-1:0] bin2gray (input logic [PTR_W-1:0] b);
  bin2gray = b ^ (b >> 1);
endfunction

function automatic logic [PTR_W-1:0] gray2bin (input logic [PTR_W-1:0] g);
  for (int i = PTR_W - 1; i >= 0; i--)
    gray2bin[i] = ^(g >> i);
endfunction

The pointer itself must be produced by a flop, never by a combinational increment of the synchronized value, or the destination would see intermediate combinational states. The write side keeps a binary counter and registers its gray encoding:

// Async-FIFO write side (read side is the mirror). The write pointer is
// kept in binary for addressing/full-comparison and converted to gray only
// to cross into the read clock domain, where a 2-flop synchronizer samples it.
module wptr_cross #(parameter int PTR_W = 4) (
  input  logic              wclk,
  input  logic              wrst_n,
  input  logic              w_en,        // enqueue when not full
  output logic [PTR_W-1:0]  waddr,       // RAM write address (binary, no MSB)
  output logic [PTR_W:0]    wptr_gray    // gray pointer to synchronize -> rclk
);
  logic [PTR_W:0] wbin_q, wbin_n;
  logic [PTR_W:0] wgray_q;

  assign wbin_n = wbin_q + {{PTR_W{1'b0}}, w_en};
  assign waddr  = wbin_q[PTR_W-1:0];

  always_ff @(posedge wclk or negedge wrst_n)
    if (!wrst_n) begin
      wbin_q  <= '0;
      wgray_q <= '0;                       // pointer updated by a FLOP, never
    end else begin                         // a combinational increment of the
      wbin_q  <= wbin_n;                   // synchronized value
      wgray_q <= wbin_n ^ (wbin_n >> 1);   // bin2gray of the next binary value
    end

  assign wptr_gray = wgray_q;
endmodule

The pointer is one bit wider than the address (an extra MSB) so the full and empty conditions can be distinguished even when the read and write pointers point at the same memory location.

Reset domain crossing

Resets are a CDC problem in their own right. An asynchronous reset can assert at any time, but if it deasserts close to a clock edge it violates the flop's recovery/removal timing and can drive it metastable, exactly like a data crossing. And if one reset releases at slightly different times across two clock domains, the reset deassertion is itself a crossing. The standard cell is the async-assert / sync-deassert reset synchronizer: it asserts the moment the raw reset drops (no clock needed) and deasserts only after two clean destination-clock edges. It does not filter glitches on the incoming async reset — it assumes a clean reset source, so clean or debounce that source upstream if it can glitch. Instance one per destination clock domain; never share an instance across domains. Reset crossings have enough of their own failure modes (one raw reset fanned out to many domains, a flop in one reset domain feeding a flop in another) that mature flows give them a dedicated analysis pass — see the full treatment in reset domain crossing (RDC).

// Async assert, sync deassert reset synchronizer.
// Asserts the moment async_rst_n drops; deasserts on two clean dst_clk edges.
// It does NOT filter glitches on async_rst_n -- it assumes a clean reset
// source; clean/debounce that source upstream if it can glitch.
// One instance per destination clock domain; never share across domains.
module rst_sync (
  input  logic dst_clk,
  input  logic async_rst_n,
  output logic dst_rst_n
);
  (* ASYNC_REG = "TRUE" *) logic meta_q;
  (* ASYNC_REG = "TRUE" *) logic sync_q;

  always_ff @(posedge dst_clk or negedge async_rst_n) begin
    if (!async_rst_n) begin
      meta_q <= 1'b0;     // async assert
      sync_q <= 1'b0;
    end else begin
      meta_q <= 1'b1;     // sync deassert, two edges later
      sync_q <= meta_q;
    end
  end

  assign dst_rst_n = sync_q;
endmodule

Common CDC bugs that survive simulation

  • No synchronizer at all. The signal is treated as if its source and destination clocks were the same. RTL simulation, with zero-delay logic and an idealized clock generator, hides this completely. The tapeout fails on bench.
  • Multi-bit bus through a 2-flop sync. Each bit synchronizes independently and the receiver sees transient bus values that never existed at the source.
  • Combinational logic before or between sync flops. A combinational source can glitch, and any gate between meta_q and sync_q eats resolution time. Both invalidate the MTBF the synchronizer was sized for. Flop in the source domain, keep the chain pure.
  • Reconvergence / fan-out from one synchronizer. Two consumers tap sync_q, or two separately-synchronized signals are later recombined. On a metastability event the downstream flops can resolve to different opinions of the same crossing, so the design sees an impossible combination. Either replicate the synchronizer per consumer where they must agree, or guarantee the recombined signals can never change in the same window.
  • Pulse lost across a slow domain. A one-cycle pulse handed to a slower destination is missed outright. Use a toggle/handshake, not a bare synchronizer.
  • Reset crossing without a synchronizer. Asynchronous resets need synchronous deassertion in each destination domain. A reset that releases asynchronously across two domains is itself a CDC violation, and most simulations never expose it.
  • Gray pointer built combinationally. The gray-coded FIFO pointer must be updated by a flop, not by a combinational increment of the synchronized value, or the other domain sees intermediate states.
  • Synchronizer optimized away by synthesis. Without an ASYNC_REG, dont_touch, or equivalent attribute, retiming or fan-out balancing can insert logic between the two flops or split them across the die. Use the foundry-provided synchronizer cell or attribute the flops explicitly.

What CDC verification looks like

Functional simulation does not catch CDC bugs, because the model of a flop in simulation is deterministic: it resolves cleanly on every edge and never goes metastable. A missing or malformed synchronizer therefore sails through a green regression. CDC verification is its own discipline, layered:

  • Structural CDC analysis. A static analyzer walks the design, identifies every signal that crosses between flops in different clock domains, and reports whether each crossing has a recognizable synchronizer structure (2-FF, gray-coded, handshake) and whether any combinational logic or fan-out violates the rules. This is the first and highest-value line of defense.
  • Metastability injection. Simulation models randomly delay the resolution of synchronizer outputs by one cycle, exposing downstream consumers that wrongly assume a synchronized signal becomes valid on a fixed edge.
  • Formal CDC properties. Coherency and glitch-free properties on the crossing (no combinational fan-in to the second flop, the data bus is stable while its synchronized valid is asserted, gray pointers change one bit at a time) can be proved exhaustively, for example with SymbiYosys on open flows.
  • Reset domain crossing (RDC) checks. Resets that span domains have the same hazards as data that spans domains and get their own analysis pass — covered in detail in reset domain crossing.

How a structural CDC checker reasons about a crossing

It helps to know what a structural CDC analyzer can and cannot see, because it sets honest expectations for the evidence it produces. A structural checker works on the elaborated netlist of flops and combinational logic, not on simulated waveforms. Its reasoning runs roughly like this:

  • Assign each flop a clock domain by tracing every register back to the clock net that drives it, and group nets that share a root or a fixed phase relationship.
  • Enumerate the crossings — every path where a flop in domain A drives, through any combinational logic, a flop in domain B with no fixed phase relationship to A. Each such fan-in is a candidate crossing.
  • Pattern-match a synchronizer at the destination: a chain of two (or more) same-domain flops with no combinational logic between them for a single-bit crossing, a gray-coded pointer for a FIFO, a synchronized valid gating a held bus for a handshake. A crossing with no recognizable structure is surfaced as unsynchronized.
  • Flag the rule violations it can decide structurally: combinational logic feeding the first synchronizer flop, a multi-bit bus through per-bit 2-FFs, reconvergent fan-out from one synchronizer, a gray pointer built combinationally.

What it does not do is just as important. A structural checker reports structural evidence: it surfaces every crossing that lacks a recognizable synchronizer and the rule violations above, with file and line. It does not compute MTBF, it does not know your process tau or clock frequencies, it cannot prove a flagged crossing is truly safe at the device level, and it is not a CDC signoff tool. It is the highest-value first pass — it catches the missing and malformed synchronizers that simulation hides — but constraint setup, formal coherency proofs, and timing/skew constraints at the crossing remain the engineer’s job. The same structural reasoning powers this free CDC checker for Verilog.

How ChipVerify AI helps

CDC bugs are exactly the bugs that survive a clean lint pass and a green simulation regression. ChipVerify AI (a pre-signoff RTL evidence tool, currently in closed beta, not a foundry signoff tool) runs structural CDC analysis on uploaded RTL and names suspicious crossings, and it can generate correct-by-construction CDC primitives (two-flop and reset synchronizers, gray-coded async FIFOs) so you start from a known-good cell rather than hand-rolling one. It also ties findings back to bug-level concept pages like inferred latches when those are the deeper cause.

FAQ

Why is a two-flop synchronizer enough, and when do you need three?

MTBF grows exponentially with the resolution time between synchronizer flops, so the second flop, by giving the first a full destination clock period to settle, typically moves MTBF from seconds to many years. Two flops suffice for most designs. Move to three when the destination clock is very fast, the process is leaky (large tau), or the safety margin must cover a large fleet over a long lifetime; each extra flop adds another clock period of resolution time and multiplies MTBF again.

Why can't I just put a two-flop synchronizer on each bit of a bus?

Because each bit resolves metastability independently. When several bits change in the same source cycle, the destination can latch them on different edges and observe a transient value that never existed at the source. Multi-bit data must cross with a coherent technique: gray code (so only one bit changes per step), a handshake, MUX-recirculation gated by a single synchronized valid, or an asynchronous FIFO.

Why does CDC verification need formal and structural analysis instead of just simulation?

RTL simulation models a flop as deterministic: it ignores setup/hold violations and resolves cleanly on every edge, so it never produces metastability. A missing or malformed synchronizer therefore passes simulation. Structural CDC analysis statically finds every crossing between clock domains and checks for a recognizable synchronizer; formal proves coherency and glitch-free properties; metastability injection in simulation models the one-cycle resolution uncertainty that downstream consumers must tolerate.

How do you safely cross a single-cycle pulse between clock domains?

A bare pulse can be missed if the destination clock is slower than the pulse width. Convert the event to a level toggle in the source domain, synchronize the toggle into the destination with a two-flop synchronizer, and recover the event by edge-detecting the synchronized toggle. For guaranteed exactly-once delivery, add an acknowledge toggle back to the source and gate the next event on it (a full handshake).

Why do asynchronous resets need a reset synchronizer?

An asynchronous reset can assert at any time (which is fine), but if it deasserts close to a clock edge it can violate recovery/removal timing and drive flops metastable, and if a single reset releases at slightly different times across two domains it is itself a clock domain crossing. A reset synchronizer provides async assert (the reset takes effect immediately, no clock needed) and synchronous deassert through two flops in the destination domain, one instance per clock domain. It does not filter glitches on the async reset source — it assumes a clean reset — so debounce or clean that source separately.

Related reading

  • RTL verification guide — where CDC analysis sits in the wider pre-tapeout flow.
  • Metastability and MTBF — the device-level failure mechanism every synchronizer exists to tame, with the full MTBF derivation.
  • Reset domain crossing (RDC) — the reset-side counterpart, where independent resets make their own crossings even in a single-clock design.
  • Free CDC checker for Verilog — run the structural pass described above on your own RTL.
  • Functional coverage closure — how to confirm metastability-injection and handshake corner cases were actually exercised, not just compiled.
  • DFT scan readiness — synchronizer flops must stay off the scan chain, so CDC and scan insertion interact directly.
  • Blocking vs non-blocking — the rule that decides whether your synchronizer flops actually pipeline the way you drew them.
  • Inferred latches — an unintended latch in a synchronizer path destroys its metastability guarantee.
  • Width mismatches — a mis-sized gray pointer or bus is a classic multi-bit-crossing bug.
  • Verible lint — lint catches the easy structural smells; CDC analysis catches what lint cannot.
  • Tapeout readiness — CDC closure is a non-negotiable line item before a cut.

Try the public scanner at /tinytapeout or request access.

Run structural CDC analysis on your RTL

Sign in and point ChipVerify AI at a multi-clock design. It runs structural CDC analysis to name every crossing between clock domains and flags the ones without a recognizable synchronizer — pre-signoff evidence and bounded structural checks, not a foundry CDC signoff.