13.7 Synchronization: Crossing Clock Domains Safely
Explain the clock-domain-crossing metastability risk, describe the two-flip-flop synchronizer and its limits, and outline pulse synchronization, handshaking, and async FIFOs as solutions for harder cases.
Any real digital system of meaningful size has more than one clock domain — a CPU clock, a peripheral bus clock, an external interface clock, or simply a genuinely asynchronous input signal (a button press, an external sensor edge) with no fixed timing relationship to any internal clock at all. Whenever a signal crosses from one clock domain (or from asynchronous-land) into another, the receiving flip-flop's setup/hold requirements cannot be guaranteed to be satisfied by construction, because the two clocks (or the signal and the destination clock) have no fixed phase relationship — this is the clock-domain-crossing (CDC) problem, and getting it wrong is one of the most common causes of intermittent, hard-to-debug hardware failures in real systems.
Core concept: why crossing domains risks metastability
When an asynchronous signal (or a signal from a different, unrelated clock domain) happens to transition right at — or within the setup/hold window around — the destination flip-flop's active clock edge, the flip-flop can enter a metastable state: an unstable intermediate condition where its output hovers at neither a clean logic 0 nor a clean logic 1 for an unpredictable amount of time before eventually resolving to one or the other (recall Module 7.8, Metastability). Because there is no fixed phase relationship between an asynchronous source and the destination clock, this setup/hold-violating alignment is not a rare edge case that can be engineered away — it will happen, with a probability and average frequency that can be estimated but never reduced to exactly zero, which is why the spec's own framing is blunt: never assume an asynchronous signal is safe to sample directly in another clock domain.
If a metastable flip-flop output is allowed to propagate onward to more logic before it has resolved, different downstream gates can interpret its still-settling voltage differently (some reading it as a 0, others as a 1, or worse, reading different values at different times as it continues to move), potentially corrupting state inconsistently in different parts of the design — a much worse outcome than a single flip-flop simply taking one extra cycle to settle.
The two-flip-flop synchronizer
The standard, simplest mitigation is the two-flip-flop (2-FF) synchronizer: the asynchronous or cross-domain signal is fed into a chain of two (sometimes three, for extra margin) flip-flops, both clocked by the *destination* clock domain, with the second flip-flop's output being the only version of the signal the rest of the destination-domain logic is allowed to use. The first flip-flop may indeed go metastable if the input transitions at a bad moment — that's expected and unavoidable — but it is given a full clock period to resolve before the second flip-flop samples *its* output; because metastability resolution time follows an exponentially decaying probability (the longer you wait, the exponentially less likely the flop is still unresolved), giving it even one full extra clock period dramatically reduces the probability that the second flip-flop also samples a still-metastable value, to the point where the resulting Mean Time Between Failures (MTBF) for a well-designed synchronizer, at realistic clock frequencies, becomes astronomically large (often stated in years or centuries) rather than being driven to a literal, provable zero.
Crucially, the 2-FF synchronizer only reduces metastability *propagation* risk — it does not, and cannot, eliminate the possibility of metastability occurring at all (this mirrors the exact wording of the spec's own tricky point back in the Module 7 metastability lesson). It is also only valid for a single-bit, relatively slowly-changing signal; it does not, by itself, solve two much harder related problems: safely transferring a multi-bit bus across a clock-domain boundary (where different bits could be sampled from different, inconsistent 'moments' if not handled carefully, producing an invalid intermediate value even if each individual bit's synchronizer works correctly) or safely transferring a single narrow pulse from a fast domain to a slower one.
Harder cases: pulse synchronization, handshaking, and async FIFOs
A single-cycle pulse generated in a fast clock domain can simply be missed entirely if it's narrower than one period of a much slower destination clock — the destination domain might never see any active clock edge occur while the pulse is actually asserted. The standard fix is to convert the pulse into a level change (e.g., a toggle flip-flop that flips state on each source pulse) which is inherently more robust to synchronize since a level, unlike a narrow pulse, is guaranteed to still be present at some destination clock edge, and then re-detect a transition on that synchronized, toggled signal back in the destination domain to reconstruct a clean pulse there.
For multi-bit data, the reliable solutions are Gray-coding (arranging a counter or value so that only a single bit changes between any two consecutive values, so a synchronizer sampling mid-transition can, at worst, be one count off rather than reading a wildly incorrect intermediate value with multiple bits in flux) or explicit req/ack handshake protocols, where the sending domain asserts a request and waits for a synchronized acknowledgment from the receiving domain before considering the data transferred, trading throughput for guaranteed correctness. The most complete, widely-used real-world solution for bulk data crossing clock domains is the asynchronous FIFO: a dual-clock-domain buffer where writes happen on one clock, reads happen on an entirely different clock, and the FIFO's internal full/empty status flags are generated by comparing Gray-coded read/write pointers that are each synchronized (via 2-FF synchronizers) into the *other* domain before the comparison — combining nearly every technique in this lesson (2-FF synchronization, Gray coding, and careful handshake-like pointer comparison) into one robust, standard building block.
Two-flip-flop synchronizer structure
Never tap the output of FF1 directly for use elsewhere in the design — only FF2's (or the final stage's) output is considered safely synchronized.
async_signal ──►[FF1]──►[FF2]──► synchronized_signal
▲clk_dst ▲clk_dst
FF1 may go metastable (input violated setup/hold relative to clk_dst)
FF1 gets one full clk_dst period to resolve before FF2 samples it
FF2's output is the only version the rest of the destination domain uses
MTBF improves exponentially with each additional synchronizer stage
and with each additional clock period of resolution time given.Code Examples
Because the destination clock samples only once every 20 ns while the pulse is only asserted for 2 ns, there's a large window of time where the destination domain simply never looks at the signal while it's active — synchronizing the raw pulse with a 2-FF synchronizer does not fix this, since a 2-FF synchronizer only handles metastability risk for a signal that IS sampled, it does nothing if the destination clock never samples during the pulse's brief active window at all. This is precisely why pulse-to-level (toggle) conversion is required before synchronizing a fast, narrow pulse into a much slower domain.
A 2-FF synchronizer alone does not guarantee a fast, narrow pulse survives a crossing into a much slower clock domain — toggle/level conversion is required first.
Fast domain clk_src period = 2 ns (500 MHz)
Slow domain clk_dst period = 20 ns (50 MHz)
src_pulse: a single clk_src-wide pulse, 2 ns long
clk_dst edges occur only once every 20 ns
→ There is roughly a 90% chance (2ns / 20ns window, worst case)
that NO clk_dst active edge occurs while src_pulse is high,
so a naive synchronizer on src_pulse directly can simply
miss the pulse entirely, not just delay or metastable-glitch it.Important Rules
Never sample an asynchronous or cross-domain signal directly with ordinary combinational logic or a single flip-flop with no synchronizer — metastability risk is real, not theoretical.
A 2-FF (or 3-FF) synchronizer reduces metastability propagation risk to a very low, but never exactly zero, probability — it does not eliminate metastability outright.
Only the final synchronizer stage's output is safe to use elsewhere — never tap an intermediate stage that might still be resolving.
Multi-bit values need Gray coding or a handshake protocol, not just N parallel 2-FF synchronizers, because independently-synchronized bits can be sampled from inconsistent moments.
Narrow, single-cycle pulses crossing into a slower domain need pulse-to-level (toggle) conversion before synchronization, or they can be missed entirely.
CDC verification as a standard signoff step
Every SoC (system-on-chip) with multiple clock domains — which is essentially every modern chip — undergoes dedicated clock-domain-crossing (CDC) verification as a mandatory step in chip signoff, using specialized static CDC analysis tools that automatically flag every signal crossing a clock-domain boundary without a proper synchronizer, precisely because this class of bug is so easy to introduce accidentally and so costly (a chip re-spin) to discover late.
Assuming an asynchronous input (or a signal from another clock domain) is 'probably fine' to sample directly because setup/hold violations 'usually' don't happen.
Always pass any asynchronous or cross-clock-domain signal through a proper synchronizer (2-FF minimum), and use pulse/Gray-code/handshake techniques as appropriate for pulses and multi-bit data.
This is the spec's own explicit tricky-problem warning, and it is one of the most consequential mistakes in real digital design: metastability-driven CDC bugs are famously intermittent, can pass simulation and even pass in the lab for a long time, and then surface unpredictably in the field — exactly the kind of failure mode that unsynchronized signal crossings are known to cause.
Tricky problem (from the spec): why should you never assume an asynchronous signal is safe to sample directly in another clock domain? A. Because asynchronous signals are always logically invalid B. Because there is no fixed phase relationship between an asynchronous source and the destination clock, so the signal can transition within the destination flip-flop's setup/hold window at any time, risking metastability — a real, physically unavoidable possibility, not a rare corner case that can be assumed away C. Because asynchronous signals only exist in simulation, never in real hardware D. Because synchronous logic cannot physically connect to asynchronous signals at all
B. Because there is no fixed phase relationship between an asynchronous source and the destination clock, so the signal can transition within the destination flip-flop's setup/hold window at any time, risking metastability — a real, physically unavoidable possibility, not a rare corner case that can be assumed away — Without a fixed timing relationship between the source and destination clock domains, there is always some (small but nonzero) probability that a signal transition lands within the destination flip-flop's setup/hold window on any given sampling instant — making metastability a real, ongoing risk rather than something a designer can rule out by inspection or luck.
Interview question: a 2-FF synchronizer is correctly implemented on a single-bit control signal, yet the design still occasionally malfunctions when that same synchronization technique is applied independently, bit-by-bit, to an 8-bit bus crossing the same clock-domain boundary. What is the most likely cause? A. The 2-FF synchronizer doesn't work for single bits, only for full buses B. Applying independent 2-FF synchronizers to each bit of a multi-bit bus does not guarantee all bits are sampled from the same, consistent source-domain moment — different bits can resolve on different destination cycles if the bus value changes near the sampling instant, producing an invalid combined value even though each individual bit's synchronizer is functioning correctly C. 2-FF synchronizers only work at frequencies below 1 MHz D. The bus must be using open-drain outputs, which are incompatible with synchronizers
B. Applying independent 2-FF synchronizers to each bit of a multi-bit bus does not guarantee all bits are sampled from the same, consistent source-domain moment — different bits can resolve on different destination cycles if the bus value changes near the sampling instant, producing an invalid combined value even though each individual bit's synchronizer is functioning correctly — Per-bit synchronization only guarantees each individual bit is metastability-safe — it says nothing about whether all bits, sampled together, represent one single consistent value from the source domain if the whole bus was changing near the sampling instant. This is exactly why multi-bit crossings need Gray coding (so at most one bit differs between adjacent values, bounding the error to at most one count) or a full handshake/FIFO protocol, rather than naively synchronizing each bit independently.
Design (in words/block diagram terms) a scheme to reliably transfer a single-cycle pulse from a fast source clock domain to a much slower destination clock domain, using the toggle technique described in this lesson.
Source domain: build a toggle flip-flop clocked by clk_src that flips its output state every time src_pulse occurs (rather than pulsing), producing a signal that changes level once per pulse and then holds that level until the next pulse — inherently safe to synchronize since a held level, unlike a narrow pulse, is guaranteed to be present across at least one full clk_dst period. Destination domain: pass this toggle signal through a standard 2-FF synchronizer clocked by clk_dst, then compare the synchronized signal's current value against its own value one clk_dst cycle earlier (an edge detector); a detected change indicates one source pulse occurred, and the destination domain can generate its own clean, one-clk_dst-cycle-wide pulse from that detected transition. This combines toggle/level conversion (to avoid missing the pulse) with a proper 2-FF synchronizer (to avoid metastability propagation) — exactly the two techniques this lesson introduces used together.
Think in two halves: how the source domain converts its pulse into something robust to synchronize, and how the destination domain converts that back into a usable pulse.