Clock domain crossing (CDC) is a point in a digital circuit where a signal passes between two parts of the design that run on different clocks. Every time data moves from one clock frequency to another, or between two clocks that aren’t perfectly aligned, you have a CDC. Managing these crossings correctly is one of the most important challenges in chip and FPGA design, because getting it wrong leads to data corruption, random glitches, and failures that are nearly impossible to debug after the fact.
Why Clock Domain Crossings Exist
Modern digital systems rarely run on a single clock. A processor core might operate at one frequency, a memory bus at another, and a peripheral interface at a third. Even two clocks with the same frequency can be “asynchronous” to each other if they come from different oscillators, because their edges won’t line up perfectly. Any signal that travels between these independently clocked regions crosses a clock domain boundary.
The fundamental problem is timing. A receiving flip-flop expects data to be stable for a brief window around its clock edge. If the sending clock and receiving clock aren’t synchronized, data can arrive right at the moment the receiving flip-flop is trying to capture it. This violates what’s called setup and hold time, and the flip-flop’s output can enter an unstable, in-between voltage called metastability. Instead of cleanly resolving to a 0 or 1, the output hovers and may eventually settle to the wrong value, or take so long to resolve that downstream logic misreads it.
Synchronizers: The Basic Solution
The simplest and most common way to handle a single-bit CDC is a two-flop synchronizer. This is just two flip-flops connected in series, both clocked by the receiving domain’s clock. The first flip-flop may go metastable, but it has an entire clock period to settle before the second flip-flop samples it. This dramatically reduces the probability of metastability propagating into the rest of the design.
Two flops are the standard in most designs, though some safety-critical or very high-speed applications use three. The tradeoff is latency: each additional flop adds one clock cycle of delay in the receiving domain. For a single control signal (like a reset or enable), a two-flop synchronizer is usually all you need.
Multi-Bit Signals Need Special Handling
A two-flop synchronizer works for one bit, but it breaks down for multi-bit buses. If you synchronize each bit of a binary counter independently, different bits can arrive at the receiving domain on different clock cycles. A value changing from 0111 to 1000, for example, could be sampled as 0000, 1111, or any other combination mid-transition. Every bit changes simultaneously in binary encoding, so the receiving side can capture a value that never actually existed on the sending side.
Gray code solves this for counters and sequential values. In Gray code, only one bit changes between any two consecutive values. This means that even if the receiving domain catches the value mid-transition, it will read either the old value or the new value, never a corrupted intermediate. This is why asynchronous FIFOs, one of the most widely used CDC structures, encode their read and write pointers in Gray code before synchronizing them across domains.
For arbitrary data (not sequential counters), Gray code doesn’t help. In those cases, you need a handshaking protocol or an asynchronous FIFO to ensure the data is stable before the receiving domain reads it.
Asynchronous FIFOs
An asynchronous FIFO is the workhorse of CDC when you need to transfer streams of data between clock domains. It consists of a dual-port memory (one port for writing, one for reading), a write pointer driven by the sending clock, and a read pointer driven by the receiving clock. The write side pushes data in; the read side pulls data out. Neither side needs to know anything about the other’s clock.
The key to making this work is synchronizing the pointers. The write pointer is Gray-coded and sent through a two-flop synchronizer into the read domain, and vice versa. Arm’s NIC-400 interconnect, widely used in system-on-chip designs, implements its CDC bridges with exactly this FIFO-pointer structure. When both pointers have the same value, the FIFO is empty. When they’re diametrically opposite (accounting for the Gray encoding), it’s full. The synchronized pointers are always slightly stale due to synchronization latency, which means the FIFO may appear slightly more full or more empty than it actually is. This is a safe, conservative error: it might cause a brief stall, but it never causes data loss.
Handshake Protocols
When you don’t need continuous streaming but do need to transfer individual data words reliably, handshake protocols are a lighter-weight alternative to a full FIFO. The idea is simple: the sender says “data is ready,” the receiver says “got it,” and only then can the sender move on.
2-Phase Handshake
In a 2-phase handshake, the protocol works on signal transitions (edges) rather than levels. Both a request signal and an acknowledge signal start low. When the sender has valid data, it toggles the request signal. This change propagates through a two-flop synchronizer into the receiving domain. The receiver captures the data and toggles the acknowledge signal, which travels back through another synchronizer. Once the acknowledge reaches the sending domain, both signals have the same value again, and the system is ready for the next transfer. The entire transaction completes in two transitions: one request toggle, one acknowledge toggle.
4-Phase Handshake
A 4-phase handshake works on signal levels instead of edges, which makes the control logic simpler at the cost of more steps. The sequence goes like this: the sender raises the request signal (phase 1). The receiver accepts the data and raises the acknowledge signal (phase 2). The sender then lowers request (phase 3). The receiver sees request go low and lowers acknowledge (phase 4). Both signals are back to zero, and a new transfer can begin. Each transition passes through a synchronizer, so the round trip takes longer than a 2-phase handshake, but the level-based logic is easier to implement and verify.
Timing Constraints for CDC Paths
Normal timing analysis assumes all flip-flops share the same clock and checks whether signals arrive within the required setup and hold windows. CDC paths violate that assumption entirely, so they need special treatment in your design constraints.
The most common approach is to tell the timing tool to ignore CDC paths altogether using a set_false_path constraint or a set_clock_groups constraint that declares two clocks as asynchronous. This prevents the tool from reporting meaningless timing violations on paths that are handled by synchronizers. For cases where you want partial analysis, set_max_delay -datapath_only constrains the combinational delay without requiring a specific clock relationship. Multi-bit buses that cross domains can be constrained with set_bus_skew, which limits how much the arrival times of individual bits can spread apart.
Getting these constraints right is critical. Marking a path as a false path when it isn’t actually synchronized is a common source of silicon bugs. The constraint tells the tool to stop worrying about the path, so if the synchronization logic is missing or incorrect, nothing will catch the error during timing analysis.
Verifying CDC Correctness
CDC bugs are notoriously hard to find through simulation alone, because metastability events depend on the exact timing relationship between two clocks at the moment a signal crosses. In simulation, clocks are typically ideal, and the bug may only appear with a specific, unlikely alignment. This is why the industry relies heavily on static CDC analysis tools in addition to simulation.
Structural CDC verification traces the design’s topology without any test vectors. It checks whether every crossing point has a proper synchronizer, whether multi-bit signals are handled with Gray coding or handshakes, and whether reconvergent paths (where a synchronized signal fans out and recombines) could create inconsistencies. This approach has been deployed on complex designs down to 32 nm process nodes and integrates into standard static timing analysis flows.
Functional CDC verification complements the structural checks. It uses simulation with assertions to verify higher-level protocol correctness: does the handshake actually complete, do FIFO pointers behave correctly under back-pressure, do reconverging signals maintain consistency? Combining both structural and functional verification significantly improves CDC coverage compared to either method alone. Most commercial RTL-level CDC tools have standard structural rules built into their algorithms, making the baseline checks largely automated.
Common CDC Mistakes
The most frequent CDC error is synchronizing a multi-bit bus one bit at a time without Gray coding or a handshake. This creates the conditions for a corrupted value that never existed on the sending side. A close second is forgetting to synchronize at all, which sometimes happens when a signal appears to be in the same domain but actually isn’t, due to clock gating or generated clocks.
Reconvergence is another subtle trap. If a single signal crosses a domain boundary, gets synchronized, and then fans out to two different pieces of logic that later recombine, the two paths may see the synchronized signal resolve on different clock cycles. The result is a brief window where downstream logic operates on inconsistent inputs. Static CDC tools flag these reconvergent paths specifically because they’re easy to miss in code review.
Glitch propagation through combinational logic before a synchronizer is another classic problem. If a signal passes through gates before reaching the first synchronizer flip-flop, those gates can produce short glitches that the synchronizer faithfully captures. The fix is to ensure that only a direct flip-flop output feeds into the synchronizer, with no combinational logic in between.

