In short: A FIFO in an FPGA is a buffer whose read and write ports advance independent pointers; a synchronous FIFO shares one clock and uses simple binary counters, while an asynchronous FIFO crosses two clock domains safely by converting its pointers to Gray code before synchronizing them, and both typically map onto on-chip Block RAM for depth and area efficiency.
First-in, first-out (FIFO) buffers are one of the most heavily used primitives in digital design. On an FPGA they decouple a producer from a consumer: a video pipeline stuffing pixels at 148.5 MHz into a buffer that a 100 MHz AXI-Stream engine drains, or a UART receiver absorbing bursts while software reads at its leisure. Interviewers love FIFOs precisely because a correct one forces you to understand pointers, flag generation, metastability, and memory inference all at once. Let us build the mental model properly.
What a FIFO really is: two pointers over a memory
Strip away the vendor IP and a FIFO is a circular buffer of depth N with a write pointer (wptr) and a read pointer (rptr). You push data at wptr and increment it; you pop from rptr and increment that. When a pointer passes the top address it wraps to zero, so the storage behaves like a ring. The entire difficulty of FIFO design is answering two questions reliably: is it full? (do not overwrite unread data) and is it empty? (do not read stale garbage).
The synchronous FIFO: one clock, binary counters
When producer and consumer share the same clock, life is easy. Use pointers that are one bit wider than needed to address the memory. For a depth-16 FIFO you address with 4 bits but keep 5-bit pointers. The extra MSB is a wrap bit. The FIFO is empty when the two pointers are identical (same address, same wrap). It is full when the addresses match but the wrap bits differ, meaning the write pointer has lapped the read pointer exactly once.
Why the extra bit? With N-bit addresses alone, full and empty both look like wptr == rptr and are indistinguishable. The N+1-bit trick lets a single comparison separate the two states without a separate counter.
Occupancy is simply wptr - rptr (modulo 2N), which you can expose as a programmable almost-full/almost-empty threshold so the upstream block can throttle a few cycles early rather than slamming into a hard full flag.
The asynchronous FIFO: two clocks and the metastability problem
The interesting case is when write and read run on unrelated clocks, for example a 125 MHz Ethernet MAC writing and a 156.25 MHz packet engine reading. Now each side must inspect the other side's pointer to compute its flag, and sampling a multi-bit value that is changing in a foreign clock domain is a recipe for disaster. If an ordinary binary counter steps from 0111 to 1000, all four bits toggle at once; sample mid-transition and you can latch 1111 or 0000 — a value the counter never held. This is clock-domain-crossing failure, and it corrupts the flag logic silently. See our deeper treatment in clock domain crossing in FPGA.
Gray-code pointers: the key idea
The fix is to carry the pointers across the clock boundary in Gray code, where successive values differ by exactly one bit. If only one bit ever changes per increment, then even if the two-flop synchronizer samples during the transition it captures either the old or the new value — never an illegal in-between. You convert binary to Gray with gray = bin ^ (bin >> 1), synchronize the Gray pointer through two flip-flops in the destination domain, and compare in Gray.
Empty detection sits in the read domain: the FIFO is empty when the read Gray pointer equals the synchronized write Gray pointer. Full detection sits in the write domain and uses the classic test: the next write Gray pointer equals the synchronized read pointer with its top two bits inverted. That inversion is the Gray-code equivalent of the binary wrap-bit test. This structure is the one popularized by Clifford Cummings' widely cited SNUG paper, and essentially every vendor async FIFO follows it.
Flags are intentionally pessimistic: because each side sees a delayed copy of the other pointer, full may assert a cycle or two early and empty may clear a cycle or two late. That is safe — you never overflow or underflow — you only ever under-utilize depth by a word or two.
BRAM-based storage: using the silicon you paid for
Where does the data live? Flip-flops are expensive; a 512-deep by 72-bit FIFO in registers would burn ~36,000 FFs. Instead you map storage onto dedicated Block RAM. A Xilinx/AMD 7-series or UltraScale device has 36 Kb BRAM primitives (configurable as two independent 18 Kb halves); Intel/Altera devices offer M20K blocks. These are true dual-port memories — one write port, one read port, each with its own clock — which is exactly what an async FIFO needs. A single 36 Kb BRAM gives you 512×72 or 1024×36 in one block.
| Aspect | Synchronous FIFO | Asynchronous FIFO |
|---|---|---|
| Clocks | One shared | Two independent |
| Pointers | Binary counters | Gray-coded + synchronized |
| Flag latency | Exact, 0 extra cycles | Pessimistic, +2 sync cycles |
| Typical use | Pipeline rate match, width convert | CDC between unrelated clocks |
| Storage | BRAM or distributed RAM | True dual-port BRAM |
For shallow FIFOs (say depth ≤ 32) the tools will instead infer distributed RAM from LUTs, which is lower latency and avoids wasting a whole BRAM. Control this with synthesis attributes such as ram_style = "block" or "distributed". In practice most engineers reach for the vendor generator — Xilinx FIFO Generator / Vivado xpm_fifo_async, or Intel's DCFIFO — and verify it, rather than hand-coding; design-services teams at firms like Avecas frequently wrap these primitives with custom almost-full logic and ECC for production SoCs.
Common pitfalls that cost interview points
- Synchronizing binary pointers. The single most common mistake. Always convert to Gray first.
- Depth must be a power of two for the single-MSB wrap trick to work. Non-power-of-two FIFOs need an explicit counter.
- Read-during-empty or write-during-full. Gate the pointer increments with the flags, not just the enables.
- Forgetting the two-flop synchronizer timing exception. You must set a false path or max-delay constraint on the pointer crossing in your SDC, or static timing analysis (in tools like Synopsys PrimeTime, Cadence Tempus, or Vivado's own STA) will try — and fail — to time the asynchronous path.
- First-word-fall-through vs. standard mode. FWFT presents the first word on the output before
rd_en, which simplifies downstream logic but changes your empty semantics.
Related reading: our write-up on Block RAM vs distributed RAM explains the memory-mapping tradeoffs in more depth, and you can see the full flow applied in our FPGA design courses.
Ready to code a bug-free async FIFO and prove it with a self-checking testbench? Our CourseTron FPGA & RTL Design track walks you from pointer math to Gray-code CDC and BRAM inference, with lab exercises on real Xilinx and Intel parts. Explore the courses →
Frequently asked questions
Why Gray code instead of just a slower clock?
You usually cannot change the clocks — they are set by external interfaces. Gray code lets you cross any clock ratio safely because only one bit ever changes per increment, so the two-flop synchronizer can never latch an illegal intermediate value.
Does the FIFO depth have to be a power of two?
For the elegant single-extra-bit full/empty scheme, yes. Non-power-of-two depths require an explicit occupancy counter and comparison against the depth constant, which costs a bit more logic and loses the clean Gray-wrap test.
When should I use distributed RAM instead of BRAM?
For shallow, narrow FIFOs (roughly depth 16–64) distributed LUT RAM gives lower read latency and avoids consuming a whole 18/36 Kb BRAM. For deep or wide buffers, BRAM is far more area-efficient.
Why does the full flag assert early?
Each domain sees a synchronizer-delayed copy of the other pointer, so the write side's view of the read pointer lags reality. The flag is deliberately conservative — it protects against overflow at the cost of a word or two of unused depth.
References & Further Reading
- Clifford E. Cummings, "Simulation and Synthesis Techniques for Asynchronous FIFO Design," SNUG 2002
- Gray code — Wikipedia
- AMD/Xilinx Documentation Portal (FIFO Generator, XPM FIFO, UltraScale Memory Resources)
- Intel FPGA Embedded Memory User Guide (M20K, DCFIFO)
- Metastability in electronics — Wikipedia