Watch read & write pointers advance on two completely independent clocks. See Gray code conversion bit by bit, the 2-FF synchronizer pipeline in action, and FULL/EMPTY flags trigger in real-time.
W→R (EMPTY):—→FF1: —→FF2: —=sync: —← used for EMPTY
R→W (FULL):—→FF1: —→FF2: —=sync: —← used for FULL
What is an Asynchronous FIFO?
An asynchronous FIFO (First-In First-Out buffer) allows data to be written and read using two completely independent clocks — there is no shared clock signal between the writer and the reader. This makes it the fundamental building block for crossing clock domain boundaries in modern SoC designs.
Common use cases include: PCIe controller ↔ NVMe SSD, CPU ↔ DDR PHY, USB MAC ↔ application logic, Ethernet MAC ↔ system fabric, and any interface between two IPs clocked at different frequencies.
Key problem: You cannot simply connect wires between two clock domains. A flip-flop in domain B sampling a signal from domain A may violate setup/hold timing and enter metastability — an indeterminate output that resolves to 0 or 1 unpredictably. The async FIFO solves this by synchronizing only a single-bit-changing Gray-coded pointer.
Property
Synchronous FIFO
Asynchronous FIFO
Clock domains
Single shared clock
Independent WCLK and RCLK
Pointer crossing
Not needed
2-FF synchronizer with Gray code
Full/Empty logic
Simple subtraction
Gray code comparison
Complexity
Low
Medium (5 modules)
Use case
Same-clock buffering
Clock domain crossing (CDC)
Architecture
A properly designed async FIFO (Cliff Cummings style) has five modules:
Why Gray Code?
This is the single most important insight in async FIFO design. When you synchronize a multi-bit signal across clock domains with 2 flip-flops, you are betting that the signal is stable when the destination FF samples it.
With a binary counter, transitioning from 3 (011) to 4 (100) changes all three bits simultaneously. If the destination FF samples during this transition, it might see 111, 010, 101 — any combination — not just 3 or 4. This corrupt value can cause a catastrophic FIFO over/underflow.
Gray code rule: Only 1 bit changes between any two consecutive values. Even if that 1 bit is caught mid-transition (metastability), the worst case is reading the old pointer — which is a conservative error (you think the FIFO has slightly less data than it really does). This is safe.
Count
Binary
Bits changed
Gray Code
Bits changed
0
000
—
000
—
1
001
1 bit
001
1 bit ✓
2
010
2 bits!
011
1 bit ✓
3
011
1 bit
010
1 bit ✓
4
100
3 bits!!!
110
1 bit ✓
5
101
1 bit
111
1 bit ✓
6
110
2 bits!
101
1 bit ✓
7
111
1 bit
100
1 bit ✓
Full & Empty Flag Logic
Pointer Width
For a FIFO of depth 2N, use (N+1)-bit pointers. The extra MSB acts as a wrap-around indicator, allowing full/empty distinction when both pointers point to the same address.
EMPTY Flag (Read Domain)
empty = (rptr_gray == wptr_gray_sync) // Both pointers at same gray value → no unread data // wptr_gray_sync = wptr_gray delayed by 2 RCLK cycles (2-FF sync)
FULL Flag (Write Domain)
full = (wptr_gray == { ~rptr_gray_sync[N:N-1], rptr_gray_sync[N-2:0] }) // Top 2 MSBs inverted — detects wrap-around in Gray code space // rptr_gray_sync = rptr_gray delayed by 2 WCLK cycles (2-FF sync)
The top-2-MSB inversion works because in Gray code, a pointer that has wrapped around once differs from the "same address" unwrapped pointer in exactly the top 2 MSBs. The remaining lower bits are identical in Gray code.
An async FIFO is a data buffer that allows safe data transfer between two clock domains with different (and potentially unrelated) frequencies or phases. You use it whenever two IPs must exchange data but run on separate clocks — e.g., PCIe controller to NVMe, CPU to DDR PHY, or USB MAC to application logic. The FIFO absorbs the timing difference and prevents metastability from propagating into the design.
When you synchronize a multi-bit counter across clock domains with 2 flip-flops, if more than 1 bit changes simultaneously, the destination FF could sample a completely invalid intermediate value (e.g., 011→100 could sample as 111, 010, 101...). Gray code guarantees only 1 bit changes per count step. Even if that 1 bit is caught mid-transition (metastability), the synchronized value is either the old count or the new count — both are safe, conservative values.
FULL is computed in the write domain. The synchronized rptr_gray (rptr_gray_sync, 2 WCLK cycles old) is compared to wptr_gray with the top 2 MSBs inverted: full = (wptr_gray == {~rptr_gray_sync[N:N-1], rptr_gray_sync[N-2:0]}). This pattern appears in Gray code when the write pointer has wrapped around once and is now exactly DEPTH entries ahead of the read pointer. The top-2-MSB inversion distinguishes "same address, same wrap" (empty/equal) from "same address, one wrap apart" (full).
The 2-FF synchronizer introduces a 2-cycle delay, so FULL/EMPTY flags are slightly conservative — they assert a bit later than the actual condition (from the other domain's perspective). This means the FIFO might be slightly fuller than the write domain thinks (it won't overwrite data) or slightly emptier than the read domain thinks (it won't read invalid data). This is safe by design. The FIFO may appear to have slightly less capacity than its rated depth due to this margin.
No. A single FF synchronizer is not sufficient for reliable CDC. The single FF output has insufficient time to resolve metastability before it is sampled by downstream logic. A 2-FF synchronizer gives the first FF one full destination clock cycle to settle, reducing the probability of metastability propagation to negligibly low levels (typically <10⁻¹⁴ errors/second for modern processes). High-speed designs at 1 GHz+ sometimes use 3-FF synchronizers for extra margin.