Design a Synchronous FIFO in Verilog
Build a parameterized FIFO in Verilog and verify it with a self-checking testbench.
Introduction
30 Second Summary
Every time data moves between two components running at different speeds inside a chip, something needs to hold onto it temporarily. Think of a queue at a busy coffee shop. Orders come in faster than the barista can make drinks, so they line up in order.
In this project, you will design a synchronous FIFO (First-In, First-Out) buffer in Verilog, write a self-checking testbench to verify it handles every corner case, and debug signal behavior using waveform analysis.
What You'll Build
A parameterized synchronous FIFO module with circular buffer pointer logic, a professional-grade self-checking testbench, and a complete compile-simulate-view workflow using Icarus Verilog and GTKWave.
By the end of this project, you'll have:
- A working parameterized FIFO module in Verilog with correct full/empty flag generation and simultaneous read/write handling.
- A self-checking testbench that exercises overflow, underflow, fill-then-drain, and simultaneous access corner cases with automated PASS/FAIL reporting.
- Hands-on experience with the compile-simulate-view workflow using Icarus Verilog and GTKWave to trace and fix bugs at the signal level.
- Secret Mission: Add a programmable almost-full threshold flag and verify it triggers at the correct level.
Are there any prerequisites?
You should be familiar with digital logic fundamentals like gates, flip-flops, registers, and FSMs from prior coursework. Experience with VHDL is helpful but not required.
Set Up Your Verilog Development Environment
Before building the FIFO, you need to confirm your simulation tools can run from the command line. Icarus Verilog and GTKWave are already installed on your machine, but they need to be accessible from any terminal window.
In this step, you will verify your tools are in PATH, set up VS Code with real-time Verilog linting, and test the complete compile-simulate workflow with a minimal module.
In this step, get ready to:
- Verify Icarus Verilog and GTKWave respond to version commands in your terminal.
- Install the VS Code Verilog extension and enable real-time linting.
- Create a project folder and test the full compile-run cycle.
Verify Icarus Verilog and GTKWave are accessible from your terminal
Your simulation tools need to be accessible from any terminal window. This means the directory containing their executables must be in your system's PATH environment variable.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check that Icarus Verilog is accessible by running:
iverilog -V
- Check that GTKWave is accessible by running:
gtkwave --version
✔️ Both commands print version information
Your tools are in PATH and ready to use. Move on to the next substep.
ⓧ One or both commands say 'not recognized' or 'not found'
The terminal cannot find the executables because their directory is not in your system PATH. You need to add the Icarus Verilog installation directory to PATH.
- On Windows, open Start and search for Environment Variables.
- Click Edit the system environment variables.
- Click Environment Variables.
- Under System variables, select Path and click Edit.
- Click New and add C:\iverilog\bin (or wherever your Icarus Verilog is installed).
- Click OK on all dialogs to save.
- Close and reopen your terminal, then run both commands again to confirm they work.
What about macOS or Linux?
On macOS or Linux, Icarus Verilog installed via a package manager (Homebrew, apt) is usually already in PATH. If not, add the install directory to your shell profile (~/.bashrc or ~/.zshrc).
Install the VS Code Verilog extension and configure linting
VS Code does not recognize Verilog syntax out of the box. A dedicated extension adds syntax highlighting, and connecting it to Icarus Verilog gives you real-time error checking as you type.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- In the search box, type Verilog-HDL/SystemVerilog/Bluespec SystemVerilog.
- Find the extension by mshr-h and click Install.
What does this extension do?
This extension adds Verilog and SystemVerilog syntax highlighting to VS Code. It also integrates with simulators like Icarus Verilog to underline errors in your code before you even compile.
- Open VS Code settings by pressing Cmd+, (macOS) or Ctrl+, (Windows).
- In the search bar at the top of Settings, type verilog.linting.linter.
- Set the dropdown value to iverilog.
Why set the linter to iverilog?
This tells the extension to run Icarus Verilog in the background every time you save a file. If you have a syntax error, it appears as a red underline immediately instead of waiting until you manually compile.
Not seeing iverilog in the dropdown?
Make sure you completed the PATH step above. The extension needs iverilog to be callable from the terminal for linting to work.
help me configure the Verilog linting extension in VS Code
Create a project folder and verify the full workflow with a minimal test
Now that your tools and editor are configured, you will test the full compile-and-run cycle. This confirms everything works together before you start building the FIFO.
- Switch back to your terminal and move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir sync_fifo_project
cd sync_fifo_project
- Confirm the folder exists by running:
ls
You should see an empty folder (no output) since you just created it.
- Switch back to VS Code.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the sync_fifo_project folder, and click Open.
Trust the folder if prompted
VS Code may ask if you trust the authors of this folder. Click Yes, I trust the authors to continue.
- Create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
- Save it immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it hello.v.
- Paste the following code into hello.v:
`timescale 1ns/1ps
module hello;
initial begin
$display("Hello Verilog");
$finish;
end
endmodule
What does this code do?
`timescale 1ns/1ps sets the simulation time unit and precision. Every Verilog file in this project starts with this directive.
The initial block runs once at simulation time zero. $display prints text to the console (like printf in C), and $finish ends the simulation.
- Save hello.v with Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm you see Verilog syntax highlighting (keywords like module and initial should be colored).
Now compile and run the module to test the full workflow.
- Open VS Code's integrated terminal by pressing Ctrl+` (same shortcut on both macOS and Windows).
- Compile hello.v into a simulation executable by running:
iverilog -o hello.vvp hello.v
- Run the simulation by running:
vvp hello.vvp
You should see Hello Verilog printed in the terminal output.
What just happened?
iverilog compiled your Verilog source into an intermediate format (hello.vvp). Then vvp executed that intermediate file as a simulation. This two-step compile-then-run workflow is what you will use throughout this project.
Seeing a syntax error or no output?
Make sure the backtick in `timescale is a true backtick character (the key above Tab on your keyboard), not an apostrophe.
Check that you saved the file before compiling. Unsaved changes are not picked up by iverilog.
help me fix a compile error in my hello.v Verilog file
- Intentionally introduce a typo in hello.v (for example, change module to modul) and save the file.
- Confirm that a red underline appears on the error. This proves real-time linting is working.
- Fix the typo back to module and save.
✔️ Awesome, I've got everything!
Great. Double check you have saved hello.v and that the terminal shows Hello Verilog in the output.
ⓧ I'd like to double check the full code
Here is the complete hello.v file for comparison:
`timescale 1ns/1ps
module hello;
initial begin
$display("Hello Verilog");
$finish;
end
endmodule
Your development environment is fully configured. Next up, you will design the synchronous FIFO module itself.
Design the Synchronous FIFO Module
Your development environment is ready. Now it is time to build the core component of this project: a synchronous FIFO buffer.
A FIFO (First-In, First-Out) is the most common data handoff mechanism inside real chips. It appears in bus interfaces, pipeline stages, and SoC interconnects. In this step, you will write the RTL for a parameterized synchronous FIFO using circular buffer pointer logic.
In this step, get ready to:
- Define a parameterized FIFO module interface with configurable width and depth.
- Implement sequential logic for pointer management and flag generation.
- Verify your design compiles cleanly with Icarus Verilog.
Create the module interface and internal signals
Every Verilog module starts with its interface: the parameters that make it reusable, and the ports that connect it to the outside world. You will also declare the internal storage and pointer registers that drive the FIFO's behavior.
- Create a new file called sync_fifo.v in your sync_fifo_project folder.
- Add the module declaration with parameters and port list by pasting this code:
`timescale 1ns/1ps
module sync_fifo #(
parameter DATA_WIDTH = 8,
parameter FIFO_DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire rd_en,
input wire [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_out,
output wire full,
output wire empty
);
// Calculate address width from FIFO depth
localparam ADDR_WIDTH = $clog2(FIFO_DEPTH);
// Internal storage and pointer registers
reg [DATA_WIDTH-1:0] mem [0:FIFO_DEPTH-1];
reg [ADDR_WIDTH:0] wr_ptr;
reg [ADDR_WIDTH:0] rd_ptr;
reg [ADDR_WIDTH:0] count;
What does this code do?
The parameter block makes the FIFO reusable. DATA_WIDTH controls how wide each word is (default 8 bits). FIFO_DEPTH controls how many words can be stored (default 16).
The localparam ADDR_WIDTH = $clog2(FIFO_DEPTH) automatically calculates how many address bits the pointers need. For a depth of 16, that is 4 bits.
The pointers and count are declared one bit wider than ADDR_WIDTH ([ADDR_WIDTH:0]) so they can represent the full range from 0 to FIFO_DEPTH without overflow.
The rst_n port is an active-low reset. When it goes low, the FIFO clears all state.
- Save sync_fifo.v.
- You won't see a change yet. The next chunk adds the behavioral logic that completes the module.
Implement the sequential logic
The FIFO needs to handle four scenarios on every clock edge: write-only, read-only, simultaneous read and write, and idle. You will use a case statement that concatenates the qualified write and read enables into a 2-bit selector.
- In sync_fifo.v, add the following code directly below the count register declaration:
assign full = (count == FIFO_DEPTH);
assign empty = (count == 0);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 0;
rd_ptr <= 0;
count <= 0;
data_out <= 0;
end else begin
case ({wr_en && !full, rd_en && !empty})
2'b10: begin // Write only
mem[wr_ptr[ADDR_WIDTH-1:0]] <= data_in;
wr_ptr <= wr_ptr + 1;
count <= count + 1;
end
2'b01: begin // Read only
data_out <= mem[rd_ptr[ADDR_WIDTH-1:0]];
rd_ptr <= rd_ptr + 1;
count <= count - 1;
end
2'b11: begin // Simultaneous read and write
mem[wr_ptr[ADDR_WIDTH-1:0]] <= data_in;
data_out <= mem[rd_ptr[ADDR_WIDTH-1:0]];
wr_ptr <= wr_ptr + 1;
rd_ptr <= rd_ptr + 1;
// count stays the same
end
default: begin
// Idle - do nothing
end
endcase
end
end
endmodule
What does this code do?
- assign full and assign empty are combinational. They update instantly whenever count changes.
- The case selector {wr_en && !full, rd_en && !empty} qualifies each enable. A write is only valid when the FIFO is not full. A read is only valid when the FIFO is not empty.
- All assignments inside the always block use <= (non-blocking). This is mandatory for sequential logic so all registers update simultaneously at the clock edge.
- In the simultaneous case (2'b11), both pointers advance but count stays unchanged because one word enters and one word leaves.
Why non-blocking assignments?
Blocking assignments (=) execute in sequence, like software. Non-blocking assignments (<=) schedule all updates to happen at the same instant. In synthesizable sequential logic, mixing these up causes simulation mismatches with real hardware behavior.
- Save sync_fifo.v.
- Compile the module to check for syntax errors by running this command in your terminal:
iverilog -o fifo.vvp sync_fifo.v
- You should see no output at all. A silent return to the prompt means zero errors.
Seeing syntax errors?
- Check that you have endmodule as the very last line. A missing endmodule causes a cascade of confusing errors.
- Verify every begin has a matching end. The case block has four begin/end pairs plus the outer if/else pair.
- Make sure `timescale 1ns/1ps is on the very first line with the backtick character, not a regular apostrophe.
Help me debug my sync_fifo.v compile errors
✔️ Awesome, I've got everything!
Great. Double check you have saved sync_fifo.v and the compile returned with no errors.
ⓧ I'd like to double check the full code
`timescale 1ns/1ps
module sync_fifo #(
parameter DATA_WIDTH = 8,
parameter FIFO_DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire rd_en,
input wire [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_out,
output wire full,
output wire empty
);
localparam ADDR_WIDTH = $clog2(FIFO_DEPTH);
reg [DATA_WIDTH-1:0] mem [0:FIFO_DEPTH-1];
reg [ADDR_WIDTH:0] wr_ptr;
reg [ADDR_WIDTH:0] rd_ptr;
reg [ADDR_WIDTH:0] count;
assign full = (count == FIFO_DEPTH);
assign empty = (count == 0);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 0;
rd_ptr <= 0;
count <= 0;
data_out <= 0;
end else begin
case ({wr_en && !full, rd_en && !empty})
2'b10: begin // Write only
mem[wr_ptr[ADDR_WIDTH-1:0]] <= data_in;
wr_ptr <= wr_ptr + 1;
count <= count + 1;
end
2'b01: begin // Read only
data_out <= mem[rd_ptr[ADDR_WIDTH-1:0]];
rd_ptr <= rd_ptr + 1;
count <= count - 1;
end
2'b11: begin // Simultaneous read and write
mem[wr_ptr[ADDR_WIDTH-1:0]] <= data_in;
data_out <= mem[rd_ptr[ADDR_WIDTH-1:0]];
wr_ptr <= wr_ptr + 1;
rd_ptr <= rd_ptr + 1;
// count stays the same
end
default: begin
// Idle - do nothing
end
endcase
end
end
endmodule
Verify your mental model with a manual trace
Before you simulate, it is worth tracing through the pointer logic by hand. This catches design errors faster than staring at waveforms later.
- Grab a piece of paper or open a text file.
- Trace the values of wr_ptr, rd_ptr, count, full, and empty through this sequence:
- Reset (all pointers and count go to 0, empty = 1, full = 0).
- Write 3 words (wr_ptr advances to 3, count = 3, empty deasserts).
- Read 2 words (rd_ptr advances to 2, count = 1).
- Write until full (count reaches 16, full asserts).
- Read until empty (count reaches 0, empty asserts again).
What should I look for?
Confirm that full only asserts when count == 16 and empty only asserts when count == 0. If your trace shows a flag triggering one cycle early or late, you have an off-by-one error in the logic.
Also check the simultaneous read/write case. If the FIFO is full and both wr_en and rd_en go high, the selector evaluates to 2'b11 because rd_en && !empty is true while wr_en && !full is false. Wait. That is actually 2'b01 (read-only). This is intentional. The FIFO protects itself from overflow.
Your FIFO module compiles cleanly and handles all four pointer scenarios. Next up, you will write a self-checking testbench that exercises every corner case automatically.
Write the Self-Checking Testbench
Your FIFO module compiles cleanly, but a design that compiles is not the same as a design that works. The only way to know your pointer logic and flag generation are correct is to exercise every corner case with a testbench that checks its own results.
In this step, you will write a self-checking testbench that automatically reports PASS or FAIL for each scenario. The testbench will also dump signal data to a VCD file so you can inspect waveforms in the next step.
In this step, get ready to:
- Create a testbench file with clock generation, reset logic, and VCD waveform dumping.
- Write five test scenarios covering overflow, underflow, and simultaneous access.
- Compile and run the simulation to see automated PASS/FAIL results.
Create the testbench module and instantiate the DUT
A testbench is a non-synthesizable Verilog module that drives stimulus into your design and checks the outputs. The first thing it needs is a copy of every signal the DUT (Design Under Test) exposes, plus an instantiation that wires them together.
- Create a new file called sync_fifo_tb.v inside your sync_fifo_project folder.
- Add the module declaration, signal declarations, and DUT instantiation by pasting this code:
`timescale 1ns/1ps
module sync_fifo_tb;
parameter DATA_WIDTH = 8;
parameter FIFO_DEPTH = 16;
reg clk;
reg rst_n;
reg wr_en;
reg rd_en;
reg [DATA_WIDTH-1:0] data_in;
wire [DATA_WIDTH-1:0] data_out;
wire full;
wire empty;
integer error_count;
integer i;
sync_fifo #(
.DATA_WIDTH(DATA_WIDTH),
.FIFO_DEPTH(FIFO_DEPTH)
) dut (
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.rd_en(rd_en),
.data_in(data_in),
.data_out(data_out),
.full(full),
.empty(empty)
);
What does this code do?
- reg signals are inputs to the DUT (driven by the testbench). wire signals are outputs from the DUT (observed by the testbench).
- The #(.DATA_WIDTH(DATA_WIDTH), .FIFO_DEPTH(FIFO_DEPTH)) syntax passes parameters to the FIFO instance, matching the defaults from your design.
- error_count and i are integer variables for tracking test failures and looping through test data.
Add clock generation, VCD dumping, and utility tasks
Every synchronous testbench needs a clock. You also need reusable tasks for reset, write, and read operations so your test scenarios stay readable.
- Add the following code below the DUT instantiation (after the closing ); of the instance):
// Clock generation: 10ns period
initial clk = 0;
always #5 clk = ~clk;
// VCD dump
initial begin
$dumpfile("fifo_waves.vcd");
$dumpvars(0, sync_fifo_tb);
end
// Reset task
task reset;
begin
rst_n = 0;
wr_en = 0;
rd_en = 0;
data_in = 0;
@(posedge clk);
@(posedge clk);
rst_n = 1;
@(posedge clk);
end
endtask
// Write task
task write_fifo;
input [DATA_WIDTH-1:0] wdata;
begin
@(negedge clk);
wr_en = 1;
data_in = wdata;
@(posedge clk);
#1;
wr_en = 0;
end
endtask
// Read task
task read_fifo;
output [DATA_WIDTH-1:0] rdata;
begin
@(negedge clk);
rd_en = 1;
@(posedge clk);
#1;
rdata = data_out;
rd_en = 0;
end
endtask
What does this code do?
- always #5 clk = ~clk toggles the clock every 5ns, creating a 10ns period (100 MHz). The initial clk = 0 sets the starting value.
- $dumpfile and $dumpvars record every signal change to a VCD file so you can view waveforms in GTKWave later.
- The reset task holds rst_n low for two clock cycles, then releases it. This ensures all registers reach a known state.
- write_fifo and read_fifo drive signals on the falling edge so data is stable well before the rising edge where the DUT samples it.
- Save sync_fifo_tb.v.
- You won't see a change yet. The next chunk adds the test scenarios that actually exercise the FIFO.
Write test scenarios with automatic checking
A self-checking testbench compares actual outputs against expected values and prints PASS or FAIL without human intervention. You will write five tests that cover the critical corner cases interviewers ask about: ordering, overflow, underflow, and simultaneous access.
- Add the first three test scenarios below the read_fifo task by pasting this code:
// Main test sequence
reg [DATA_WIDTH-1:0] read_data;
initial begin
error_count = 0;
// Test 1: Write and read back - verify FIFO ordering
$display("--- Test 1: Write and Read Back ---");
reset;
for (i = 0; i < 5; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
for (i = 0; i < 5; i = i + 1) begin
read_fifo(read_data);
if (read_data !== i[DATA_WIDTH-1:0]) begin
$display("FAIL: Test 1 - Expected %0d, Got %0d at time %0t", i, read_data, $time);
error_count = error_count + 1;
end
end
if (empty !== 1) begin
$display("FAIL: Test 1 - FIFO not empty after reading all data");
error_count = error_count + 1;
end else begin
$display("PASS: Test 1");
end
// Test 2: Fill until full
$display("--- Test 2: Fill Until Full ---");
reset;
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
if (full !== 1) begin
$display("FAIL: Test 2 - FIFO not full after %0d writes", FIFO_DEPTH);
error_count = error_count + 1;
end else begin
$display("PASS: Test 2 - Full flag asserted correctly");
end
// Test 3: Write when full should not corrupt
$display("--- Test 3: Write When Full ---");
write_fifo(8'hFF);
// Read back all data, verify no corruption
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
read_fifo(read_data);
if (read_data !== i[DATA_WIDTH-1:0]) begin
$display("FAIL: Test 3 - Data corrupted. Expected %0d, Got %0d", i, read_data);
error_count = error_count + 1;
end
end
if (error_count == 0) $display("PASS: Test 3 - No corruption on write-when-full");
What are these tests checking?
- Test 1 writes values 0-4, reads them back, and confirms they come out in the same order. This proves first-in, first-out behavior.
- Test 2 writes exactly FIFO_DEPTH words and checks that the full flag asserts at exactly the right count.
- Test 3 attempts a write while the FIFO is already full. It then drains the entire FIFO and verifies no existing data was overwritten.
- Add Tests 4 and 5 immediately below Test 3 (still inside the same initial begin block):
// Test 4: Read when empty should not corrupt
$display("--- Test 4: Read When Empty ---");
reset;
if (empty !== 1) begin
$display("FAIL: Test 4 - FIFO not empty after reset");
error_count = error_count + 1;
end
rd_en = 1;
@(posedge clk);
#1;
rd_en = 0;
if (empty !== 1) begin
$display("FAIL: Test 4 - Empty flag cleared after invalid read");
error_count = error_count + 1;
end else begin
$display("PASS: Test 4 - Read-when-empty handled correctly");
end
// Test 5: Simultaneous read/write when full
$display("--- Test 5: Simultaneous R/W When Full ---");
reset;
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
// Simultaneous read and write
@(negedge clk);
wr_en = 1;
rd_en = 1;
data_in = 8'hAB;
@(posedge clk);
#1;
wr_en = 0;
rd_en = 0;
if (full !== 1) begin
$display("FAIL: Test 5 - Full flag incorrectly cleared");
error_count = error_count + 1;
end
// Read the newly written value (should be at end after draining)
read_fifo(read_data);
if (read_data !== 8'h01) begin
$display("FAIL: Test 5 - Expected first remaining element 1, Got %0d", read_data);
error_count = error_count + 1;
end else begin
$display("PASS: Test 5 - Simultaneous R/W when full works");
end
What are these edge-case tests checking?
- Test 4 attempts a read from an empty FIFO and verifies the empty flag stays asserted. This confirms your design gracefully ignores invalid reads.
- Test 5 fills the FIFO completely, then asserts both wr_en and rd_en simultaneously. The FIFO should accept the new write and produce a valid read without losing data or de-asserting full.
Add the final summary and close the testbench
The summary block runs after all tests complete and gives you a single go/no-go answer. This is what makes the testbench truly self-checking.
- Add this code immediately after Test 5 to close out the initial block and the module:
// Final summary
$display("================================");
if (error_count == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d errors", error_count);
$display("================================");
#100;
$finish;
end
endmodule
What does this code do?
- If error_count is zero after all tests, the testbench prints ALL TESTS PASSED. Otherwise it reports exactly how many failures occurred.
- #100 adds a short delay so the final signals are captured in the VCD file before $finish terminates the simulation.
- Save sync_fifo_tb.v.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on to compile.
ⓧ I'd like to double check the full code
`timescale 1ns/1ps
module sync_fifo_tb;
parameter DATA_WIDTH = 8;
parameter FIFO_DEPTH = 16;
reg clk;
reg rst_n;
reg wr_en;
reg rd_en;
reg [DATA_WIDTH-1:0] data_in;
wire [DATA_WIDTH-1:0] data_out;
wire full;
wire empty;
integer error_count;
integer i;
sync_fifo #(
.DATA_WIDTH(DATA_WIDTH),
.FIFO_DEPTH(FIFO_DEPTH)
) dut (
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.rd_en(rd_en),
.data_in(data_in),
.data_out(data_out),
.full(full),
.empty(empty)
);
// Clock generation: 10ns period
initial clk = 0;
always #5 clk = ~clk;
// VCD dump
initial begin
$dumpfile("fifo_waves.vcd");
$dumpvars(0, sync_fifo_tb);
end
// Reset task
task reset;
begin
rst_n = 0;
wr_en = 0;
rd_en = 0;
data_in = 0;
@(posedge clk);
@(posedge clk);
rst_n = 1;
@(posedge clk);
end
endtask
// Write task
task write_fifo;
input [DATA_WIDTH-1:0] wdata;
begin
@(negedge clk);
wr_en = 1;
data_in = wdata;
@(posedge clk);
#1;
wr_en = 0;
end
endtask
// Read task
task read_fifo;
output [DATA_WIDTH-1:0] rdata;
begin
@(negedge clk);
rd_en = 1;
@(posedge clk);
#1;
rdata = data_out;
rd_en = 0;
end
endtask
// Main test sequence
reg [DATA_WIDTH-1:0] read_data;
initial begin
error_count = 0;
// Test 1: Write and read back - verify FIFO ordering
$display("--- Test 1: Write and Read Back ---");
reset;
for (i = 0; i < 5; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
for (i = 0; i < 5; i = i + 1) begin
read_fifo(read_data);
if (read_data !== i[DATA_WIDTH-1:0]) begin
$display("FAIL: Test 1 - Expected %0d, Got %0d at time %0t", i, read_data, $time);
error_count = error_count + 1;
end
end
if (empty !== 1) begin
$display("FAIL: Test 1 - FIFO not empty after reading all data");
error_count = error_count + 1;
end else begin
$display("PASS: Test 1");
end
// Test 2: Fill until full
$display("--- Test 2: Fill Until Full ---");
reset;
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
if (full !== 1) begin
$display("FAIL: Test 2 - FIFO not full after %0d writes", FIFO_DEPTH);
error_count = error_count + 1;
end else begin
$display("PASS: Test 2 - Full flag asserted correctly");
end
// Test 3: Write when full should not corrupt
$display("--- Test 3: Write When Full ---");
write_fifo(8'hFF);
// Read back all data, verify no corruption
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
read_fifo(read_data);
if (read_data !== i[DATA_WIDTH-1:0]) begin
$display("FAIL: Test 3 - Data corrupted. Expected %0d, Got %0d", i, read_data);
error_count = error_count + 1;
end
end
if (error_count == 0) $display("PASS: Test 3 - No corruption on write-when-full");
// Test 4: Read when empty should not corrupt
$display("--- Test 4: Read When Empty ---");
reset;
if (empty !== 1) begin
$display("FAIL: Test 4 - FIFO not empty after reset");
error_count = error_count + 1;
end
rd_en = 1;
@(posedge clk);
#1;
rd_en = 0;
if (empty !== 1) begin
$display("FAIL: Test 4 - Empty flag cleared after invalid read");
error_count = error_count + 1;
end else begin
$display("PASS: Test 4 - Read-when-empty handled correctly");
end
// Test 5: Simultaneous read/write when full
$display("--- Test 5: Simultaneous R/W When Full ---");
reset;
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
// Simultaneous read and write
@(negedge clk);
wr_en = 1;
rd_en = 1;
data_in = 8'hAB;
@(posedge clk);
#1;
wr_en = 0;
rd_en = 0;
if (full !== 1) begin
$display("FAIL: Test 5 - Full flag incorrectly cleared");
error_count = error_count + 1;
end
// Read the newly written value (should be at end after draining)
read_fifo(read_data);
if (read_data !== 8'h01) begin
$display("FAIL: Test 5 - Expected first remaining element 1, Got %0d", read_data);
error_count = error_count + 1;
end else begin
$display("PASS: Test 5 - Simultaneous R/W when full works");
end
// Final summary
$display("================================");
if (error_count == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d errors", error_count);
$display("================================");
#100;
$finish;
end
endmodule
Compile and run the testbench
Now you will compile both files together and run the simulation to see whether your FIFO passes all five tests.
- Compile both source files together by running this command in your terminal:
iverilog -o fifo_sim.vvp sync_fifo_tb.v sync_fifo.v
What does this command do?
iverilog compiles both sync_fifo_tb.v and sync_fifo.v into a single simulation executable called fifo_sim.vvp. The testbench needs the FIFO source because it instantiates the module.
- Confirm the compilation produced no error output. You should see no messages at all (a clean compile is silent).
Seeing compile errors?
- Check that your port names in the DUT instantiation match exactly (.clk(clk), .rst_n(rst_n), etc.). Verilog port names are case-sensitive.
- Make sure both files are in the same sync_fifo_project folder and your terminal is in that folder.
- Look for mismatched begin/end pairs. Every begin needs a matching end, and every task needs an endtask.
Help me debug my iverilog compile error for sync_fifo_tb.v
- Run the simulation by executing this command:
vvp fifo_sim.vvp
- You should see test output with PASS/FAIL messages for each test, followed by ALL TESTS PASSED or a failure count.
Seeing test failures?
Do not worry if you see failures on the first run. This is expected and valuable. The error messages include the simulation time and expected vs. actual values, which will help you debug in the next step using GTKWave waveforms.
If the simulation hangs (no output and no termination), you likely have an infinite loop. Check that your clock always #5 clk = ~clk is present and that $finish exists at the end of your initial block. Press Ctrl+C to stop a hung simulation.
Help me understand why my FIFO testbench is failing
Your testbench is now exercising every critical corner case of your FIFO design. Next up, you will open the VCD waveform file in GTKWave and learn to trace signal behavior cycle-by-cycle to find and fix bugs.
Simulate and Debug with GTKWave
Your testbench is written and compiles cleanly. The next challenge is actually running it and proving your FIFO works.
In this step, you will execute the simulation to generate a waveform file, then use GTKWave to visually trace signal behavior across time. You will also practice the most important debugging skill in RTL design: finding exactly which clock cycle a bug appears by inspecting pointer and flag values in the waveform viewer.
In this step, get ready to:
- Run the simulation and generate a VCD waveform file.
- Open and navigate waveforms in GTKWave.
- Trace and fix a bug using waveform analysis.
Run the simulation and generate the VCD file
Your testbench already contains the $dumpfile and $dumpvars system tasks that tell Icarus Verilog to record every signal transition into a VCD file during simulation. Running the simulation will produce both the test output and the waveform file simultaneously.
- Run the compiled simulation by entering this command in your terminal:
vvp fifo_sim.vvp
You should see all five tests print their results, ending with the summary line.
What should I see?
Your terminal prints each test's PASS/FAIL status followed by a final summary. If your FIFO logic is correct, you will see ALL TESTS PASSED at the bottom.
A file called fifo_waves.vcd now exists in your sync_fifo_project folder. This contains the full signal history of the simulation.
- Confirm the VCD file was created by running:
ls fifo_waves.vcd
You should see fifo_waves.vcd listed in the output.
If all tests passed on the first try, that is great news for your FIFO design. To practice debugging with waveforms, you will now intentionally introduce a bug so you have a real failure to trace.
- In sync_fifo.v, find this line inside the full flag assignment:
assign full = (count == FIFO_DEPTH);
- Change == to >, so the line now looks like this:
assign full = (count > FIFO_DEPTH);
Why this specific bug?
Since count can never exceed FIFO_DEPTH (the write guard prevents it), full will never assert. The FIFO will keep accepting writes past its memory limit, corrupting data. This is a subtle but realistic bug that mirrors real-world off-by-one errors in RTL.
- Save sync_fifo.v.
- Recompile the design with the bug by running:
iverilog -o fifo_sim.vvp sync_fifo_tb.v sync_fifo.v
- Run the simulation again by running:
vvp fifo_sim.vvp
This time you should see FAIL messages in your terminal output. Note the simulation time printed alongside each failure. You will use that time value to navigate directly to the bug in GTKWave.
Still seeing ALL TESTS PASSED?
Make sure you saved sync_fifo.v after the edit. If you recompiled before saving, the old code was used.
Double check that you changed the assign full line specifically, not the assign empty line.
Help me figure out why my intentional bug isn't causing test failures.
Open and navigate waveforms in GTKWave
The VCD file contains a record of every signal's value at every simulation time step. GTKWave lets you visualize these transitions as waveforms so you can see exactly what happened inside your FIFO, clock cycle by clock cycle.
- Open the waveform file in GTKWave by running:
gtkwave fifo_waves.vcd
GTKWave opens with an empty Waves panel on the right and a Signal Search Tree (SST) on the left.
- In the Signal Search Tree (left panel), click on sync_fifo_tb to expand it.
- Click on the FIFO instance (dut) to see its internal signals.
- Select clk, rst_n, wr_en, rd_en, data_in, data_out, full, empty, wr_ptr, rd_ptr, and count and drag them into the Waves panel on the right.
Select multiple signals at once
Hold Ctrl (Windows) or Cmd (macOS) while clicking to select multiple signals before dragging them all into the Waves panel together.
- Press Ctrl+0 or click the Zoom Fit button in the toolbar to fit the entire simulation into view.
- Left-click anywhere on the waveform to drop the primary marker. The signal values at that exact time appear in the value column.
- Use Ctrl+Scroll Up to zoom in and Ctrl+Scroll Down to zoom out.
You should now see all your FIFO signals displayed as waveforms across time. The clk signal appears as a regular square wave, and you can see when wr_en and rd_en toggle during each test.
What am I looking at?
Each horizontal trace represents one signal's value over time. Single-bit signals (like clk and full) show as high/low transitions. Multi-bit signals (like count and data_in) show their numeric value inside colored blocks.
The primary marker (vertical line) lets you inspect the exact value of every signal at one specific point in time. This is how you will pinpoint bugs.
Trace and fix the bug
Now you will use the waveforms to find exactly where the intentional bug causes incorrect behavior. This is the same workflow you would use to debug any RTL failure in a real project.
- Look at the FAIL message in your terminal output from the last simulation run. Note the simulation time printed in the message (the %0t value).
- In GTKWave, left-click on the waveform at approximately that time to place your primary marker there.
- Zoom in with Ctrl+Scroll Up until you can see individual clock cycles around the failure point.
What should I look for?
With the bug you introduced, count keeps incrementing past the FIFO depth but full never goes high. In a correct design, full should assert when count reaches 16 (FIFO_DEPTH).
Move your marker to the cycle where count reaches 16. You can see that full remains at 0. This confirms the flag logic is wrong.
- Examine the wr_ptr signal at this point. Since full never asserted, writes continued past the memory boundary, corrupting earlier entries.
You have identified the root cause: the full flag condition uses > instead of ==, so it never triggers. Now fix it.
- Switch back to VS Code and open sync_fifo.v.
- Find this line:
assign full = (count > FIFO_DEPTH);
- Change > back to ==, restoring the correct logic:
assign full = (count == FIFO_DEPTH);
- Save sync_fifo.v.
- Recompile the design by running:
iverilog -o fifo_sim.vvp sync_fifo_tb.v sync_fifo.v
- Re-run the simulation to verify the fix by running:
vvp fifo_sim.vvp
You should now see ALL TESTS PASSED in your terminal output. The bug is fixed and every corner case passes.
What did you just practice?
You completed the full RTL debugging loop: observe a test failure, open the waveform, navigate to the failing clock cycle, inspect signal values to identify root cause, fix the code, and re-verify. This compile-simulate-view workflow with Icarus Verilog and GTKWave is the same process used in professional FPGA and ASIC verification.
Still seeing test failures after the fix?
Confirm the assign full line reads exactly assign full = (count == FIFO_DEPTH); with the double equals operator.
Make sure you saved the file before recompiling. VS Code shows a dot on the tab name if the file has unsaved changes.
Help me debug why my FIFO tests are still failing after fixing the full flag.
✔️ Awesome, I've got everything!
Great. Double check that you have saved sync_fifo.v with the correct == operator restored and that your last simulation run prints ALL TESTS PASSED.
ⓧ I'd like to double check the full code
Here is the complete sync_fifo.v with the correct flag logic:
`timescale 1ns/1ps
module sync_fifo #(
parameter DATA_WIDTH = 8,
parameter FIFO_DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire rd_en,
input wire [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_out,
output wire full,
output wire empty
);
localparam ADDR_WIDTH = $clog2(FIFO_DEPTH);
reg [DATA_WIDTH-1:0] mem [0:FIFO_DEPTH-1];
reg [ADDR_WIDTH:0] wr_ptr;
reg [ADDR_WIDTH:0] rd_ptr;
reg [ADDR_WIDTH:0] count;
assign full = (count == FIFO_DEPTH);
assign empty = (count == 0);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 0;
rd_ptr <= 0;
count <= 0;
data_out <= 0;
end else begin
case ({wr_en && !full, rd_en && !empty})
2'b10: begin // Write only
mem[wr_ptr[ADDR_WIDTH-1:0]] <= data_in;
wr_ptr <= wr_ptr + 1;
count <= count + 1;
end
2'b01: begin // Read only
data_out <= mem[rd_ptr[ADDR_WIDTH-1:0]];
rd_ptr <= rd_ptr + 1;
count <= count - 1;
end
2'b11: begin // Simultaneous read and write
mem[wr_ptr[ADDR_WIDTH-1:0]] <= data_in;
data_out <= mem[rd_ptr[ADDR_WIDTH-1:0]];
wr_ptr <= wr_ptr + 1;
rd_ptr <= rd_ptr + 1;
// count stays the same
end
default: begin
// Idle - do nothing
end
endcase
end
end
endmodule
And here is the complete sync_fifo_tb.v (unchanged from the previous step):
`timescale 1ns/1ps
module sync_fifo_tb;
parameter DATA_WIDTH = 8;
parameter FIFO_DEPTH = 16;
reg clk;
reg rst_n;
reg wr_en;
reg rd_en;
reg [DATA_WIDTH-1:0] data_in;
wire [DATA_WIDTH-1:0] data_out;
wire full;
wire empty;
integer error_count;
integer i;
sync_fifo #(
.DATA_WIDTH(DATA_WIDTH),
.FIFO_DEPTH(FIFO_DEPTH)
) dut (
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.rd_en(rd_en),
.data_in(data_in),
.data_out(data_out),
.full(full),
.empty(empty)
);
// Clock generation: 10ns period
initial clk = 0;
always #5 clk = ~clk;
// VCD dump
initial begin
$dumpfile("fifo_waves.vcd");
$dumpvars(0, sync_fifo_tb);
end
// Reset task
task reset;
begin
rst_n = 0;
wr_en = 0;
rd_en = 0;
data_in = 0;
@(posedge clk);
@(posedge clk);
rst_n = 1;
@(posedge clk);
end
endtask
// Write task
task write_fifo;
input [DATA_WIDTH-1:0] wdata;
begin
@(negedge clk);
wr_en = 1;
data_in = wdata;
@(posedge clk);
#1;
wr_en = 0;
end
endtask
// Read task
task read_fifo;
output [DATA_WIDTH-1:0] rdata;
begin
@(negedge clk);
rd_en = 1;
@(posedge clk);
#1;
rdata = data_out;
rd_en = 0;
end
endtask
// Main test sequence
reg [DATA_WIDTH-1:0] read_data;
initial begin
error_count = 0;
// Test 1: Write and read back - verify FIFO ordering
$display("--- Test 1: Write and Read Back ---");
reset;
for (i = 0; i < 5; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
for (i = 0; i < 5; i = i + 1) begin
read_fifo(read_data);
if (read_data !== i[DATA_WIDTH-1:0]) begin
$display("FAIL: Test 1 - Expected %0d, Got %0d at time %0t", i, read_data, $time);
error_count = error_count + 1;
end
end
if (empty !== 1) begin
$display("FAIL: Test 1 - FIFO not empty after reading all data");
error_count = error_count + 1;
end else begin
$display("PASS: Test 1");
end
// Test 2: Fill until full
$display("--- Test 2: Fill Until Full ---");
reset;
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
if (full !== 1) begin
$display("FAIL: Test 2 - FIFO not full after %0d writes", FIFO_DEPTH);
error_count = error_count + 1;
end else begin
$display("PASS: Test 2 - Full flag asserted correctly");
end
// Test 3: Write when full should not corrupt
$display("--- Test 3: Write When Full ---");
write_fifo(8'hFF);
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
read_fifo(read_data);
if (read_data !== i[DATA_WIDTH-1:0]) begin
$display("FAIL: Test 3 - Data corrupted. Expected %0d, Got %0d", i, read_data);
error_count = error_count + 1;
end
end
if (error_count == 0) $display("PASS: Test 3 - No corruption on write-when-full");
// Test 4: Read when empty should not corrupt
$display("--- Test 4: Read When Empty ---");
reset;
if (empty !== 1) begin
$display("FAIL: Test 4 - FIFO not empty after reset");
error_count = error_count + 1;
end
rd_en = 1;
@(posedge clk);
#1;
rd_en = 0;
if (empty !== 1) begin
$display("FAIL: Test 4 - Empty flag cleared after invalid read");
error_count = error_count + 1;
end else begin
$display("PASS: Test 4 - Read-when-empty handled correctly");
end
// Test 5: Simultaneous read/write when full
$display("--- Test 5: Simultaneous R/W When Full ---");
reset;
for (i = 0; i < FIFO_DEPTH; i = i + 1) begin
write_fifo(i[DATA_WIDTH-1:0]);
end
@(negedge clk);
wr_en = 1;
rd_en = 1;
data_in = 8'hAB;
@(posedge clk);
#1;
wr_en = 0;
rd_en = 0;
if (full !== 1) begin
$display("FAIL: Test 5 - Full flag incorrectly cleared");
error_count = error_count + 1;
end
read_fifo(read_data);
if (read_data !== 8'h01) begin
$display("FAIL: Test 5 - Expected first remaining element 1, Got %0d", read_data);
error_count = error_count + 1;
end else begin
$display("PASS: Test 5 - Simultaneous R/W when full works");
end
// Final summary
$display("================================");
if (error_count == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d errors", error_count);
$display("================================");
#100;
$finish;
end
endmodule
Secret mission
Add a Programmable Almost-Full Flag
Real FIFO designs don't just have full and empty flags. They have early-warning signals that let upstream logic throttle before overflow hits. Can you add a programmable almost-full threshold to your FIFO and prove it works with targeted verification?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference, or delete them to free up disk space.
Resources you used:
- Local sync_fifo_project folder (Verilog source files, testbench, compiled VVP files, VCD waveform).
- VS Code Verilog-HDL extension (mshr-h.VerilogHDL).
Keep everything running
No action needed. Choose this if you plan to continue experimenting with your FIFO design, adding new features, or using it as a reference for future RTL projects.
- Your sync_fifo_project folder stays on your Desktop with all source files, compiled outputs, and waveform data.
- The VS Code Verilog extension continues providing syntax highlighting and linting for any .v files you open.
Pause - I'll come back to this later
There are no running processes to stop. Your simulation finished when it hit $finish, so nothing is consuming memory or CPU.
- Your project folder and all files remain on disk, ready for you to pick up where you left off.
- To resume later, open a terminal, navigate to your sync_fifo_project folder, and recompile with iverilog.
Delete - I don't want to use this again
Remove all project files and the VS Code extension to start fresh.
- Delete the project folder by running this command in your terminal (macOS/Linux):
rm -rf ~/Desktop/sync_fifo_project
- On Windows, run this in PowerShell instead:
Remove-Item -Recurse -Force ~\Desktop\sync_fifo_project
- To uninstall the Verilog extension, open VS Code and press Ctrl+Shift+X to open Extensions.
- Find Verilog-HDL/SystemVerilog/Bluespec SystemVerilog in your installed extensions list.
- Click the gear icon next to the extension and select Uninstall.
Nice Work!
Nice Work!
Nice work! You just designed, verified, and debugged a parameterized synchronous FIFO in Verilog from scratch.
You've learned how to:
- Build a parameterized synchronous FIFO in Verilog with circular buffer pointer logic, full/empty flag generation, and safe simultaneous read/write handling.
- Write a self-checking testbench with automated PASS/FAIL reporting that exercises overflow, underflow, and simultaneous access corner cases.
- Use the full Icarus Verilog + GTKWave workflow to compile, simulate, dump VCD waveforms, and trace bugs down to the exact clock cycle.
- Secret Mission: Extended the FIFO with a programmable almost-full threshold flag and verified it triggers correctly using a second FIFO instance with an alternate threshold.
Ready to quiz yourself?