Modern SoCs, ASICs and FPGA designs commonly contain multiple clock domains. A processor may run at one frequency, a peripheral at another, and a high-speed interface at yet another. When data needs to move between these unrelated clock domains, simply connecting a multi-bit bus from one domain to another can create serious clock domain crossing (CDC) problems.
An asynchronous FIFO is one of the most widely used solutions for transferring multi-bit data safely between independent clock domains.
Unlike a synchronous FIFO, an asynchronous FIFO has separate write and read clocks. The write side operates using one clock, while the read side operates using another clock with no fixed phase or frequency relationship.
The challenging part is not storing the data. The difficult part is safely communicating the read and write positions between the two clock domains.
This article explains how to design an asynchronous FIFO for CDC applications, including its architecture, binary and Gray-code pointers, two-flop synchronizers, full and empty detection, RTL implementation considerations, and verification strategy.
FIFO stands for First-In, First-Out.
Data written first into the FIFO is read first, preserving the order of transactions.
In an asynchronous FIFO, the write and read operations use independent clocks:
WRITE CLOCK DOMAIN
|
Write Controller
|
v
+———–+
| Memory |
+———–+
|
^
Read Controller
|
READ CLOCK DOMAIN
For example:
write_clk = 100 MHz
read_clk = 75 MHz
The clocks may have completely different frequencies and phase relationships.
This makes the asynchronous FIFO useful when two subsystems cannot operate from the same clock.
Typical applications include:
Inskill’s CDC material also identifies asynchronous FIFOs as a common solution for multi-bit data transfer between clock domains.
Consider a simple 8-bit data bus crossing from clk_a to clk_b.
A beginner might try:
clk_a domain
|
8-bit data
|
v
clk_b domain
This is unsafe when the clocks are asynchronous.
Each bit can potentially be sampled at a different point in time. If the source data changes near the receiving clock edge, the destination may capture an inconsistent combination of bits.
A two-flop synchronizer is useful for a single-bit control signal, but it is not by itself a safe solution for transferring an arbitrary multi-bit data bus.
An asynchronous FIFO solves this problem by separating:
The data is stored in the FIFO memory, while the information about where the writer and reader are located is transferred safely between clock domains.
A typical asynchronous FIFO contains:
The architecture can be represented as:
WRITE CLOCK DOMAIN
|
Write Binary Ptr
|
v
Gray Conversion
|
v
Write Gray Ptr
|
| CDC
v
+————–+
| 2-FF Sync |
+————–+
|
v
READ CLOCK DOMAIN
READ CLOCK DOMAIN
|
Read Binary Ptr
|
v
Gray Conversion
|
v
Read Gray Ptr
|
| CDC
v
+————–+
| 2-FF Sync |
+————–+
|
v
WRITE CLOCK DOMAIN
The important design principle is:
Binary pointers are used locally for addressing and counting, while Gray-coded pointers are used for clock-domain crossing.
This is one of the most important concepts in asynchronous FIFO design.
A binary counter can change multiple bits during a single increment.
For example:
0111 → 1000
Four bits change at the same time.
If another asynchronous clock samples the counter during this transition, it could observe an unintended combination of bits.
That can produce an incorrect pointer value.
Gray code solves this problem because only one bit changes between adjacent Gray-code values.
For example:
Binary Gray
0000 0000
0001 0001
0010 0011
0011 0010
0100 0110
Therefore, asynchronous FIFO designs normally convert the local binary pointer to Gray code before sending it to the other clock domain.
The standard approach is widely used in asynchronous FIFO implementations and CDC methodologies.
The standard conversion from binary to Gray code is:
gray = binary ^ (binary >> 1);
For example, in SystemVerilog:
assign wr_ptr_gray = wr_ptr_bin ^ (wr_ptr_bin >> 1);
assign rd_ptr_gray = rd_ptr_bin ^ (rd_ptr_bin >> 1);
The binary pointer remains local to its own clock domain.
The Gray-coded version is the one that crosses into the other domain.
An asynchronous FIFO generally uses a pointer that is one bit wider than the memory address.
For a FIFO with depth 8:
Address bits = 3
Pointer bits = 4
The lower bits identify the memory location.
The additional bit helps identify whether the write pointer has wrapped around and caught up with the read pointer.
This distinction is important because:
write pointer == read pointer
can mean that the FIFO is empty, but pointer equality can also occur after the writer has wrapped around and filled the FIFO.
The extra pointer bit helps distinguish these conditions.
After converting a pointer to Gray code, it still cannot be directly used in the opposite clock domain.
It must pass through a synchronizer.
For example:
Write Gray Pointer
|
v
+——-+
| Sync1 |
+——-+
|
v
+——-+
| Sync2 |
+——-+
|
v
Read Domain
A simplified SystemVerilog implementation is:
always_ff @(posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n) begin
wr_gray_sync1 <= ‘0;
wr_gray_sync2 <= ‘0;
end
else begin
wr_gray_sync1 <= wr_ptr_gray;
wr_gray_sync2 <= wr_gray_sync1;
end
end
The same approach is used to transfer the read pointer into the write domain.
The first synchronizer stage may encounter metastability. The second stage provides additional time for that metastability to resolve before the synchronized pointer is used by destination-domain logic.
Importantly, the synchronizer does not make metastability impossible. It reduces the probability that metastability propagates into functional logic.
The write pointer belongs entirely to the write clock domain.
A typical sequence is:
write request
|
v
FIFO full?
/ \
YES NO
| |
No write Write data
|
v
Increment pointer
|
v
Binary → Gray
The pointer should advance only when a valid write is accepted.
Conceptually:
if (wr_en && !full)
wr_ptr_bin <= wr_ptr_bin + 1’b1;
The corresponding Gray pointer is then generated from the next binary pointer.
The write address is derived from the local binary pointer.
The read pointer operates independently using the read clock.
The basic sequence is:
read request
|
v
FIFO empty?
/ \
YES NO
| |
No read Read data
|
v
Increment pointer
|
v
Binary → Gray
Conceptually:
if (rd_en && !empty)
rd_ptr_bin <= rd_ptr_bin + 1’b1;
The read address comes from the local read pointer.
The empty condition is generated in the read clock domain.
Conceptually, the FIFO becomes empty when the next read pointer catches up with the synchronized write pointer.
A simplified expression is:
empty = next_read_gray == synchronized_write_gray
The important point is that the read side does not directly compare its pointer against an asynchronous write pointer.
It compares against the synchronized Gray-coded write pointer.
This keeps the comparison within the read clock domain.
Full detection is generated in the write clock domain.
The write side compares its next Gray-coded pointer against the synchronized read pointer.
For a conventional power-of-two asynchronous FIFO, the full comparison uses the wrap-around relationship represented by the additional pointer bits.
Conceptually:
next_write_pointer
|
v
Compare with
synchronized read pointer
|
v
FULL
In a common Gray-pointer implementation, the required upper pointer bits are inverted for the full comparison while the remaining bits match. This distinguishes a full FIFO from an empty FIFO after pointer wrap-around.
Putting the pieces together:
WRITE DOMAIN
|
wr_en
|
v
Full Check
|
+—+—+
| |
Full Not Full
| |
Stop Write
|
v
Memory Write
|
Binary Pointer
|
Gray Convert
|
v
2-FF Sync
|
v
READ DOMAIN
READ DOMAIN
|
rd_en
|
v
Empty Check
|
+—+—+
| |
Empty Not Empty
| |
Stop Read
|
v
Memory Read
|
Binary Pointer
|
Gray Convert
|
v
2-FF Sync
|
v
WRITE DOMAIN
This architecture allows the two sides to operate independently.
Instead of placing everything inside one large RTL module, a clean asynchronous FIFO can be divided into logical blocks:
async_fifo
│
├── FIFO memory
│
├── write pointer logic
│
├── read pointer logic
│
├── write-to-read synchronizer
│
├── read-to-write synchronizer
│
├── full generation
│
└── empty generation
This modular structure makes the design easier to review, debug and verify.
It also makes CDC analysis more straightforward.
This is a common interview question.
Suppose a binary pointer changes:
0111 → 1000
Several bits change simultaneously.
Because the receiving clock is asynchronous, it may sample the transition at an arbitrary point.
The resulting value can be invalid.
With Gray code:
Gray pointer
changes by one bit
↓
Only one CDC bit changes
↓
Synchronize Gray pointer
↓
Use synchronized value
That is why asynchronous FIFO designs normally transfer Gray-coded pointers rather than raw binary pointers.
Reset handling is another important part of asynchronous FIFO design.
At reset, both pointers are normally initialized to zero:
write pointer = 0
read pointer = 0
This results in the FIFO initially being empty.
However, reset signals themselves cross clock-domain boundaries in many real designs, so reset assertion and especially reset de-assertion must be designed carefully.
For independently reset clock domains, engineers need to consider:
A FIFO that works correctly during normal operation can still have a CDC problem if reset behavior is not considered.
Designing the FIFO is only half of the task.
A proper verification environment should test the FIFO with independent clocks.
For example:
write_clk = 10 ns
read_clk = 14 ns
The relative phase continuously changes, giving the testbench different sampling relationships.
Useful verification scenarios include:
Write several values and confirm that they are read in exactly the same order.
Read until the FIFO becomes empty and verify that no additional read is accepted.
Write until the FIFO becomes full and verify that additional writes are blocked.
Perform reads and writes at the same time using independent clocks.
Try several combinations:
Fast write / slow read
Slow write / fast read
Equal frequency / unrelated phase
Test reset at different points in the FIFO transaction sequence.
Generate random write/read operations and compare the FIFO output against a reference queue.
A self-checking reference model is especially useful because it can verify both data ordering and data integrity. Current asynchronous-FIFO verification examples commonly combine reference models, assertions and randomized traffic.
SystemVerilog Assertions can help verify important FIFO properties.
For example, the write pointer should not advance when the FIFO is full:
full && wr_en
|
v
write pointer must remain unchanged
Similarly:
empty && rd_en
|
v
read pointer must remain unchanged
Another useful property is checking the Gray-code transition.
Only one Gray-code bit should change between consecutive pointer values.
These checks help detect CDC implementation errors early.
Simulation alone cannot prove that a CDC architecture is safe.
Static CDC analysis tools can inspect clock crossings and identify issues such as:
Inskill’s Lint and CDC training specifically includes asynchronous FIFO analysis, binary-to-Gray synchronization, multi-bit crossings and CDC labs.
For production RTL, the asynchronous FIFO should therefore be checked using both functional verification and dedicated CDC analysis.
Asynchronous FIFOs are useful whenever data needs to cross between unrelated clock domains while maintaining ordering and buffering.
Typical applications include:
They are especially useful when the producer and consumer operate at different rates.
Designing an asynchronous FIFO for CDC applications requires more than implementing a memory with read and write pointers.
The key is safely transferring pointer information between independent clock domains.
The standard architecture combines:
Binary pointers → Gray-code conversion → clock-domain synchronization → full/empty detection
The write and read sides maintain their own local state, while synchronized Gray-coded pointers provide the information required to determine whether the FIFO is full or empty.
For beginners, the most important concepts to master are CDC, metastability, Gray code, pointer synchronization, full/empty detection and reset handling.
Once these concepts are clear, asynchronous FIFO design becomes much easier to understand and is an excellent practical RTL project for learning real-world CDC design.