Virtual Expo 2026

D01-Design and verification of a Synchronous FIFO buffer

Envision Diode

Design and Verification of a Synchronous FIFO Buffer

Project Report

Mentors
Sai Easwar  |  Aarav Balaji  |  Akash

Mentees
Areeb Khan  |  Om Shinde  |  Mihir Shahane  |  Tanmay Jhagekar  |  Ishaan Krishna Acharya  |  Varun Nanajkar

Project Presentation Link:
https://meet.google.com/zar-winy-gbw

1. Introduction

Modern digital systems are rarely monolithic. A processor, a peripheral interface, and a memory controller each operate at their own pace. When a high-speed transmitter generates data faster than a receiver can absorb it, the excess data is simply lost — unless there is a buffer in between.

This project addresses that fundamental problem by designing a Synchronous FIFO (First-In, First-Out) buffer in Verilog HDL. A FIFO is a queue-type memory structure: the first data element written is always the first to be read back. By operating on a single shared clock, the synchronous FIFO offers simplicity, predictability, and a clear path toward understanding more advanced architectures such as the asynchronous FIFO.

The project is structured progressively — beginning with the theoretical motivation for data buffering, working through the architectural design of each sub-module, and concluding with a simulation-based verification campaign. The Verilog implementation is decomposed into four cooperating modules: a memory array, a write-pointer controller, a read-pointer controller, and a top-level integrator. A self-checking testbench exercises the design under realistic traffic patterns.

2. Motivation: Why Do Digital Systems Need Buffers?

Digital systems generate data in bursts. Consider a microcontroller sampling a sensor at high speed and pushing readings to a slower UART transmitter. Without an intermediate buffer, every sample generated during a UART busy period is irretrievably lost.

The FIFO buffer solves this by acting as an elastic queue: the producer writes freely until the queue is full, while the consumer reads at its own pace until the queue is empty. Status flags (full, empty) prevent either side from violating the buffer boundaries. Real-world applications include:

  • Keyboard scan-code queues between the keyboard controller and the host CPU
  • UART transmit/receive holding registers in serial communication
  • Audio sample buffers between a DSP and a DAC
  • Data pipelines between clock domains in SoC designs

3. Background Concepts

3.1 Sequential Logic and Clocking

Unlike combinational logic, which produces outputs solely as a function of current inputs, sequential circuits maintain state. A synchronous design routes all state-changes through a common clock edge, guaranteeing predictable timing and simplifying formal verification. This FIFO is fully synchronous: all pointer updates occur on the rising edge of the system clock.

3.2 Circular Buffer and Pointer Arithmetic

A FIFO of depth N is implemented over a linear memory array by treating it as circular. Two pointers track positions within the array:

  • Write Pointer (wr_ptr): points to the next location to be written.
  • Read Pointer (rd_ptr): points to the next location to be read.

Both pointers are incremented modulo N. When a pointer reaches the end of the array it wraps back to address 0, re-using locations that have already been consumed by the reader. This avoids the costly data-shifting required by a straight linear queue.

3.3 Full and Empty Detection with the Extra-Bit Technique

Distinguishing a full FIFO from an empty FIFO is non-trivial: in both states the lower-order bits of wr_ptr and rd_ptr are identical. The extra-bit technique resolves this ambiguity by extending each pointer by one additional bit — a "wrap" flag that toggles every time the pointer completes a full revolution.

Empty condition: wr_ptr == rd_ptr (all bits, including the wrap bit, are equal).

Full condition: The MSBs (wrap bits) differ but the lower address bits are equal, expressed as {{~wr_ptr[3], wr_ptr[2:0]}} == rd_ptr.

This technique is used verbatim in the Verilog implementation described in Section 5.

4. Design Specification

4.1 Interface Definition

The top-level module (sync_fifo_top) exposes the following ports:

Port Direction Width Description
clk Input 1-bit System clock — all state changes on rising edge
reset Input 1-bit Synchronous active-high reset; clears all pointers
wr_enable Input 1-bit Write request from the producer
rd_enable Input 1-bit Read request from the consumer
wr_data Input 8-bit Data word to be written into the FIFO
full Output 1-bit Asserted when the FIFO cannot accept more data
empty Output 1-bit Asserted when there is no valid data to read
rd_data Output 8-bit Data word read from the FIFO

4.2 Parameters

Parameter Value Description
Data Width 8 bits Width of each data word stored in the FIFO
FIFO Depth 8 words Number of storage locations (addresses 0–7)
Pointer Width 4 bits 3 address bits + 1 extra wrap-detection bit
Clock Single domain All logic clocked on the same rising edge

5. Module Architecture

The design is decomposed into three leaf modules instantiated by a single top-level wrapper. This separation of concerns mirrors standard industry practice: the memory array, the write controller, and the read controller are independently verifiable units.

5.1 Top-Level Module — sync_fifo_top

The top-level module acts as a structural shell. Its responsibilities are:

  • Gate the write enable: wr_enable_fifo = wr_enable & ~full prevents writes when the FIFO is full.
  • Gate the read enable: rd_enable_fifo = rd_enable & ~empty prevents reads when the FIFO is empty.
  • Wire the four-bit pointers between the memory array and the two controller modules.

Because gating happens at the top level, the sub-modules receive clean, pre-qualified enables and do not need to re-check the status flags internally (though the pointer controllers add an additional guard for safety).

Verilog excerpt — internal signal gating:

