Loading header...

Exercise: Design an Asynchronous FIFO

An asynchronous FIFO is one of the most important CDC structures in FPGA design. This exercise focuses on the architecture and verification mindset. In production, prefer a proven vendor FIFO or a heavily reviewed reusable implementation.

Learning Objectives

You will:

  • understand write and read clock domains;
  • use pointers, gray code, full, and empty flags;
  • verify FIFO ordering under different clock rates;
  • identify common CDC failure symptoms.

Prerequisites

  • Clock-domain crossing basics.
  • Block RAM/FIFO concepts.
  • Verilog testbenches.
  • Ability to read timing and CDC warnings.

Task

Design a small asynchronous FIFO with:

  • wr_clk, wr_en, wr_data, full;
  • rd_clk, rd_en, rd_data, empty;
  • depth of 16 entries;
  • data width of 8 bits.

Architecture

flowchart LR subgraph W["Write clock domain"] A["wr_en + wr_data"] --> B["write RAM"] C["write pointer binary"] --> D["write pointer gray"] D --> E["sync into read domain"] F["synced read pointer"] --> G["full logic"] end subgraph R["Read clock domain"] H["rd_en"] --> I["read RAM"] J["read pointer binary"] --> K["read pointer gray"] K --> L["sync into write domain"] M["synced write pointer"] --> N["empty logic"] end

The actual RAM is shared. The control logic must respect which clock domain owns each pointer.

Buildable Simplified Skeleton

This skeleton shows structure, not a drop-in production FIFO.

module async_fifo #(
    parameter DW = 8,
    parameter AW = 4
) (
    input  wire          wr_clk,
    input  wire          wr_rst,
    input  wire          wr_en,
    input  wire [DW-1:0] wr_data,
    output wire          full,

    input  wire          rd_clk,
    input  wire          rd_rst,
    input  wire          rd_en,
    output reg  [DW-1:0] rd_data,
    output wire          empty
);
    localparam DEPTH = (1 << AW);

    reg [DW-1:0] mem [0:DEPTH-1];

    reg [AW:0] wr_bin, wr_gray;
    reg [AW:0] rd_bin, rd_gray;
    reg [AW:0] rd_gray_w1, rd_gray_w2;
    reg [AW:0] wr_gray_r1, wr_gray_r2;

    function [AW:0] bin2gray(input [AW:0] b);
        bin2gray = (b >> 1) ^ b;
    endfunction

    wire [AW:0] wr_bin_next  = wr_bin + (wr_en && !full);
    wire [AW:0] wr_gray_next = bin2gray(wr_bin_next);
    wire [AW:0] rd_bin_next  = rd_bin + (rd_en && !empty);
    wire [AW:0] rd_gray_next = bin2gray(rd_bin_next);

    assign empty = (rd_gray == wr_gray_r2);
    assign full  = (wr_gray_next == {~rd_gray_w2[AW:AW-1], rd_gray_w2[AW-2:0]});

    always @(posedge wr_clk) begin
        if (wr_rst) begin
            wr_bin <= 0;
            wr_gray <= 0;
        end else begin
            if (wr_en && !full)
                mem[wr_bin[AW-1:0]] <= wr_data;
            wr_bin <= wr_bin_next;
            wr_gray <= wr_gray_next;
        end
    end

    always @(posedge rd_clk) begin
        if (rd_rst) begin
            rd_bin <= 0;
            rd_gray <= 0;
            rd_data <= 0;
        end else begin
            if (rd_en && !empty)
                rd_data <= mem[rd_bin[AW-1:0]];
            rd_bin <= rd_bin_next;
            rd_gray <= rd_gray_next;
        end
    end

    always @(posedge wr_clk) begin
        rd_gray_w1 <= rd_gray;
        rd_gray_w2 <= rd_gray_w1;
    end

    always @(posedge rd_clk) begin
        wr_gray_r1 <= wr_gray;
        wr_gray_r2 <= wr_gray_r1;
    end
endmodule

Expected Behavior

  • Data read order must match write order.
  • empty must be high after reset.
  • Writes must stop when full is high.
  • Reads must stop when empty is high.
  • The FIFO must tolerate unrelated write and read clocks.

Verification Steps

  1. Use different clock periods, for example wr_clk = 10 ns, rd_clk = 14 ns.
  2. Reset both domains.
  3. Write a known sequence such as 00, 01, 02, ....
  4. Read whenever not empty.
  5. Compare every read byte with the expected sequence.
  6. Repeat with faster read than write and faster write than read.
  7. Add pauses to both sides.

Common Failure Symptoms

Symptom Likely cause
duplicated data read pointer update bug
missing data write pointer or full logic bug
random order unsafe pointer crossing
false full/empty gray-code comparison error
works only when clocks match not actually asynchronous-safe

Debugging Guidance

  • Display binary and gray pointers.
  • Check full/empty calculations in their own domains.
  • Confirm only gray-coded pointers cross domains.
  • Add assertions: never read when empty, never write when full.
  • Test with clocks whose edges drift relative to each other.

Extension Challenge

Add almost_full and almost_empty flags. Then write a testbench that proves the flags assert at the intended occupancy thresholds.

Explained Solution

The FIFO separates data storage from clock-domain control. The write domain owns the write pointer; the read domain owns the read pointer. Pointers cross domains in gray code so only one bit changes per increment, reducing ambiguity during synchronization. Full and empty are calculated using synchronized opposite-domain pointers. The design trades flag latency for safe CDC behavior.

Summary

Asynchronous FIFOs are powerful because they turn unsafe clock crossings into a reviewed structure. They are also easy to get subtly wrong. Use proven implementations in real products, but understand the architecture well enough to review and verify them.

Next: Verification and Design Review.

Further Reading

  • Clifford Cummings asynchronous FIFO papers
  • Vendor FIFO IP documentation
  • CDC verification guides

Mind Map

mindmap root((Async FIFO Exercise)) Core concept Write clock domain Read clock domain Gray pointers Full empty flags Applications Applications CDC buffering Rate matching Design checks Checks Overflow underflow Pointer sync Common mistakes Common mistakes Binary pointer crossing Flag off by one