A FIFO (First-In, First-Out) is a commonly used data-storage structure in digital and RTL design. It temporarily stores data so that information written into the FIFO is read in the same order. FIFOs are widely used in processors, communication interfaces, data pipelines, SoCs, and systems where the producer and consumer do not always operate at the same rate.
Two important FIFO architectures are synchronous FIFO and asynchronous FIFO.
The main difference is the relationship between the read and write clocks:
This difference significantly changes the RTL architecture, pointer handling, full/empty detection, and clock-domain-crossing requirements.
Understanding this distinction is important for anyone learning Verilog, SystemVerilog, RTL design, FPGA design, or VLSI design.
A FIFO is a memory structure where the first piece of data written is the first piece of data read.
For example, if the following values are written:
10 → 20 → 30 → 40
they must be read in the same order:
10 → 20 → 30 → 40
A typical FIFO contains:
The write pointer identifies where new data should be stored, while the read pointer identifies which stored data should be read.
The way these pointers and status flags are implemented is different for synchronous and asynchronous FIFOs.
A synchronous FIFO uses a common clock for both writing and reading.
For example:
Common Clock
|
+——–+——–+
| |
Write Logic Read Logic
| |
+——- FIFO —-+
Both the write and read operations occur with respect to the same clock domain.
If the FIFO receives:
clk = 100 MHz
both its write and read logic operate from that clock.
The design therefore does not require clock-domain crossing between the read and write sides.
When write_enable is asserted and the FIFO is not full, data is written into the memory.
When read_enable is asserted and the FIFO is not empty, data is read from the memory.
A simplified operation looks like:
if (write_enable && !full)
memory[write_pointer] <= data_in;
if (read_enable && !empty)
data_out <= memory[read_pointer];
The read and write pointers are updated according to the same clock.
Because both sides belong to the same clock domain, the pointer and status logic is comparatively straightforward.
An asynchronous FIFO is designed for data transfer between two different clock domains.
For example:
Write Clock Domain Read Clock Domain
wr_clk rd_clk
| |
v v
Write Logic —> FIFO Memory —> Read Logic
| |
write pointer read pointer
The write side may operate at 100 MHz while the read side operates at 75 MHz.
The clocks can also have no fixed phase relationship.
This creates a clock domain crossing (CDC) problem.
Simply passing binary read/write pointers directly from one clock domain to another is unsafe because the receiving clock can sample a changing multi-bit value.
Therefore, asynchronous FIFO designs require additional CDC techniques, commonly including Gray-coded pointers and synchronizers.
Feature | Synchronous FIFO | Asynchronous FIFO |
Read clock | Same clock | Independent clock |
Write clock | Same clock | Independent clock |
Clock domains | One | Two |
CDC required | No | Yes |
Pointer implementation | Usually binary | Binary + Gray code |
Synchronizers | Normally not required | Required |
Design complexity | Lower | Higher |
Full/empty logic | Simpler | More complex |
Metastability concern | Lower | Important |
Typical application | Same-clock modules | Different clock domains |
The fundamental architectural distinction is therefore one clock domain versus two independent clock domains.
In a synchronous FIFO, binary pointers can generally be used directly because both pointers are controlled by the same clock.
For example, for an 8-entry FIFO:
Write Pointer
|
v
0 1 2 3 4 5 6 7
After reaching the final location, the pointer wraps around.
The FIFO can determine its status by comparing the read and write pointers or by maintaining a count of stored entries.
For example:
empty = (write_pointer == read_pointer)
Full detection depends on the chosen pointer architecture.
One common implementation uses an additional pointer bit to distinguish between the empty and full conditions when the address portions are equal.
The pointer architecture becomes more complicated when the FIFO has independent clocks.
The write pointer belongs to the write clock domain.
The read pointer belongs to the read clock domain.
However, each side needs information about the other side to determine whether the FIFO is full or empty.
For example:
Write Domain Read Domain
write pointer —–> synchronized
read pointer
read pointer ——> synchronized
write pointer
These pointer values cross clock-domain boundaries.
This is where Gray code becomes important.
A normal binary counter can change multiple bits at the same time.
For example:
0111 → 1000
Four bits change during this transition.
If another clock samples the binary value while the transition is occurring, it may observe an inconsistent combination of bits.
Gray code solves an important part of this problem because only one bit changes between consecutive Gray-code values.
For example:
Binary: 000 → 001 → 010 → 011
Gray: 000 → 001 → 011 → 010
Only one Gray-code bit changes during each increment.
The asynchronous FIFO therefore commonly maintains:
The Gray-coded pointer is synchronized into the opposite clock domain before it is used for status generation.
FIFO status detection is another important difference.
Since both pointers operate in the same clock domain, the design can compare them directly or use a counter.
Typical empty logic is conceptually:
write_pointer == read_pointer
Full detection is implemented according to the FIFO depth and pointer scheme.
The write side needs a synchronized version of the read pointer.
Similarly, the read side needs a synchronized version of the write pointer.
Therefore:
Write Domain:
write pointer + synchronized read pointer
↓
FULL logic
Read Domain:
read pointer + synchronized write pointer
↓
EMPTY logic
This avoids directly using an asynchronous signal in local synchronous logic.
Suppose the write pointer changes according to wr_clk while the read-side logic samples it using rd_clk.
Because the clocks are unrelated, the read-side flip-flop may sample the pointer during a transition.
This can cause metastability.
A common solution is to pass the Gray-coded pointer through synchronizer stages before using it in the receiving clock domain.
Conceptually:
Write Domain
Gray Pointer
|
v
+———–+
| Sync FF 1 |
+———–+
|
v
+———–+
| Sync FF 2 |
+———–+
|
v
Read Domain
This is one of the major architectural differences between synchronous and asynchronous FIFO designs.
The choice depends primarily on the clock relationship between the producer and consumer.
An asynchronous FIFO should not be selected simply because it appears more flexible. Its CDC logic makes it more complex to design, verify, and debug.
Whether you are designing a synchronous or asynchronous FIFO, several mistakes can cause functional problems.
If the full condition is calculated incorrectly, the FIFO can overwrite unread data.
Incorrect empty logic can cause the design to read invalid or unavailable data.
The design should prevent:
Write when FIFO is full
Read when FIFO is empty
These boundary conditions should also be explicitly tested during verification.
This is a major asynchronous FIFO design error.
Multi-bit binary counters should not simply be transferred into an unrelated clock domain and assumed to be safe.
The remote pointer must be safely synchronized before it is used for local-domain status logic.
Reset behavior must be carefully defined for both clock domains, particularly in an asynchronous FIFO.
An asynchronous FIFO should be tested with different write and read clock frequencies and phases.
FIFO verification should test normal operation as well as boundary conditions.
Important test scenarios include:
For asynchronous FIFOs, CDC verification is particularly important because the design crosses independent clock domains.
Inskill’s CDC training material specifically covers multi-bit crossing, Gray-code synchronization, asynchronous FIFO lint analysis, and CDC labs, which makes this topic directly relevant to practical RTL development.
The difference between these two FIFO architectures is also a common RTL design discussion.
A good interview answer should not stop at:
“Synchronous FIFO has one clock and asynchronous FIFO has two clocks.”
A stronger answer explains the architectural consequences.
For example:
Synchronous FIFO:
Both read and write operations belong to the same clock domain. Binary pointers can generally be used directly, and there is no read/write CDC between independent clocks.
Asynchronous FIFO:
Read and write operations belong to separate clock domains. Gray-coded pointers are commonly synchronized into the opposite domain, and full/empty detection is generated using synchronized pointer information.
This demonstrates an understanding of why the RTL implementation changes, rather than simply memorizing definitions.
The simplest way to remember the difference is:
Synchronous FIFO
↓
One clock
↓
Simpler pointer logic
↓
No FIFO-level CDC between read/write sides
Whereas:
Asynchronous FIFO
↓
Separate read/write clocks
↓
Clock-domain crossing
↓
Gray-coded pointers
↓
Synchronizers
↓
More complex full/empty logic
A synchronous FIFO is generally the better choice when the producer and consumer share a clock. An asynchronous FIFO is appropriate when data must safely move between independent clock domains.
The important point is that an asynchronous FIFO is not simply a synchronous FIFO with another clock added. Its pointer synchronization, Gray-code representation, reset behavior, and full/empty detection must all be designed specifically for CDC operation.
For RTL engineers, understanding this distinction provides a strong foundation for more advanced topics such as Clock Domain Crossing, metastability, Gray-code counters, dual-port memory, and CDC verification.