Clock Domain Crossing (CDC) & Asynchronous FIFO Design in Verilog
Master Clock Domain Crossing (CDC) and Asynchronous FIFO design in Verilog. Learn metastability, 2-FF synchronizers, Gray code pointers, and full/empty math.
By BitForBytes Editorial & Hardware Research Team, Official BitForBytes Hardware Publication · · 8 min read
⚡ Quick Answer for AI Summaries & Fast Reading
Clock Domain Crossing (CDC) occurs when data travels between flip-flops driven by unsynchronized, independent clock frequencies. Crossing multi-bit data directly causes metastability and bus skew. An Asynchronous FIFO (First-In, First-Out) buffer safely bridges asynchronous clock domains by buffering data in dual-port RAM while passing read and write pointers through Gray code conversion and 2-Flip-Flop (2-FF) synchronizers to ensure only one bit changes per clock cycle.
In modern System-on-Chip (SoC) architectures, different functional blocks run at vastly different clock frequencies. For example, a PCIe controller may run at 250 MHz, a DDR5 memory controller at 800 MHz, and a CPU core at 3.2 GHz.
When data moves between these unsynchronized clock domains, conventional sequential design rules break down. If a signal changes within the setup and hold time window of the destination flip-flop, the circuit enters a metastable state - producing non-deterministic voltage levels that can crash an entire chip.
This guide explores CDC fundamentals, why 2-FF synchronizers fail on multi-bit buses, and how to build a production-grade Asynchronous Dual-Clock FIFO in synthesizable Verilog.
1. What is CDC & Metastability?
When a signal generated in Clock Domain A arrives at a flip-flop clocked by Clock Domain B, the arrival time relative to Clock B's rising edge is completely random.
Clock A (Fast) ---+
Data A ------------+---> [ Destination D-FF (Clock B) ] ---> Metastable Output?
Clock B (Slow) -------> [ (Violates Setup/Hold) ]
The Mean Time Between Failures (MTBF)
If the data transition violates the destination flip-flop's setup (t_su) or hold (t_h) requirements:
- The flip-flop output oscillates between logic
0and1before settling to a random state. - The duration of this oscillation is non-deterministic.
- If downstream combinational logic samples this oscillating signal, incorrect states propagate across the chip.
For single-bit control signals, we mitigate metastability using a 2-Flip-Flop (2-FF) Synchronizer, allowing one full clock cycle for the metastable output of the first flop to resolve before being sampled by the second flop.
Asynch Input ---> [ D-FF 1 ] ---> (Metastable Node) ---> [ D-FF 2 ] ---> Synchronized Output
clk_dst clk_dst
2. Why 2-FF Synchronizers Fail for Data Buses
A common beginner mistake is passing a multi-bit binary bus (e.g., an 8-bit counter or data value) through parallel 2-FF synchronizers.
Why This Fails:
Due to physical on-chip wire variations, clock skew, and standard-cell delays, different bits in a bus experience slightly different propagation delays.
Binary Counter Transition: 4'b0111 (7) ---> 4'b1000 (8) [All 4 bits flip!]
If Bit[3] arrives slightly earlier than Bits[2:0]:
The destination domain might sample: 4'b1111 (15) ---> Corrupted Intermediate Data!
Because multiple bits change simultaneously, the receiving domain can capture an invalid intermediate value that never existed in the transmitter.
The Rule of CDC: Never synchronize multi-bit binary data directly across clock domains. Use an Asynchronous FIFO.
3. Asynchronous FIFO Architecture
An Asynchronous FIFO uses a dual-port SRAM or register file to store data, with two separate clock domains:
- Write Domain (
wclk): Writes data into memory and manages the write pointer (wptr). - Read Domain (
rclk): Reads data from memory and manages the read pointer (rptr).
+---------------------------------------------------------+
| Dual-Port Memory Buffer |
wdata --->| (Written at waddr on wclk) (Read at raddr on rclk) +----> rdata
+---------------------------------------------------------+
^ ^
| waddr | raddr
+----------+----------+ +----------+----------+
wclk --->| Write Pointer Logic | | Read Pointer Logic |<--- rclk
wr_en -->| (wptr & wfull) | | (rptr & rempty) |<--- rd_en
+----------+----------+ +----------+----------+
| wptr (Gray) | rptr (Gray)
| +---------------------+ |
+------->| 2-FF Sync to rclk +------->| (Calculates rempty)
| +---------------------+ |
| +---------------------+ |
+------->| 2-FF Sync to wclk |<-------+
(Calculates wfull) +---------------------+
4. Why Gray Code is Mandatory for Pointer Synchronization
To pass pointer values safely into the opposite clock domain via 2-FF synchronizers, pointers are converted from Binary to Gray Code.
The Gray Code Property: Only one bit changes between any two consecutive values (Hamming distance = 1).
| Decimal Value | Binary Representation | Gray Code Equivalent | Bits Changed from Previous |
|---|---|---|---|
| 0 | 0000 | 0000 | - |
| 1 | 0001 | 0001 | 1 bit (Bit 0) |
| 2 | 0010 | 0011 | 1 bit (Bit 1) |
| 3 | 0011 | 0010 | 1 bit (Bit 0) |
| 4 | 0100 | 0110 | 1 bit (Bit 2) |
| 5 | 0101 | 0111 | 1 bit (Bit 0) |
| 6 | 0110 | 0101 | 1 bit (Bit 1) |
| 7 | 0111 | 0100 | 1 bit (Bit 0) |
Because only one bit transitions at any clock edge, the receiving synchronizer can only sample either the old pointer value or the new pointer value - it can never sample a corrupted intermediate value.
Conversion Math in Verilog:
// Binary to Gray Code conversion
assign gray_code = binary_val ^ (binary_val >> 1);
5. Full and Empty Condition Logic
To distinguish between an Empty FIFO and a Full FIFO, we use an extra (N+1)th bit for pointer depth 2^N.
- FIFO Empty Condition (Evaluated in rclk Domain): The FIFO is empty when the synchronized write pointer equals the current read pointer:
assign rempty = (rptr_gray == sync_wptr_gray);
- FIFO Full Condition (Evaluated in wclk Domain): The FIFO is full when the write pointer has wrapped around the memory buffer exactly once while the read pointer has not:
- MSB is inverted.
- 2nd MSB is inverted.
- All remaining LSB bits are identical.
assign wfull = (wptr_gray == {~sync_rptr_gray[ADDRSIZE:ADDRSIZE-1], sync_rptr_gray[ADDRSIZE-2:0]});
Frequently Asked Questions
Why can't we use standard 2-FF synchronizers for multi-bit data buses?
Standard 2-FF synchronizers only work reliably for single-bit signals. On a multi-bit bus, unequal wire delays mean bits transition at different times, causing intermediate phantom values.
Why is Gray code used instead of binary for FIFO pointers?
Gray code ensures that exactly one bit changes during any sequential increment. This single-bit transition property guarantees receiving synchronizers never capture invalid intermediate values.
Is the FIFO full condition pessimistic or optimistic?
The full condition is pessimistic (safe). Because the read pointer takes two clock cycles to synchronize into the write domain, the write logic sees an older read pointer, which prevents data overwrite.