Synchronous FIFO Verilog Code with Testbench
A FIFO (first-in, first-out buffer) is the glue that lets two parts of a chip exchange data without either side dropping a word. This tutorial hands you complete, synthesizable synchronous FIFO Verilog code — parameterized for any depth and width, with correct full and empty flags — plus three runnable testbenches so you can watch it behave in simulation before you commit it to silicon.
“Synchronous” means one clock drives both the write side and the read side. That single-clock assumption keeps the logic simple: no clock-domain-crossing, no Gray-coded pointers, just two binary counters and a small block of memory. Every file below compiles and runs with Icarus Verilog using iverilog -g2005.
The complete FIFO module
The module stores DEPTH words of DATA_WIDTH bits each. The one idea worth memorizing is the extra pointer bit: each pointer is one bit wider than it needs to be to address the memory, and that extra bit is what lets the design tell “completely full” apart from “completely empty”.
// sync_fifo.v -- parameterized single-clock (synchronous) FIFO
// DATA_WIDTH : bit width of each word
// DEPTH : number of words (use a power of 2)
module sync_fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 8
)(
input wire clk,
input wire rst_n, // active-low synchronous-ish reset
input wire wr_en,
input wire rd_en,
input wire [DATA_WIDTH-1:0] din,
output reg [DATA_WIDTH-1:0] dout,
output wire full,
output wire empty
);
// ADDR_WIDTH indexes DEPTH locations; pointers carry one EXTRA MSB
// (the "wrap" bit) so we can tell a full FIFO from an empty one.
localparam ADDR_WIDTH = $clog2(DEPTH);
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [ADDR_WIDTH:0] wr_ptr; // {wrap, addr}
reg [ADDR_WIDTH:0] rd_ptr; // {wrap, addr}
// A write/read only happens when it is legal (guards overflow/underflow).
wire do_write = wr_en & ~full;
wire do_read = rd_en & ~empty;
// Write port
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (do_write) begin
mem[wr_ptr[ADDR_WIDTH-1:0]] <= din;
wr_ptr <= wr_ptr + 1'b1;
end
end
// Read port (registered: dout is valid one clock after do_read)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rd_ptr <= 0;
else if (do_read) begin
dout <= mem[rd_ptr[ADDR_WIDTH-1:0]];
rd_ptr <= rd_ptr + 1'b1;
end
end
// Empty: pointers identical (same addr AND same wrap bit).
assign empty = (wr_ptr == rd_ptr);
// Full: same addr but wrap bits differ (writer lapped the reader once).
assign full = (wr_ptr[ADDR_WIDTH] != rd_ptr[ADDR_WIDTH]) &&
(wr_ptr[ADDR_WIDTH-1:0] == rd_ptr[ADDR_WIDTH-1:0]);
endmodule
How the pointer logic works
Walk through the module from the top and the design stops feeling like a trick:
- Two pointers, one bit too wide.
wr_ptrandrd_ptrareADDR_WIDTH+1bits. The lowerADDR_WIDTHbits (wr_ptr[ADDR_WIDTH-1:0]) address the RAM; the top bit is a “wrap” or phase bit that flips each time a pointer rolls past the last location. - Writing. On a rising edge, if
do_writeis true, the module storesdinat the write address and adds one towr_ptr. The carry naturally ripples into the wrap bit when the address overflows. - Reading. Likewise, a legal read drives
doutfrom the read address and advancesrd_ptr. Becausedoutis aregassigned in a clocked block, the data lands one clock after you assertrd_en. - Empty.
empty = (wr_ptr == rd_ptr)— identical address and identical wrap bit means every written word has already been read. - Full.
fullis true when the address fields match but the wrap bits differ: the writer has gone exactly one lap ahead of the reader, so allDEPTHslots hold unread data. - The guards.
do_write = wr_en & ~fullanddo_read = rd_en & ~emptyare the safety interlocks. A write request while full, or a read request while empty, is quietly ignored so the pointers never corrupt the data order.
This scheme assumes DEPTH is a power of two so the address wraps cleanly. With the extra bit you get the full depth usable — an 8-deep FIFO really holds 8 words, not 7.
Testbench 1: write, then read it back
The first bench pushes four bytes (0xA0–0xA3) in, stops, then pops them out. The log proves they return in the exact order they arrived — the entire purpose of a FIFO. Compile the design and the bench together:
iverilog -g2005 -o sim sync_fifo.v tb_fifo_rw.v && vvp sim
`timescale 1ns/1ps
// tb_fifo_rw.v -- write four words, then read them back in FIFO order
module tb_fifo_rw;
localparam DW = 8;
localparam DEPTH = 8;
reg clk, rst_n, wr_en, rd_en;
reg [DW-1:0] din;
wire [DW-1:0] dout;
wire full, empty;
sync_fifo #(.DATA_WIDTH(DW), .DEPTH(DEPTH)) dut (
.clk(clk), .rst_n(rst_n), .wr_en(wr_en), .rd_en(rd_en),
.din(din), .dout(dout), .full(full), .empty(empty)
);
initial clk = 1'b0;
always #5 clk = ~clk; // 100 MHz
integer i;
initial begin
rst_n = 0; wr_en = 0; rd_en = 0; din = 0;
@(posedge clk); #1;
rst_n = 1;
$display("[%0t] reset released: empty=%b full=%b", $time, empty, full);
// ---- write phase: push 0xA0,0xA1,0xA2,0xA3 ----
for (i = 0; i < 4; i = i + 1) begin
@(negedge clk);
wr_en = 1; din = 8'hA0 + i[7:0];
@(posedge clk); #1;
$display("[%0t] WRITE 0x%0h empty=%b full=%b", $time, din, empty, full);
end
@(negedge clk); wr_en = 0; din = 0;
// ---- read phase: pop and print in order ----
for (i = 0; i < 4; i = i + 1) begin
@(negedge clk);
rd_en = 1;
@(posedge clk); #1; // dout updated on this edge
$display("[%0t] READ 0x%0h empty=%b full=%b", $time, dout, empty, full);
end
@(negedge clk); rd_en = 0;
@(posedge clk); #1;
$display("[%0t] drained: empty=%b full=%b", $time, empty, full);
$finish;
end
endmodule
[6000] reset released: empty=1 full=0 [16000] WRITE 0xa0 empty=0 full=0 [26000] WRITE 0xa1 empty=0 full=0 [36000] WRITE 0xa2 empty=0 full=0 [46000] WRITE 0xa3 empty=0 full=0 [66000] READ 0xa0 empty=0 full=0 [76000] READ 0xa1 empty=0 full=0 [86000] READ 0xa2 empty=0 full=0 [96000] READ 0xa3 empty=1 full=0 [106000] drained: empty=1 full=0 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp1ooj18qj/tb_fifo_rw.v:47: $finish called at 106000 (1ps)
Testbench 2: full and empty flags
Now the FIFO is only four deep. The bench attempts six writes — the fifth and sixth must be refused once full asserts — then six reads, where the last two are refused once empty asserts. The accepted column is sampled before each clock edge, so it reports what the guard will actually allow.
`timescale 1ns/1ps
// tb_fifo_flags.v -- fill a depth-4 FIFO to full, prove the overflow guard,
// then drain to empty and prove the underflow guard
module tb_fifo_flags;
localparam DW = 8;
localparam DEPTH = 4;
reg clk, rst_n, wr_en, rd_en;
reg [DW-1:0] din;
wire [DW-1:0] dout;
wire full, empty;
sync_fifo #(.DATA_WIDTH(DW), .DEPTH(DEPTH)) dut (
.clk(clk), .rst_n(rst_n), .wr_en(wr_en), .rd_en(rd_en),
.din(din), .dout(dout), .full(full), .empty(empty)
);
initial clk = 1'b0;
always #5 clk = ~clk;
integer i;
reg decision; // what the guard will allow on the next rising edge
initial begin
rst_n = 0; wr_en = 0; rd_en = 0; din = 0;
@(posedge clk); #1; rst_n = 1;
$display("[%0t] start: empty=%b full=%b", $time, empty, full);
// push 6 words into a 4-deep FIFO: last two must be rejected by guard
for (i = 0; i < 6; i = i + 1) begin
@(negedge clk); wr_en = 1; din = 8'h10 + i[7:0];
#1 decision = wr_en & ~full; // sampled before the edge
@(posedge clk); #1;
$display("[%0t] try WRITE 0x%0h -> accepted=%b empty=%b full=%b",
$time, din, decision, empty, full);
end
@(negedge clk); wr_en = 0;
// pull 6 words out: last two reads hit an empty FIFO (guarded)
for (i = 0; i < 6; i = i + 1) begin
@(negedge clk); rd_en = 1;
#1 decision = rd_en & ~empty; // sampled before the edge
@(posedge clk); #1;
if (decision)
$display("[%0t] READ 0x%0h empty=%b full=%b", $time, dout, empty, full);
else
$display("[%0t] READ blocked (empty) empty=%b full=%b", $time, empty, full);
end
@(negedge clk); rd_en = 0;
$display("[%0t] done: empty=%b full=%b", $time, empty, full);
$finish;
end
endmodule
[6000] start: empty=1 full=0 [16000] try WRITE 0x10 -> accepted=1 empty=0 full=0 [26000] try WRITE 0x11 -> accepted=1 empty=0 full=0 [36000] try WRITE 0x12 -> accepted=1 empty=0 full=0 [46000] try WRITE 0x13 -> accepted=1 empty=0 full=1 [56000] try WRITE 0x14 -> accepted=0 empty=0 full=1 [66000] try WRITE 0x15 -> accepted=0 empty=0 full=1 [86000] READ 0x10 empty=0 full=0 [96000] READ 0x11 empty=0 full=0 [106000] READ 0x12 empty=0 full=0 [116000] READ 0x13 empty=1 full=0 [126000] READ blocked (empty) empty=1 full=0 [136000] READ blocked (empty) empty=1 full=0 [140000] done: empty=1 full=0 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpvexcawtj/tb_fifo_flags.v:50: $finish called at 140000 (1ps)
Testbench 3: reading and writing at the same time
Real designs rarely pause writing just to read. This bench primes the buffer with two words, then asserts wr_en and rd_en together for five cycles. One word enters and one leaves every cycle, so occupancy stays flat while data streams straight through in order.
`timescale 1ns/1ps
// tb_fifo_stream.v -- prime the FIFO, then read and write on the SAME cycles
// so data streams through while occupancy stays constant
module tb_fifo_stream;
localparam DW = 8;
localparam DEPTH = 8;
reg clk, rst_n, wr_en, rd_en;
reg [DW-1:0] din;
wire [DW-1:0] dout;
wire full, empty;
sync_fifo #(.DATA_WIDTH(DW), .DEPTH(DEPTH)) dut (
.clk(clk), .rst_n(rst_n), .wr_en(wr_en), .rd_en(rd_en),
.din(din), .dout(dout), .full(full), .empty(empty)
);
initial clk = 1'b0;
always #5 clk = ~clk;
integer i;
initial begin
rst_n = 0; wr_en = 0; rd_en = 0; din = 0;
@(posedge clk); #1; rst_n = 1;
// prime with two words so a read always has something to return
@(negedge clk); wr_en = 1; din = 8'h50;
@(negedge clk); din = 8'h51;
@(posedge clk); #1;
$display("[%0t] primed: empty=%b full=%b", $time, empty, full);
// stream: write 0x52..0x56 while reading on the very same cycles
for (i = 0; i < 5; i = i + 1) begin
@(negedge clk); wr_en = 1; rd_en = 1; din = 8'h52 + i[7:0];
@(posedge clk); #1;
$display("[%0t] in=0x%0h out=0x%0h empty=%b full=%b",
$time, din, dout, empty, full);
end
// stop writing, flush the two words still inside
@(negedge clk); wr_en = 0; rd_en = 1;
repeat (2) begin
@(posedge clk); #1;
$display("[%0t] flush out=0x%0h empty=%b full=%b", $time, dout, empty, full);
end
@(negedge clk); rd_en = 0;
@(posedge clk); #1;
$display("[%0t] done: empty=%b full=%b", $time, empty, full);
$finish;
end
endmodule
[26000] primed: empty=0 full=0 [36000] in=0x52 out=0x50 empty=0 full=0 [46000] in=0x53 out=0x51 empty=0 full=0 [56000] in=0x54 out=0x52 empty=0 full=0 [66000] in=0x55 out=0x53 empty=0 full=0 [76000] in=0x56 out=0x54 empty=0 full=0 [86000] flush out=0x55 empty=0 full=0 [96000] flush out=0x56 empty=1 full=0 [106000] done: empty=1 full=0 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpteou8o4f/tb_fifo_stream.v:49: $finish called at 106000 (1ps)
Port and parameter reference
| Name | Kind | Meaning |
|---|---|---|
DATA_WIDTH | parameter | Bits per stored word |
DEPTH | parameter | Number of words; use a power of two |
clk | input | Single clock for both ports |
rst_n | input | Active-low reset; clears both pointers |
wr_en / din | input | Write request and data in |
rd_en / dout | in / out | Read request and registered data out |
full / empty | output | Status flags for the two sides |
Common mistakes
- Using a single-width pointer. If both pointers are only
ADDR_WIDTHbits, full and empty both look likewr_ptr == rd_ptrand become indistinguishable. The extra MSB is the fix. - Dropping the full/empty guards. Writing while full or reading while empty silently advances a pointer and scrambles the order. Always gate with
do_writeanddo_read. - Expecting combinational read data.
douthere is registered, so it is valid one clock afterrd_en. If you need same-cycle data, build a first-word-fall-through variant instead. - Non-power-of-two DEPTH. The wrap-bit comparison relies on the address rolling over cleanly. For odd depths, track occupancy with an explicit up/down counter.
- Mixing blocking and non-blocking assignments inside the clocked blocks. Keep sequential logic on
<=to avoid simulation-versus-synthesis mismatches.
Quick check
Why is each pointer one bit wider than the address?
The extra most-significant bit records how many times the pointer has wrapped. When the address fields match, equal wrap bits mean empty and opposite wrap bits mean full — a distinction the address bits alone cannot make.
How many words can an 8-deep FIFO actually hold?
All eight. Because the pointers carry a wrap bit, full detection uses the entire depth; you do not sacrifice a slot the way a “leave one empty” scheme does.
When is dout valid after you assert rd_en?
On the next rising clock edge. The read port is registered, so the data appears one cycle after the read is accepted.
What stops a write to a full FIFO from corrupting data?
The do_write gate ANDs wr_en with not-full, so an illegal write never stores data or advances wr_ptr. The read side is guarded against underflow in the same way.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses