Exercise: Design an Asynchronous FIFO
An asynchronous FIFO is a standard way to move streaming data between unrelated clock domains. It is also one of the easiest FPGA structures to get almost right and still unsafe. This exercise builds the smallest useful version so you can understand the architecture, write meaningful tests, and review vendor FIFO IP with confidence. For production hardware, prefer a proven FIFO generator or a reusable implementation that has been reviewed, constrained, and verified.
Learning Objectives
You will be able to:
- separate write-domain and read-domain responsibilities;
- explain why binary counters should not cross directly between unrelated clocks;
- use gray-coded pointers with two-flop synchronizers;
- calculate
fullandemptyfrom synchronized opposite-domain pointers; - verify ordering, overflow protection, underflow protection, and different clock rates.
Prerequisites
- Clock-domain crossing basics and metastability risk.
- Synchronous RAM or block RAM concepts.
- Verilog testbench skills.
- Ability to inspect simulation waveforms and CDC warnings.
Task
Design a small asynchronous FIFO with these properties:
wr_clk,wr_rst,wr_en,wr_data, andfullin the write domain;rd_clk,rd_rst,rd_en,rd_data, andemptyin the read domain;- depth of
16entries; - data width of
8bits; - no writes accepted when
fullis high; - no reads accepted when
emptyis high; - data read order exactly matches accepted write order.
Architecture
The RAM stores data. The pointers and flags decide whether a transfer is allowed. Each pointer is owned by one clock domain, converted to gray code, and synchronized into the opposite domain for flag calculation.
Why Gray Code Is Used
A binary pointer may change several bits at once. If those bits are sampled during a transition by another clock domain, the sampled value can look like an unrelated pointer. A gray-coded pointer changes only one bit per increment, so the synchronized value is either the old pointer or the new pointer. The two-flop synchronizer reduces metastability risk, while the FIFO tolerates the resulting flag latency.
For an address width AW, the pointer has AW + 1 bits. The extra bit distinguishes empty from full after the address wraps.
Buildable RTL Skeleton
This version is suitable for learning and simulation. Confirm RAM inference style and CDC constraints for your target device before using it in hardware.
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;
begin
bin2gray = (b >> 1) ^ b;
end
endfunction
wire wr_take = wr_en && !full;
wire rd_take = rd_en && !empty;
wire [AW:0] wr_bin_next = wr_bin + wr_take;
wire [AW:0] rd_bin_next = rd_bin + rd_take;
wire [AW:0] wr_gray_next = bin2gray(wr_bin_next);
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_take)
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_take)
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
if (wr_rst) begin
rd_gray_w1 <= 0;
rd_gray_w2 <= 0;
end else begin
rd_gray_w1 <= rd_gray;
rd_gray_w2 <= rd_gray_w1;
end
end
always @(posedge rd_clk) begin
if (rd_rst) begin
wr_gray_r1 <= 0;
wr_gray_r2 <= 0;
end else begin
wr_gray_r1 <= wr_gray;
wr_gray_r2 <= wr_gray_r1;
end
end
endmodule
Testbench Plan
Use unrelated clock periods so the edges drift.
always #5 wr_clk = ~wr_clk; // 100 MHz equivalent
always #7 rd_clk = ~rd_clk; // about 71.4 MHz equivalent
Drive a monotonically increasing byte sequence into the write side whenever !full. On the read side, pop data whenever !empty and compare it with the next expected byte. Repeat with:
- write faster than read;
- read faster than write;
- random write pauses;
- random read pauses;
- reset asserted at startup only, then clean continuous operation.
Expected Behavior
emptyis high after both resets are released and synchronization latency has settled.- Accepted writes appear at the read side in the exact same order.
- Write attempts while
fullis high do not corrupt stored data. - Read attempts while
emptyis high do not advance the expected sequence. fullandemptymay be conservative by a few cycles because opposite-domain pointers are synchronized.
title "Illustrative async FIFO clocks"
time start=0 end=14 unit=cycles divisions=14
WRCLK: square label="wr clk" low=0 high=1 duty=50 cycles=7 unit=logic color=#2563eb
RDCLK: square label="rd clk" low=0 high=1 duty=50 cycles=5 unit=logic color=#16a34a
WREN: pulse label="write burst" low=0 high=1 at=2 width=4 unit=logic color=#dc2626
RDEN: pulse label="read burst" low=0 high=1 at=6 width=5 unit=logic color=#7c3aed
marker FIRST at=2 label="writes"
marker LATER at=6 label="reads"
The waveform is illustrative. Real clock edges are independent, not phase-locked to this drawing.
Verification Steps
- Confirm
emptyis true after reset and before writes. - Write at least
DEPTH * 3accepted values so pointers wrap several times. - Read back all accepted values and compare order.
- Force the FIFO toward full and prove writes stop without overwriting unread data.
- Force the FIFO toward empty and prove reads stop without duplicating data.
- Test faster-write and faster-read cases.
- Add assertions: never write when full in the driver, never read when empty in the driver, and every popped value matches the queue.
- Review CDC reports and ensure only gray-coded pointer buses cross domains.
Common Failure Symptoms
| Symptom | Likely cause | Debugging move |
|---|---|---|
| Duplicated data | Read pointer failed to advance or empty flag stale in the wrong direction | Trace rd_take and rd_bin |
| Missing data | Write pointer or full calculation is wrong | Compare accepted writes against memory addresses |
| Random order | Binary pointer crossed clocks or RAM read style mismatch | Inspect CDC paths and RAM behavior |
| False full forever | Full comparison bit inversion is wrong | Check the two MSBs of synchronized read pointer |
| Works only with same clocks | Testbench did not create a real CDC stress case | Use unrelated periods and pauses |
Debugging Guidance
- Display binary and gray pointers side by side.
- Keep full logic entirely in the write clock domain.
- Keep empty logic entirely in the read clock domain.
- Synchronize pointer buses through exactly staged registers marked appropriately for your FPGA flow.
- Do not synchronize data bits individually; the RAM plus pointer protocol controls data movement.
- Review reset release carefully. Many production FIFOs require reset sequencing rules.
Extension Challenge
Add almost_full and almost_empty flags. Define thresholds in entries, such as almost_full at 12 of 16 entries and almost_empty at 4 or fewer. Then extend the testbench queue to check that the flags assert conservatively and never permit overflow or underflow.
Explained Solution
The write side owns the write pointer and updates RAM only on accepted writes. The read side owns the read pointer and reads RAM only on accepted reads. Each domain sends a gray-coded version of its pointer through two synchronizer flops to the other domain. empty is true when the read pointer equals the synchronized write pointer. full is true when the next write pointer equals the synchronized read pointer with the wrap bits inverted. This structure accepts small flag latency because conservative full and empty flags are safer than unsafe direct pointer crossing.
Summary
An asynchronous FIFO is a CDC protocol, not just a memory array. Keep ownership clear, cross only gray-coded pointers, calculate flags in the correct domains, and verify with unrelated clocks and a self-checking queue. In shipped systems, use a proven implementation unless you have strong verification evidence.
Next: Verification and Design Review.
Further Reading
- Clifford E. Cummings, "Simulation and Synthesis Techniques for Asynchronous FIFO Design"
- AMD and Intel FIFO IP user guides
- Verilator and cocotb examples for queue-based scoreboards
- Vendor CDC methodology guides