wire wr_enable_fifo = wr_enable & ~full;
wire rd_enable_fifo = rd_enable & ~empty;

5.2 Memory Array — fifo_memory

This module contains the actual 8-word, 8-bit-wide storage array and the read/write sequencing logic.

Write port: Synchronous. On every rising clock edge where wr_enable is asserted, wr_data is written to mem[wr_ptr[2:0]]. The write address uses only the lower 3 bits of the 4-bit pointer, discarding the wrap bit.

Read port: Also synchronous. On every rising clock edge where rd_enable is asserted (and reset is deasserted), rd_data is updated from mem[rd_ptr[2:0]].

Using synchronous reads introduces exactly one clock cycle of read latency. The read data presented on rd_data corresponds to the memory content at the time of the previous clock edge — a behavior that must be accounted for in the testbench.

Verilog excerpt — write port:

always@(posedge clk) begin
    if (wr_enable) begin
        mem[wr_ptr[2:0]] <= wr_data;
    end
end

5.3 Write Controller — write_logic

The write controller maintains the 4-bit write pointer and derives the full flag.

Pointer update (sequential): On the rising clock edge, if wr_enable is asserted and the FIFO is not full, wr_ptr is incremented by one. The reset condition returns wr_ptr to 4'b0000.

Full detection (combinational): An always@(*) block evaluates whether the next-write location equals the current read position after accounting for the wrap bit.

Verilog excerpt — full detection:

always@(*) begin
    if ( {{~wr_ptr[3], wr_ptr[2:0]}} == rd_ptr ) begin
        full = 1'b1;
    end else begin
        full = 1'b0;
    end
end

5.4 Read Controller — read_logic

The read controller maintains the 4-bit read pointer and derives the empty flag.

Pointer update (sequential): On the rising clock edge, if rd_enable is asserted and the FIFO is not empty, rd_ptr is incremented by one. Reset returns rd_ptr to 4'b0000.

Empty detection (combinational): The FIFO is empty whenever rd_ptr == wr_ptr across all four bits. This covers both the initial reset state and any state in which the consumer has caught up with the producer.

Verilog excerpt — empty detection:

always@(*) begin
    if ( rd_ptr == wr_ptr ) begin
        empty = 1'b1;
    end else begin
        empty = 1'b0;
    end
end

5.5 Module Hierarchy Summary

Module Type Key Signals Clocking
sync_fifo_top Structural (Top) All I/O ports; internal gating wires Passes clk/reset to children
fifo_memory Behavioral (Leaf) mem[0:7], wr_ptr, rd_ptr, wr_data, rd_data Synchronous R/W
write_logic Behavioral (Leaf) wr_ptr[3:0], full Sync pointer; Comb flag
read_logic Behavioral (Leaf) rd_ptr[3:0], empty Sync pointer; Comb flag

6. Verification Strategy

6.1 Testbench Overview

The testbench module sync_fifo_top_tb instantiates the DUT (Device Under Test) and drives its inputs through a structured sequence of test cases. A 10 ns clock period (toggling every 5 ns) is used throughout. All stimulus is applied one clock cycle at a time using @(posedge clk) synchronisation, with a 1 ns post-edge delay (#1) to model realistic setup times.

6.2 Test Cases

Initialization and Reset: A one-cycle reset pulse zeros all inputs, forcing both pointers to 4'b0000 and asserting the empty flag.

Case 3: Write Rate > Read Rate (Net Fill): Writing every cycle while reading every other cycle adds one entry every two cycles, asserting the full flag after 16 cycles.

Case 4: Read Rate > Write Rate (Net Drain): Reading every cycle while writing every other cycle drains the FIFO by one entry every two cycles until empty asserts and halts simulation.

6.3 Waveform Configuration

Pre-configured Visual Tracking: The .wcfg file pre-arranges top-level interfaces and internal pointers (wr_ptr/rd_ptr) to visually confirm flag timing and data integrity without manual setup.

6.4 Observed Results

Test Scenario Observable Condition Expected Flag Behaviour
Reset applied wr_ptr = rd_ptr = 0 empty = 1, full = 0
Write > Read (fill) Occupancy grows by 1 every 2 cycles full asserts after ~16 cycles
FIFO full wr_ptr MSB inverted matches rd_ptr Writes blocked; full = 1
Read > Write (drain) Occupancy falls by 1 every 2 cycles empty asserts after ~16 cycles
FIFO empty wr_ptr == rd_ptr (all bits) Reads blocked; empty = 1

Simulation waveform 1

Simulation waveform 2

7. Conclusion

This project delivers a complete, simulation-verified synchronous FIFO buffer in Verilog HDL. The design cleanly separates the memory array from the pointer control logic, making each sub-module independently readable and testable. The extra-bit pointer technique provides a simple and provably correct mechanism for distinguishing full from empty without any additional logic beyond a single bit comparison.

The testbench exercises the two most demanding operating regimes — sustained write pressure leading to a full condition, and sustained read pressure draining the buffer to empty — and confirms that the status flags respond correctly in both scenarios. Together, the design and testbench form a solid foundation for understanding circular buffer architecture, synchronous design methodology, and HDL-based verification practice.

The logical next milestone is extending this work to an asynchronous FIFO with Gray-code pointer synchronization, as outlined in Section 8.1 — a natural progression that reuses every module developed here.

Report Information

Explore More Projects

View All 2026 Projects