Synchronous FIFO Design in Verilog - Complete Guide
A FIFO (First-In-First-Out) buffer is essential in digital design for clock domain crossing, rate matching, and data buffering. This guide covers synchronous FIFO design with parameterized depth and width.
FIFO Architecture
Key Components
- Memory array: Stores the data
- Write pointer: Points to next write location
- Read pointer: Points to next read location
- Full/Empty flags: Prevent overflow/underflow
Full/Empty Detection (Extra-Bit Method)
The classic challenge: both full and empty occur when rd_ptr == wr_ptr. Solution: add one extra MSB to pointers.
| Condition | Detection |
|---|---|
| Empty | rd_ptr == wr_ptr (entire pointer matches) |
| Full | rd_ptr[ADDR-1:0] == wr_ptr[ADDR-1:0] AND rd_ptr[ADDR] != wr_ptr[ADDR] |
The MSB acts as a "wrap counter" - when pointers have different MSBs but same address bits, the write pointer has wrapped around and caught up to the read pointer.
RTL Implementation
//////////////////////////////////////////////////////////////
// Synchronous FIFO with Parameterized Depth and Width
// Uses extra-bit method for full/empty detection
//////////////////////////////////////////////////////////////
module sync_fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16,
parameter ADDR_WIDTH = $clog2(DEPTH) // Auto-calculate
)(
input wire clk,
input wire rst_n,
// Write interface
input wire wr_en,
input wire [DATA_WIDTH-1:0] wr_data,
output wire full,
// Read interface
input wire rd_en,
output reg [DATA_WIDTH-1:0] rd_data,
output wire empty
);
// Pointers with extra MSB for full/empty detection
reg [ADDR_WIDTH:0] wr_ptr, rd_ptr;
// Memory array
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
// Full and Empty flags
assign empty = (wr_ptr == rd_ptr);
assign full = (wr_ptr[ADDR_WIDTH-1:0] == rd_ptr[ADDR_WIDTH-1:0]) &&
(wr_ptr[ADDR_WIDTH] != rd_ptr[ADDR_WIDTH]);
// Write logic
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 0;
end else if (wr_en && !full) begin
mem[wr_ptr[ADDR_WIDTH-1:0]] <= wr_data;
wr_ptr <= wr_ptr + 1;
end
end
// Read logic
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_ptr <= 0;
rd_data <= 0;
end else if (rd_en && !empty) begin
rd_data <= mem[rd_ptr[ADDR_WIDTH-1:0]];
rd_ptr <= rd_ptr + 1;
end
end
endmodule
Testbench
module tb_sync_fifo;
parameter DATA_WIDTH = 8;
parameter DEPTH = 4;
reg clk, rst_n;
reg wr_en, rd_en;
reg [DATA_WIDTH-1:0] wr_data;
wire [DATA_WIDTH-1:0] rd_data;
wire full, empty;
// Instantiate FIFO
sync_fifo #(
.DATA_WIDTH(DATA_WIDTH),
.DEPTH(DEPTH)
) dut (
.clk (clk),
.rst_n (rst_n),
.wr_en (wr_en),
.wr_data (wr_data),
.full (full),
.rd_en (rd_en),
.rd_data (rd_data),
.empty (empty)
);
// Clock generation
initial clk = 0;
always #5 clk = ~clk;
// Test sequence
initial begin
$dumpfile("fifo.vcd");
$dumpvars(0, tb_sync_fifo);
// Reset
rst_n = 0; wr_en = 0; rd_en = 0; wr_data = 0;
#15 rst_n = 1;
// Test 1: Check empty flag
#10;
assert (empty == 1) else $error("Should be empty");
assert (full == 0) else $error("Should not be full");
// Test 2: Write until full
@(posedge clk);
for (int i = 0; i < DEPTH; i++) begin
wr_en = 1; wr_data = i + 8'hA0;
@(posedge clk);
end
wr_en = 0;
#10;
assert (full == 1) else $error("Should be full");
assert (empty == 0) else $error("Should not be empty");
// Test 3: Read all data and verify
for (int i = 0; i < DEPTH; i++) begin
rd_en = 1;
@(posedge clk);
#1;
assert (rd_data == i + 8'hA0)
else $error("Data mismatch: exp=%h got=%h", i + 8'hA0, rd_data);
end
rd_en = 0;
#10;
assert (empty == 1) else $error("Should be empty after reads");
// Test 4: Simultaneous read/write
@(posedge clk);
wr_en = 1; wr_data = 8'hFF;
@(posedge clk);
wr_en = 1; rd_en = 1; wr_data = 8'hEE;
@(posedge clk);
wr_en = 0; rd_en = 0;
#20;
$display("All tests passed!");
$finish;
end
endmodule
Design Variations
First-Word Fall-Through (FWFT)
Data appears on rd_data as soon as it's written (no read cycle needed for first word):
// FWFT read - combinational output
assign rd_data = mem[rd_ptr[ADDR_WIDTH-1:0]];
Almost Full/Empty Flags
parameter ALMOST_FULL_THRESH = DEPTH - 2;
parameter ALMOST_EMPTY_THRESH = 2;
wire [ADDR_WIDTH:0] fifo_count = wr_ptr - rd_ptr;
assign almost_full = (fifo_count >= ALMOST_FULL_THRESH);
assign almost_empty = (fifo_count <= ALMOST_EMPTY_THRESH);
With Fill Level Output
output wire [ADDR_WIDTH:0] fill_level;
assign fill_level = wr_ptr - rd_ptr;
Common Gotchas
Pointer Width: Don't forget the extra MSB! A DEPTH=16 FIFO needs 5-bit pointers (4 for address + 1 for wrap detection).
Power-of-2 Depth: This design assumes DEPTH is a power of 2. For non-power-of-2 depths, you need modulo arithmetic or different full/empty logic.
Read During Empty: Always gate reads with
!empty. Reading from an empty FIFO returns stale data.
Timing Diagram
clk _|^|_|^|_|^|_|^|_|^|_|^|_|^|_|^|_
wr_en ____|^^^^^^^^^^^|________________
wr_data ____| A | B | C | D |____________
full ________________________|^^^^^^^^
rd_en __________________________|^^^^^^
rd_data ___________________________| A | B
empty ^^^^|__________________________|^^
[write 4] [full] [read 2]
Synthesis Notes
- Memory inference: Most tools infer block RAM for deeper FIFOs (>64 entries)
- Timing: Full/empty are combinational - register them if they're on critical paths
- Reset: Only pointers need reset, not memory contents (saves reset routing)
Related Topics
- Asynchronous FIFO (CDC) - different clock domains
- Dual-Port RAM Design
- Single-Port RAM Design
Comments (0)
Leave a Comment