Why this is the canonical CDC FIFO pattern
Whenever two blocks in a design run on genuinely unrelated (or rationally-unrelated) clocks and need to move a stream of data between them — a network MAC feeding a core running at a different clock, or two clock domains bridged over an on-chip interconnect — the asynchronous FIFO is the standard, general-purpose solution. Every other CDC technique (2-flop sync, pulse sync, handshake sync) is either a narrower special case or a building block used to construct something like this. If a chip design interview asks “how do you move data between two clock domains,” this is very often the expected answer.
The entire correctness of the design rests on one fact: Gray code changes exactly one bit per increment, so no matter when a synchronizer samples a Gray-coded pointer mid-transition, it captures either the old value or the new value — never a corrupted mix of both (which is exactly what could happen if you tried to synchronize a binary counter directly, since multiple bits can change at once and land on wildly wrong intermediate values).
Interface
| Signal | Domain | Description |
|---|---|---|
wr_clk |
write | Write-domain clock. |
wr_rst |
write | Synchronous reset for the write domain. |
wr_en |
write | Assert to push wr_data. |
wr_data |
write | Data to push. |
full |
write | High when the FIFO cannot accept another write. |
rd_clk |
read | Read-domain clock — may run at any frequency vs. wr_clk. |
rd_rst |
read | Synchronous reset for the read domain. |
rd_en |
read | Assert to pop the front entry. |
rd_data |
read | Current front-of-queue entry (fall-through). |
empty |
read | High when there is nothing left to read. |
Structure
- Each domain keeps a binary pointer (for addressing memory) and its Gray-coded equivalent (for crossing the boundary).
- The write domain’s Gray pointer is synchronized into the read domain with a 2-flop synchronizer; the read domain’s Gray pointer is synchronized into the write domain the same way. These are two independent synchronizer chains, one per direction.
fullandemptyare registered outputs computed from the next Gray-coded pointer value compared against the synchronized far-domain pointer. Using next-pointer-vs-synchronized-pointer (rather than combinationally gating the write/read enable on a same-cycle recomputation) avoids a spurious combinational dependency loop and matches the standard published design (Cliff Cummings, “Simulation and Synthesis Techniques for Asynchronous FIFO Design”).fullcompares the write domain’s next Gray pointer against the synchronized read pointer with its top two bits inverted — this is the correct wraparound comparison for Gray-coded pointers where the write pointer has lapped the read pointer by exactly one full trip around the buffer.emptycompares the read domain’s next Gray pointer directly against the synchronized write pointer, with no inversion.
Correctness constraints
- Read and write pointers must be converted to Gray code before crossing the clock boundary. Never synchronize a raw binary pointer across domains.
- Each direction needs its own independent 2-flop (minimum) synchronizer chain — do not share flops between the two directions.
fullandemptymust never be asserted simultaneously.DEPTHmust be a power of two, and pointers must be$clog2(DEPTH)+1bits wide (the extra bit disambiguates “wrapped all the way around” from “caught up exactly”).- The underlying dual-port memory itself does not need synchronization — only the pointers do — because the memory is written and read at different addresses by design (the pointers guarantee the reader never reads an address the writer hasn’t committed data to yet, once the synchronized pointer reflects it).
wr_rstandrd_rstare shown here as independent synchronous resets per domain for clarity; a production design would also reset-synchronize each domain’s asynchronous reset assertion (see the 2-flop synchronizer problem) so that reset release doesn’t itself create a new metastability hazard.