FIFO design is not uncommon. We can find much information, including publicly available codes. Do you think FIFO design still matters in 2026? Yes, of course. FIFO is still important in the modern FPGA-based solutions that demand efficient hardware design in both performance and power. Furthermore, this is still one of the crucial design steps for those who are willing to become RTL/Silicon designers/engineers in the FPGA domain. I get too many questions about design through Verilog coding, and this is the perfect moment to include here. This is just a beginning.
FIFO stands for First In First Out, an unnoticed and hidden but still important part of the streaming system for flow control. Whether it is a trivial design or a high-speed Ethernet design, a High-Performance Video System, or another computational system, this little component prevents you from stream failure.
If you look at the types of FIFOs, there are Synchronous and Asynchronous FIFOs.
# Synchronous -- same clock domain -- very useful for the design running under the same clock domain
# Asynchronous -- clock cross domain -- very useful for clock-crossing hardware design
Here, I am presenting the design of a synchronous FIFO through principal understanding, coding, behavioral simulation, and verification using RTL design flow. I am going to use Verilog HDL with the AMD Vivado Tool.
Before starting Verilog coding, we need to have a basic understanding of the working mechanism of a FIFO operation.
FIFO is used as a temporary buffer, commonly used for implementing a line buffer. The major function is to receive valid packets from the master device, queue them until the slave device is ready to process them. FIFO helps to prevent data loss, resolving streaming backpressure issues.
AXI4-stream, AXI-MM, Ethernet, and Video/Audio Pipeline have FIFO underneath the interface, continuously handling the packet flow.
The OSI Layer
The FIFO works in both the Data Link Layer and the Physical Layer for both flow control and transmission of the data in the physical media.
PrincipalWithout understanding the underlying components of FIFO, we will not be able to design and verify it. We will go through the following crucial components of FIFO. The FIFO components are already shown in the picture above.
Memory Element and FIFO Pointer
These are two crucial parts of any FIFO. The memory element is nothing but just an array element to hold/buffer the data. The length of the array is the depth of the FIFO. In most cases, the depth of the FIFO is set according to the power-of-2 rule. That is, 1024, 2048, 4096, 8192, and so on. When you synthesize a memory element, the synthesizer generally infers BRAM, or you can directly instantiate BRAM through a BRAM primitive template. However, if the memory read and write operations are not synchronous and do not obey the BRAM read-write pattern, the memory elements are inferred in the LUT. This may result in a huge consumption of LUT elements if proper logic resource planning is not done.
The FIFO Pointer is nothing but simply an index to the array element. This tracks the memory to be read or written. As there are reading and writing operations, there will be a separate read pointer and write pointer, respectively. These independently track the read and write data. The size/width of the pointer is also defined according to the power of 2 and expressed in "bits". For example, if the FIFO depth is set as 1024, the width of the pointer is determined as pointerwidth = log2(1024) = 10-bit. That is, a pointer can track up to 1024 data in the FIFO.
Once the pointer value reaches maximum depth, the pointer should roll/reset back to the initial value with the indication of wrapping of the pointer. That's why, read or write pointer has one extra bit in the MSB part. For example,
For 1024 FIFO depth,
Actual Pointer Width = log2(1024) = 10-bit
Total pointerwidth = 1 MSB for wrap-bit + Actual Pointer Width = 11-bitThis MSB must be toggled whenever the pointer rolls back to the initial value after reaching the maximum FIFO depth value. This is important for correctly detecting FIFO full and empty conditions.
FIFO Read and Write Enable
These are other components of a FIFO. These signals are responsible for whether to start or stop FIFO read or write operations. There are two independent enable signals, they are "read_enable" and "write_enable", to control read and write operations separately. If the "enable" signal is asserted, the read or write operation is started; otherwise is stopped. Meanwhile, the read or write pointer is also incremented during the "enable" HIGH state to track the index of the memory element.
FIFO Full and Empty
These are the important parts of the FIFO, which play a significant role in controlling the flow in reality. These must not be missed or forgotten while designing any FIFO. As you might have noticed, FIFO is a temporary buffer. Obviously, the buffer can be "Full" or "Empty" at any time during the operation. The FIFO should be able to report its buffer status. The status information is utilized by FIFO's master and slave to control the data flow precisely.
FIFO full and empty conditions detection is one of the charming parts of any FIFO design. The conditions can be determined based on the current value of read and write pointers.
FIFO Empty => Read Pointer == Write Pointer (Same Wrap Bits)
FIFO Full => Read Pointer == Next Write Pointer (Different Wrap Bits)The term "Wrap Bit" here has great significance. Otherwise, a FIFO ambiguity situation will occur. The wrap bit is used to track the wrapping of the pointers for both reading and writing pointers. These wrap bits accurately help to determine the FIFO full and empty conditions.
In both full and empty conditions, both reading and writing pointers are equal to each other, but the wrap bit plays a significant role in accurately determining the conditions.
At this condition (Read Pointer == Write Pointer), whenever both read and write pointers have the same wrap bit value, then it is the FIFO Empty Condition; otherwise, if wrap bit values are different, then it is the FIFO Full condition.
We can conveniently understand the conditions by two cases:
Case 1: No read operation, only write operation, wrap bit 0
At the initial condition, the read pointer and write pointer values are 0. There exists a FIFO Empty condition, as both read and write pointers have the same addresses and wrap bit values.
When the writing operation begins, the pointer value increases. At this point, both read and write pointers' values are NOT equal. This gives FIFO Not Empty and Not FULL conditions. In the meantime, the Next Write Pointer and Wrap Bit values are also calculated aside.
Whenever the write-pointer reaches the maximum FIFO depth value.
Let's say,
Reading pointer = 0
Read pointer wrap bit = 0
Writing pointer = 1023
Write Pointer Wrap bit = 0,
These results,
next writing pointer = 0 (roll back to initial value)
wrap bit = 1 (indicating that the write pointer is rolled back or wrapped back to the initial value).
We notice here that the read pointer and next upcoming writing pointer value become the same, while the wrap bits between the read and write pointers are also different. This gives the FIFO FULL condition. If the writing operation is continued beyond this condition, the data will be overwritten. So, the FULL condition will have to be leveraged to prevent further write operations.
Case 2: Read operation with write operation wrapped
In this case, we are assuming that the write operation is wrapped with a FIFO FULL condition and performing the reading operation.
At this moment,
Reading pointer = 0
Read pointer wrap bit = 0
Writing pointer value = 0
Write Pointer Wrap bit = 1 (Write operation is wrapped),
In the reading operation, the read pointer is incremented up to the maximum FIFO depth value, and it will be wrapped.
We will have the following condition,
Reading pointer = 0
Read pointer wrap bit = 1 (Read Operation is Wrapped)
Writing pointer value = 0
Write Pointer Wrap bit = 1 (Write operation is wrapped),
Here, the pointer value of both read and write operations is zero with the same wrap-bit value. This is the condition for the FIFO Empty condition. The reading operation should not be proceeded further. Otherwise, an unknown data reading condition occurs. So, the slave must utilize the Empty condition to prevent further data reading after this condition.
Other cases:
In the above cases, we considered the write operation and then the read operation separately to clearly state the FIFO FULL and EMPTY conditions. In real cases, the reading and writing operations happen dynamically. However, in any case, detecting the FIFO FULL and EMPTY conditions remains the same.
Clock and Reset
As we are designing a synchronous FIFO, a single clock is used to operate both read and write operations synchronously. So, we don't need to have CDC logic. Similarly, a synchronous reset is used to reset the FIFO state.
CodingOnce we understand the FIFO working mechanism, we can write Verilog code following the same principle explicitly to define the functional behavior of a synchronous FIFO.
The following picture illustrates a simplified FIFO state for write and read operations.
#1 Port Declaration
We create a FIFO module starting from the RTL port declaration, as attached below. As we are designing a synchronous FIFO, there will be a single clock and reset pins. Both read and write operations happen in the same clock domain.
module sfifo #(
parameter fifo_depth = 8;
)(
input clk, //input clock
input rstn, //active low synchronous reset
input wr_en, //fifo write enable
input rd_en, //fifo read enable
input [7:0] data_in, //fifo data input
output [7:0] data_out, //fifo data output
output fifo_empty, //fifo empty status
output fifo_full //fifo full status
);
//write operation
//read operation
//status
endmodule#2 FIFO Memory Element is declared by creating a one-dimensional array. The array size is determined by the "fifo_depth" value.
//declare memory element to hold 8-bit data
reg [7:0] Mem[0:fifo_depth-1];#3 FIFO Write Pointer Declaration. As we discussed in the principal section, we set the pointer value by adding one extra bit for wrap-indication.
localparam wr_ptr_width = $clog2(fifo_depth) + 1; //power of 2 rule with 1 wrap-bit#4 FIFO Write Pointer Increment. We define sequential logic to calculate the current and next write pointer value whenever a write operation is enabled. Meanwhile, we must also make sure FIFO is not full during the write operation. Otherwise, we get data loss.
always @(posedge clk)
begin
if(~rstn) begin
wr_ptr<=0;
nxt_wr_ptr<=1; //making this value 1 clock a head of actual write operation.
end
else if(wr_en && !fifo_full)
begin
wr_ptr <= wr_ptr + 1;
nxt_wr_ptr <= nxt_wr_ptr + 1;
end
end#5 FIFO Write Operation. Whenever a write operation is enabled, we write incoming data into the Memory element, tracked by write_pointer. Note that we are only using the write pointer value (NOT including the wrap-bit).
always @(posedge clk ) begin
if(wr_en) Mem[wr_ptr[wr_ptr_width-2:0]] <= data_in;
end#6 In a similar manner, we can write Verilog code for the read operation, similar to the write operation.
//read pointer declaration, including wrap-bit
localparam rd_ptr_width = $clog2(fifo_depth) + 1;
//read pointer increment
reg [rd_ptr_width-1:0] rd_ptr;
always @(posedge clk)
begin
if(~rstn) rd_ptr<=0;
else if(rd_en && !fifo_empty) rd_ptr <= rd_ptr + 1;
end
//data reading from fifo element
reg [7:0] fifo_dat;
always @(posedge clk ) begin
if(rd_en) fifo_dat <= Mem[rd_ptr[rd_ptr_width-2:0]];
end#7 FIFO Status. As we discussed in the principal section, we detect FIFO full and empty conditions in the following manner. The combinational logic is used to quickly compare the pointer status.
//wrap bit comparison
wire wrap_bit_empty = (rd_ptr[rd_ptr_width-1] == wr_ptr[wr_ptr_width-1]);
wire wrap_bit_full = (rd_ptr[rd_ptr_width-1] != nxt_wr_ptr[wr_ptr_width-1]);
//pointer comparison
wire empty_condtn = (rd_ptr[rd_ptr_width-2:0] == wr_ptr[wr_ptr_width-2:0]);
wire full_condtn = (rd_ptr[rd_ptr_width-2:0] == nxt_wr_ptr[wr_ptr_width-2:0]);
//status
assign fifo_empty = ( wrap_bit_empty && empty_condtn)? 1:0;
assign fifo_full = ( wrap_bit_full && full_condtn)? 1:0;#8 We wrap up the above code into a single Verilog code, create a Vivado project, and head for simulation and verification.
SimulationAs we already discussed different FIFO cases, here we will evaluate the cases through behavioral simulations.
Case 1: Write Operation Only
You can check the first portion of the simulation waveform below. We initially set the "wr_en" signal only. It drives the write operation. This can be monitored in the following waveform. The wr_ptr is incremented, and data is being written into the memory element. As we set the FIFO depth value to be 8, the write operation continues for 8 clock cycles up to the fifo_full condition.
case2: read-only after write operation
In this case, we are assuming that there is no write operation once the full condition occurs, and then we are asserting the "rd_en" signal only for this case. It drives the read operation that increments the read pointer and reads the data from the memory element. The read operation lasts until the FIFO is empty. These are clearly illustrated in the second portion of the simulation waveform below.
Case 3: Reality
In previous cases, we considered only write operations and read operations for illustrative purposes. However, in reality, these operations are actively controlled by slave and master devices, considering the full and empty conditions. Most high-speed interfaces and pipelines have a FIFO to actively control the packet/data flow without having a loss of data.
For example, in the AXI4Stream protocol, the TVALID signal of the master device is responsible for driving the write operation of the FIFO. In the meantime, the TREADY signal is also important for the master for the valid data transfer. This signal brings the FIFO's full information to the master device. If FIFO is full, the tready signal is deasserted, and the master holds the transfer.
From the slave AXI4Stream protocol perspective, the FIFO itself becomes the master for the succeeding pipeline. The FIFO empty status is responsible for driving the TVLAID signal, while TREADY from the slave device becomes responsible for driving the FIFO read operation. So, valid data transfer occurs when both Tvalid and Tready signals are at the HIGH states. Meaning, through TREADY signaling, the slave is driving the FIFO read operation. In the meantime, through TVALID, it is signaling that FIFO is NOT empty. Data is available for read operation.
DemonstrationFor the demonstration, I have taken an Xilinx TPG-based simulation design, where I pass a TPG frame into the synchronous FIFO, and then I capture it.
The following frame is obtained after capturing the TPG frame through the synchronous FIFO.
This confirms our synchronous FIFO is functionally working.
You can find resources here. You need to execute the.tcl file to create a Vivado project and then proceed with the simulation.
Conclusion/What's Next?
In this way, this article provided you with a clear foundation of a simple, yet elegant synchronous FIFO design through Verilog coding and simulation. With this basis, you can build your own FIFO further. In the next session, we will go over various FIFO types and their features.
Don't forget to follow, like, share, and comment.








Comments