Parameterized single-clock and dual-clock (clock-domain-crossing) FIFOs in Verilog.
Author: Avinash Kollu · GitHub: @avinashkollu-git
Two production-style FIFO designs, each with a self-checking testbench and a reproducible open-source simulation flow:
rtl/sync_fifo.v: a single-clock FIFO. ParameterizedDATA_WIDTHand a power-of-twoDEPTH. Read and write pointers carry an extra wrap (MSB) bit sofullandemptyare never ambiguous even when the read and write pointers land on the same address, which lets the buffer hold allDEPTHentries. The read data path is registered.rtl/async_fifo.v: a dual-clock FIFO for safe clock-domain crossing (CDC). Independentwr_clkandrd_clkdomains, gray-coded pointers passed between them, two-flop synchronizers on every crossing, and registeredfull/emptyflags. Follows the classic Cummings SNUG-2002 formulation.
When a design has two clocks that are asynchronous to each other, you cannot simply hand a multi-bit value from one domain to the other: the sampling clock can catch the bus mid-transition and latch a mix of old and new bits, i.e. garbage. A dual-clock FIFO is the standard way to move a data stream across that boundary safely, decoupling a fast producer from a slow consumer (or vice versa). The hard part is not the RAM but crossing the read/write pointers between domains without corruption, which is exactly what gray coding plus two-flop synchronization solve.
| Feature | Sync FIFO | Async FIFO |
|---|---|---|
Parameterized DATA_WIDTH / DEPTH |
Yes | Yes |
Power-of-two depth, holds all DEPTH entries |
Yes | Yes |
| Extra wrap-bit pointers (unambiguous full/empty) | Yes | Yes |
| Registered read data | Yes | Yes |
| Independent write / read clocks | No | Yes |
| Gray-coded pointer crossing | No | Yes |
| Two-flop synchronizers | No | Yes |
| Registered flags (no combinational pointer loop) | No | Yes |
| Self-checking testbench | Yes | Yes |
ASYNC FIFO (dual-clock CDC)
WRITE DOMAIN (wr_clk) READ DOMAIN (rd_clk)
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ wr_ptr (binary) │ │ rd_ptr (binary) │
│ │ │ │ │ │
│ ▼ bin→gray │ │ bin→gray ▼ │
│ wr_ptr_gray ───────────────┼──gray──┐ ┌─┼─────────────── rd_ptr_gray │
│ │ │ │ │ │ │ │
│ │ ┌─────────────┼──┐ ┌─┼───┘ │ │ │
│ │ │ 2FF sync │◄─┼───┘ │ 2FF │ │ │
│ │ │ (rd→wr) │ │ │ sync│ │ │
│ ▼ └─────┬───────┘ │ │(wr→rd) ▼ │
│ compare ◄─────────┘ │ └──┬──┘ ──────────► compare │
│ → FULL (registered) │ │ → EMPTY (registered) │
└───────────────┬─────────────────┘ └────────────────┬──────────────┘
│ write addr / data │ read addr
▼ ▼
┌──────────────────────────────────────────────────────┐
│ DUAL-PORT MEMORY (write port | read port) │
│ mem[DEPTH-1:0] , width = DATA_WIDTH │
└──────────────────────────────────────────────────────┘
wr_ptr_gray crosses wr_clk → rd_clk (compared against rd_ptr to form EMPTY)
rd_ptr_gray crosses rd_clk → wr_clk (compared against wr_ptr to form FULL)
async-fifo/
├── rtl/
│ ├── sync_fifo.v # single-clock FIFO (parameterized, wrap-bit ptrs)
│ └── async_fifo.v # dual-clock CDC FIFO (gray ptrs, 2FF sync)
├── tb/
│ ├── tb_sync_fifo.v # fills to full, drains, checks order + flags
│ └── tb_async_fifo.v # fast writer / slow reader, 32 items cross in order
├── docs/
│ └── async_fifo_wave.svg # committed reference waveform
├── tools/
│ └── vcd2svg.py # renders a VCD dump to the SVG waveform
├── Makefile
└── LICENSE # MIT
Requires Icarus Verilog (iverilog/vvp) and, optionally, GTKWave for waveform viewing. A fully open-source flow.
| Command | Description |
|---|---|
make test |
Run both testbenches |
make test-sync |
Run the synchronous FIFO testbench |
make test-async |
Run the asynchronous FIFO testbench |
make wave |
Regenerate docs/async_fifo_wave.svg from the async VCD dump |
Verified simulation results (Icarus Verilog):
tb_sync_fifo : RESULT: ALL TESTS PASSED
tb_async_fifo : RESULT: ALL 32 ITEMS CROSSED CORRECTLY - PASSED
| Testbench | What it checks | Result |
|---|---|---|
tb/tb_sync_fifo.v |
Fills to full, drains, verifies FIFO order and full/empty flags |
PASS |
tb/tb_async_fifo.v |
Fast writer (~167 MHz) vs slow reader (~63 MHz), 32 items cross in order | PASS |
The fast writer fills the FIFO while the slower reader drains it; every data word crosses the clock boundary in order, and the full/empty flags assert and deassert correctly throughout.
- Gray-coded pointers. Each pointer is incremented in binary, then converted to gray code before it crosses into the other clock domain. A multi-bit binary pointer sampled on an asynchronous edge can be caught mid-increment (e.g.
0111 → 1000, where several bits toggle at once) and latched as a wild intermediate value. Gray code changes exactly one bit per increment, so a metastable sample can only resolve to either the old value or the new one, never a bogus third value. That property is what makes the cross-domain pointer comparison trustworthy. - Two-flop synchronizers. Every gray pointer passes through two back-to-back flip-flops in the destination domain. The first flop may go metastable when it samples a signal that changed too close to its clock edge; the second flop gives that metastability a full clock period to settle before the value is used, driving the probability of a metastable value propagating into logic to a negligible level.
- Registered flags break the combinational loop.
fullandemptyare registered rather than combinational. Computing a flag straight frompointer → flag → pointerwould create a combinational path that folds back on the pointers; registering the flags cuts that loop and keeps the comparison timing clean. - Wrap-bit full/empty scheme. Both FIFOs use pointers one bit wider than the address.
emptyis when read and write pointers are fully equal;fullis when the lower address bits match but the wrap (MSB) bit differs. This disambiguates the "pointers point at the same address" case and lets the buffer store allDEPTHentries instead ofDEPTH-1. - Reference. Clifford E. Cummings, "Simulation and Synthesis Techniques for Asynchronous FIFO Design," SNUG San Jose 2002, the canonical treatment this async FIFO follows.
- Clock-domain crossing (CDC) design and analysis
- Metastability mitigation with two-flop synchronizers
- Gray-code encoding and single-bit-transition reasoning
- Pointer arithmetic with wrap-bit full/empty detection
- Multi-clock, self-checking verification (fast/slow clock ratios)
- Parameterized, reusable RTL IP
Released under the MIT License. Copyright (c) 2026 Avinash Kollu.