Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SYNCHRONOUS & ASYNCHRONOUS FIFO

Parameterized single-clock and dual-clock (clock-domain-crossing) FIFOs in Verilog.

Author: Avinash Kollu · GitHub: @avinashkollu-git


Overview

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. Parameterized DATA_WIDTH and a power-of-two DEPTH. Read and write pointers carry an extra wrap (MSB) bit so full and empty are never ambiguous even when the read and write pointers land on the same address, which lets the buffer hold all DEPTH entries. The read data path is registered.
  • rtl/async_fifo.v: a dual-clock FIFO for safe clock-domain crossing (CDC). Independent wr_clk and rd_clk domains, gray-coded pointers passed between them, two-flop synchronizers on every crossing, and registered full/empty flags. Follows the classic Cummings SNUG-2002 formulation.

Why an Async FIFO

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.


Features

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

Block Diagram

                          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)

Repository Layout

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

Simulation & Results

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

Waveform

Async FIFO waveform

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.


Design Notes

  • 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. full and empty are registered rather than combinational. Computing a flag straight from pointer → flag → pointer would 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. empty is when read and write pointers are fully equal; full is 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 all DEPTH entries instead of DEPTH-1.
  • Reference. Clifford E. Cummings, "Simulation and Synthesis Techniques for Asynchronous FIFO Design," SNUG San Jose 2002, the canonical treatment this async FIFO follows.

Skills Demonstrated

  • 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

License

Released under the MIT License. Copyright (c) 2026 Avinash Kollu.

About

Synchronous + asynchronous (CDC) FIFO in Verilog: gray-code pointers, two-flop synchronizers, registered flags.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages