Clock Domain Crossing & Reset Synchronizers
Everything so far assumed one clock. Real chips have dozens. The moment a signal crosses between two clocks with no fixed phase relationship, setup and hold cannot be guaranteed - not because your design is slow, but because no design can guarantee them. CDC is the discipline of making that unavoidable failure so rare it never happens, and it is the single most common source of bugs that pass simulation and fail in the lab.
5.1 The physics of metastability & MTBF
Volume 3.1 established that a flip-flop has a setup/hold window in which data must not change. When the source is in a different clock domain, its transitions land wherever they like relative to your clock edge - so sooner or later, one lands inside that window. What happens then is not "the flop reads the wrong value". It is stranger than that.
Inside every flop is a pair of cross-coupled inverters - a bistable element with two stable points and, between them, an unstable equilibrium. Sample right at the window and the loop can be left balanced near that point, with its output sitting at neither a valid 0 nor a valid 1.
The recovery is exponential: the imbalance grows like e^(t/τ), where
τ is the regeneration time constant of the latch. So the probability of
still being unresolved after time tr decays exponentially -
which gives the central equation of the whole field.
| Term | Meaning | Who controls it |
|---|---|---|
| tr | Resolution time available before anything reads the output | You - add synchronizer stages |
| τ | Regeneration time constant of the flop | The silicon process and cell library |
| T0 | Metastability aperture - effective width of the bad window | The silicon process |
| fclk | Destination sampling frequency | System architecture |
| fdata | How often the asynchronous signal actually toggles | The traffic pattern |
Worked numbers - why the second flop matters so much
Take a plausible set of values: τ = 0.2 ns, T₀ = 1 ns,
f_clk = 100 MHz (10 ns period), f_data = 10 MHz. The denominator
is then 1e-9 × 1e8 × 1e7 = 10⁶.
| Design | tr available | e(tr/τ) | MTBF |
|---|---|---|---|
| No synchronizer async straight into logic |
≈ 2 ns | e¹⁰ ≈ 2.2 × 10⁴ | 22 ms |
| 2-flop synchronizer | ≈ 9.9 ns | e⁴⁹·⁵ ≈ 3.1 × 10²¹ | ≈ 98 million years |
| 3-flop synchronizer | ≈ 19.9 ns | e⁹⁹·⁵ ≈ 10⁴³ | ≈ 3 × 10²⁹ years |
5.2 Single-bit control signal synchronization
module sync_2ff #(
parameter STAGES = 2, // 3 for very high frequency domains
parameter INIT = 1'b0 // value held during reset
) (
input wire clk_dst,
input wire rst_n,
input wire din_async, // from another clock domain
output wire dout
);
// ASYNC_REG tells the place-and-route tool to keep these flops adjacent,
// which maximises the settling time actually available to FF1.
(* ASYNC_REG = "TRUE" *) reg [STAGES-1:0] sync;
always @(posedge clk_dst or negedge rst_n)
if (!rst_n) sync <= {STAGES{INIT}};
else sync <= {sync[STAGES-2:0], din_async};
assign dout = sync[STAGES-1];
endmodule
The direction problem: fast to slow
A two-flop synchronizer only works if the signal is stable long enough to be sampled. Going slow → fast, that is automatic. Going fast → slow, it is not: a one-cycle pulse in a 200 MHz domain is 5 ns wide, and a 25 MHz destination samples every 40 ns. The pulse can vanish entirely between two edges.
// Crossing a ONE-CYCLE PULSE from a fast domain into a slow one.
// Trick: a pulse cannot survive the crossing, but a LEVEL CHANGE can.
// So toggle a flop on each pulse, synchronize the level, and rebuild the
// pulse on the far side with an edge detector.
module pulse_sync (
input wire clk_src,
input wire rst_n_src,
input wire pulse_in, // 1 cycle wide in clk_src
input wire clk_dst,
input wire rst_n_dst,
output wire pulse_out // 1 cycle wide in clk_dst
);
// ---- source: pulse -> level change -----------------------------------
reg toggle;
always @(posedge clk_src or negedge rst_n_src)
if (!rst_n_src) toggle <= 1'b0;
else if (pulse_in) toggle <= ~toggle;
// ---- destination: 3 flops. Two to synchronize, one to remember the
// previous value so we can detect the edge. ------------------------
(* ASYNC_REG = "TRUE" *) reg [2:0] sync;
always @(posedge clk_dst or negedge rst_n_dst)
if (!rst_n_dst) sync <= 3'b000;
else sync <= {sync[1:0], toggle};
// Either edge of the toggle means "one pulse happened".
assign pulse_out = sync[2] ^ sync[1];
endmodule
5.3 Why 2-FF synchronizers fail on multi-bit buses
The obvious next step is wrong. Instantiate a synchronizer per bit of an 8-bit bus and you have not built a safe crossing - you have built eight independent random events. Each bit's metastability resolves on its own schedule, so bits that left the source together can arrive on different destination edges.
b3 resolved a cycle before the rest, so the
destination latches 1111 - a value the counter never produced. Every
individual synchronizer worked perfectly. The system is still broken.
start and mode are meant to
be interpreted together, synchronizing them through two independent 2-FF chains lets them
arrive on different cycles, and the destination briefly sees a combination that never
existed. Cross one signal, or cross them as a protocol - never as
parallel independent bits.
The four correct answers
| Technique | How it avoids the problem | Use when |
|---|---|---|
| Gray code | Only one bit changes per step, so a mistimed sample yields the old or the new value - never a third | Counters and FIFO pointers only |
| Data + synchronized flag | The bus is never synchronized at all - it is held stable while a single control bit crosses | Occasional transfers, low rate |
| Handshake | Explicit request/acknowledge means neither side moves until the other confirms | Slow control buses, config registers |
| Asynchronous FIFO | Gray-coded pointers plus dual-port memory | Sustained high-throughput data. Volume 06 |
5.4 Multi-bit handshake synchronizers
The handshake is the general-purpose answer for control and configuration buses. The data bus itself gets no synchronizer - instead the protocol guarantees the data is already stable and unchanging by the time the destination reads it.
req and ack is a single bit
crossing through its own 2-FF synchronizer. The wide data bus rides along beneath them,
unsynchronized and completely safe, because it provably does not move while
req is asserted.
// ---------- SOURCE SIDE ------------------------------------------------
module cdc_handshake_src #(
parameter W = 8
) (
input wire clk_src,
input wire rst_n,
input wire send, // request a transfer
input wire [W-1:0] din,
input wire ack_sync, // ack, already synced INTO clk_src
output reg busy,
output reg req,
output reg [W-1:0] data_held // crosses UNSYNCHRONIZED, on purpose
);
always @(posedge clk_src or negedge rst_n) begin
if (!rst_n) begin
req <= 1'b0; busy <= 1'b0; data_held <= {W{1'b0}};
end else if (!busy && send) begin
// Capture and FREEZE the data, then raise req. Because data_held
// cannot change again until the whole handshake completes, the
// destination can read it with no synchronizer at all.
data_held <= din;
req <= 1'b1; // phase 1
busy <= 1'b1;
end else if (busy && req && ack_sync) begin
req <= 1'b0; // phase 3: ack seen, drop req
end else if (busy && !req && !ack_sync) begin
busy <= 1'b0; // phase 4 complete, ready for the next
end
end
endmodule
// ---------- DESTINATION SIDE -------------------------------------------
module cdc_handshake_dst #(
parameter W = 8
) (
input wire clk_dst,
input wire rst_n,
input wire req_sync, // req, already synced INTO clk_dst
input wire [W-1:0] data_held, // stable by protocol, not by synchronizer
output reg [W-1:0] dout,
output reg valid,
output reg ack
);
always @(posedge clk_dst or negedge rst_n) begin
if (!rst_n) begin
ack <= 1'b0; valid <= 1'b0; dout <= {W{1'b0}};
end else begin
valid <= 1'b0; // single-cycle pulse
if (req_sync && !ack) begin
// req has come through 2 flops, so data_held has been stable for
// at least two destination clocks. Safe to sample directly.
dout <= data_held;
valid <= 1'b1;
ack <= 1'b1; // phase 2
end else if (!req_sync && ack) begin
ack <= 1'b0; // phase 4
end
end
end
endmodule
Interview grilling - "Why is the data bus safe without a synchronizer? Isn't that exactly what you just told me never to do?"
This is the question that separates people who memorised the circuit from people who understand it. The answer is about timing, not about synchronizers.
"The rule is not 'never cross a bus unsynchronized' - it is 'never sample a signal that
might be changing near the clock edge'. Here the data is written once, then frozen. By
the time req has propagated through two destination flops, the data has
been stable for at least two destination clock periods. There is no transition anywhere
near the sampling edge, so there is nothing to go metastable about."
The follow-up: "What if the source changes the data early?"
Then the scheme breaks, which is why busy exists - the source is
structurally prevented from touching data_held until phase 4 completes.
The protocol, not the wire, is what provides the safety.
And the one they really want: "What does this cost?" Two full round trips - about 4-6 clocks in each direction, so roughly 10-20 clocks per transfer. Fine for configuration registers. Useless for streaming data, which is what asynchronous FIFOs are for.
5.5 Reset domain crossing & reset trees
Volume 3.2 built the AASD reset synchronizer - asynchronous assert, synchronous de-assert - and noted that every clock domain needs its own. Here is the consequence nobody expects: once you have several reset signals, you have created a second, independent crossing problem. And unlike CDC, it can bite you inside a single clock domain.
| Fix | How it works | Trade-off |
|---|---|---|
| One reset domain | Reset both flops from the same synchronized reset | Simplest - do this unless you truly need independent resets |
| Qualify the crossing | Gate the path with an enable that is held off while either reset is active | Needs care to get the enable's own timing right |
| Synchronize the path | Put a 2-FF synchronizer on the crossing signal, reset by domain B | Adds latency; only valid for control, not wide data |
| Sequence the resets | Assert and release resets in a defined order so no crossing is ever live | System-level discipline, easy to break later |
The reset tree
The CDC verification reality
# CDC paths must be excluded from normal timing analysis - the tool would
# otherwise try (and fail) to close timing between unrelated clocks.
# Tell STA the two clocks have no phase relationship at all.
set_clock_groups -asynchronous \
-group [get_clocks clk_src] \
-group [get_clocks clk_dst]
# Bound the crossing wire anyway: keep the source-flop-to-FF1 delay short so
# FF1 gets the maximum possible settling time. A bare false_path would let
# the router make this arbitrarily long.
set_max_delay -datapath_only \
-from [get_cells src_ff_reg] \
-to [get_cells {sync_inst/sync_reg[0]}] 4.0
set_false_path and walk away
A false path tells the tool to ignore the crossing completely, which means the router is
free to give that wire a huge delay. Every nanosecond spent on the wire is a nanosecond
stolen from FF1's settling budget - directly reducing the MTBF you calculated in §5.1.
Use set_max_delay -datapath_only so the path is still bounded.
Volume 05 recap
| Concept | The one thing to remember |
|---|---|
| Metastability | Not a wrong value - no valid value yet. Resolution time is unbounded. |
| MTBF | Exponential in resolution time, linear in frequency. That asymmetry is the whole game. |
| 2-FF synchronizer | Turns ~22 ms into ~98 million years. Reduces risk; never eliminates it. |
| Synchronizer rules | No logic before FF1. Flops placed adjacent. Fan out to exactly one synchronizer. |
| Fast → slow pulses | A pulse can vanish. Convert to a toggle, synchronize, then edge-detect. |
| Multi-bit buses | Per-bit synchronizers produce values that never existed. Never do it. |
| The unifying fix | Reduce every crossing to a single bit - Gray, flag, handshake, or FIFO. |
| Handshake data bus | Unsynchronized and safe, because the protocol freezes it. |
| Reset domain crossing | Happens inside one clock domain. Audit resets, not just clocks. |
| Verification | Simulation cannot see CDC bugs. Static CDC analysis is the sign-off gate. |