The synchronous FIFO: pointers, full, empty, the extra bit

Build a synchronous FIFO from a memory and two pointers, tell full from empty with one extra pointer bit, and handle a push and a pop in the same cycle.

Lesson
R5.2
Time
35 min
Runs on
Your browser
Check
5 questions

A 4-deep FIFO took four words and then reported empty. Its pointers were as wide as the address, so after four pushes the write pointer wrapped to equal the read pointer. The fifth push overwrote the first word, and the consumer read one word of five.

Four pushes, and the FIFO says emptyEdges 2 to 7 of the lab bench, with the shipped design at DEPTH 4. The pointers are 2 bits, the width of the address.
012345clkwrite_enread_enwptr0110110001rptr0001fullempty
  1. Cycle 0

    Edge 2: the first word, 0xa0, goes into slot 00 and the write pointer moves to 01.

  2. Cycle 3

    Edge 5: the fourth word fills slot 11 and the write pointer wraps to 00. Now both pointers are 00, which is what empty looks like, and empty rises.

  3. Cycle 4

    Edge 6: full is 0, so the fifth push, 0xa4, is accepted into slot 00, over 0xa0.

  4. Cycle 5

    Edge 7: the consumer reads one word, 0xa4, and the FIFO is empty again. Words 0xa0 to 0xa3 are lost.

Measured on the lab bench. Equal pointers mean empty and full alike when the pointers are no wider than the address.

A memory with two pointers

A FIFO (first in, first out) is a memory (R5.1) with two pointers. A write goes to mem[wptr] and moves wptr on; a read comes from mem[rptr] and moves rptr on. The words held are the difference, wptr - rptr.

The FIFO: a memory, two pointers and two flagsArrows carry data and control. The flags come from the pointers alone, and both accept decisions use the flags from before the edge.
The FIFO: a memory, two pointers and two flagsArrows carry data and control. The flags come from the pointers alone, and both accept decisions use the flags from before the edge. component Producer write_en, write_data register Write pointer wptr_q: AW + 1 bits queue Memory DEPTH words register Read pointer rptr_q: AW + 1 bits logic Flags full, empty component Consumer read_en; read_data,read_valid accepted writeslotslotword, one edge laterwptrrptr
  1. Write

    An accepted write stores the word at the slot the low bits of wptr_q name, then adds one to the pointer.

  2. Read

    An accepted read takes the word at rptr_q into read_data at the edge and adds one to the read pointer. read_valid is 1 after that edge.

  3. Flags

    Equal pointers mean empty. Equal low bits with different top bits mean the writer is one lap ahead: full.

Read the diagram as text
  • Producer: write_en, write_data
  • Write pointer: wptr_q: AW + 1 bits
  • Memory: DEPTH words
  • Read pointer: rptr_q: AW + 1 bits
  • Flags: full, empty
  • Consumer: read_en; read_data, read_valid
  • Producer → Write pointer: accepted write
  • Write pointer → Memory: slot
  • Read pointer → Memory: slot
  • Memory → Consumer: word, one edge later
  • Write pointer → Flags: wptr
  • Read pointer → Flags: rptr

The top pointer bit counts laps. It never addresses the memory.

With 2-bit pointers on a 4-deep FIFO, the difference can only be 0 to 3. Four words wrap it back to 0. That is the hook: four slots need five states, empty plus one to four words.

The extra bit, and the count register

The classic cure adds one bit to each pointer. The low bits still address the memory, and the top bit flips each time a pointer wraps. Empty is equal pointers. Full is the same address with a different top bit: the writer is exactly one lap ahead.

The same bench with 3-bit pointersEdges 2 to 13 of the lab bench after the fix, DEPTH 4. The top pointer bit is the wrap bit.
01234567891011clkwrite_enread_enwptr001010011100101110rptr000001010011100101110fullempty
  1. Cycle 3

    Edge 5: the write pointer reaches 100. Its address bits, 00, equal the read pointer's, but its wrap bit is 1: full rises.

  2. Cycle 4

    Edge 6: the fifth push is refused, so the write pointer stays at 100 and nothing is overwritten.

  3. Cycle 8

    Edge 10: the fourth read brings the read pointer to 100 as well. Equal pointers: empty.

  4. Cycle 10

    Edge 12: a push and a pop in one cycle. Both pointers move, so the FIFO still holds one word.

Measured on the lab bench with the fix. The reads return 0xa0, 0xa1, 0xa2 and 0xa3 in order.

Flags from the pointer difference

logic [AW:0] used;
assign used  = wptr_q - rptr_q;
assign empty = (used == 0);
assign full  = (used == (AW+1)'(DEPTH));

Correct, and it gives the occupancy. Yosys 0.70: 453 cells, 12 levels of logic.

Flags from the wrap bit

assign empty = (wptr_q == rptr_q);
assign full  = (wptr_q[AW] != rptr_q[AW]) &&
               (wptr_q[AW-1:0] == rptr_q[AW-1:0]);

Correct, with no subtractor. Yosys 0.70: 413 cells, 7 levels of logic.

The other cure is a count register: pointers as wide as the address, plus a register of AW + 1 bits. It adds one per accepted write and subtracts one per accepted read. It holds the occupancy outright and allows any DEPTH, but it is one more register to keep right. Measured at 16 x 8: 451 cells, 150 flops, 13 levels of logic.

The rules the brief states

  1. A push when full is refused

    The write pointer does not move and no word is written. The producer must hold its word or lose it.

  2. A pop when empty is refused

    The read pointer does not move and read_valid stays 0 after the edge.

  3. A push and a pop together are both honoured

    Both pointers move, so the count stays. At full only the pop is accepted, and at empty only the push, because both decisions use the flags from before the edge.

  4. Reset clears the pointers, not the memory

    Equal pointers make the FIFO empty, and a word is never read before it is written. Clearing 128 data flops would add a reset to each for no change in behaviour.

This FIFO answers an accepted read one edge later, with read_valid beside the word. Another style shows the head word on read_data while the FIFO is not empty, before any read: first-word fall-through. It saves a cycle and puts the memory's read mux on the output path.

The lab: add the extra bit

The FIFO brief.
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
write_eninput1a write is accepted when write_en and not full
write_datainput[WIDTH-1:0]the word to write
read_eninput1a read is accepted when read_en and not empty
read_dataoutput[WIDTH-1:0]the word comes out in read_data after the accepting edge
read_validoutput11 for the cycle after the accepting edge
fulloutput1no room; read before the edge
emptyoutput1no word; read before the edge
Reset
clears the pointers, read_valid and read_data; the memory keeps its contents
Parameters
DEPTH words, a power of two, 16 by default; WIDTH bits, 8 by default. The bench builds a 4-deep FIFO
Corner cases
a push when full; a pop when empty; a push and a pop together; pointers that wrap

Run the lab. Each line shows the offers (w, r), both pointers in binary and the flags after the edge. The shipped design says empty at edge 5 and loses four words. Press Lint: it reports that used gives 4 bits where the comparison expects 5.

Fix it by giving both pointers and used one more bit: [AW:0] for [AW-1:0]. Run again; the reads return all four words in order. Synthesize both: the shipped design is 431 cells, 145 flops and 8 levels of logic; the fix is 453 cells, 147 flops and 12 levels. The two new flops are the wrap bits.

Then replace the flags with the wrap-bit version of the compare and synthesize: 413 cells and 7 levels, still 147 flops. Of these, 128 hold data, 10 are pointers, 1 is read_valid and 8 are read_data.

Asked in interviews
  1. How do you tell full from empty?

    Give each pointer one bit more than the address. Equal pointers mean empty; equal address bits with different top bits mean full. A count register is the other way.

  2. What does the extra pointer bit do?

    It counts wraps. Without it, a FIFO with all slots used and an empty one both have equal pointers. With it, the writer being one lap ahead is visible.

  3. What happens on a simultaneous push and pop?

    Both are accepted when the FIFO is neither full nor empty, and the occupancy stays. At full only the pop is taken, at empty only the push, because both use the flags from before the edge.

Try it

After four pushes both pointers are 00 and empty is 1, so the fifth push is taken and overwrites slot 00. One read returns 0xa4; the next three are refused as empty. The last lines push 0xb0, push 0xb1 while popping 0xb0, then pop 0xb1.

With the extra bit, full is 1 at edges 5 and 6, and the fifth push is refused. The reads at edges 7 to 10 return 0xa0, 0xa1, 0xa2 and 0xa3. The pointers end at 110: the write pointer has passed the wrap once.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e2 w- wptr=01 rptr=00 full=0 empty=0 e3 w- wptr=10 rptr=00 full=0 empty=0 e4 w- wptr=11 rptr=00 full=0 empty=0 e5 w- wptr=00 rptr=00 full=0 empty=1 e6 w- wptr=01 rptr=00 full=0 empty=0 e7 -r wptr=01 rptr=01 full=0 empty=1 read 0xa4 e8 -r wptr=01 rptr=01 full=0 empty=1 e9 -r wptr=01 rptr=01 full=0 empty=1 e10 -r wptr=01 rptr=01 full=0 empty=1 e11 w- wptr=10 rptr=01 full=0 empty=0 e12 wr wptr=11 rptr=10 full=0 empty=0 read 0xb0 e13 -r wptr=11 rptr=11 full=0 empty=1 read 0xb1 e14 -- wptr=11 rptr=11 full=0 empty=1

Original design · 431 cells · 145 flip-flops · 0 latches · 8 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic       write_en = 0, read_en = 0;
  logic [7:0] write_data = 0, read_data;
  logic       read_valid, full, empty;
  bit show = 0;
  // One cycle of offers: w writes d, r reads. The line after the edge shows what the FIFO did.
  task automatic cycle(input bit w, input bit r, input logic [7:0] d = 0);
    write_en = w; read_en = r; write_data = d; tick(); write_en = 0; read_en = 0;
  endtask
  sync_fifo #(.DEPTH(4)) dut (.clk(clk), .rst_n(rst_n), .write_en(write_en), .write_data(write_data),
                .read_en(read_en), .read_data(read_data), .read_valid(read_valid), .full(full), .empty(empty));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (show) begin
      $write("e%0d %s%s wptr=%b rptr=%b full=%b empty=%b", edge_n, write_en ? "w" : "-", read_en ? "r" : "-",
             dut.wptr_q, dut.rptr_q, full, empty);
      if (read_valid) $write(" read 0x%h", read_data);
      $display("");
    end
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    show = 1;
    cycle(1, 0, 8'ha0); cycle(1, 0, 8'ha1); cycle(1, 0, 8'ha2); cycle(1, 0, 8'ha3);   // fill: four words
    cycle(1, 0, 8'ha4);                                                               // a fifth push
    repeat (4) cycle(0, 1);                                                           // drain
    cycle(1, 0, 8'hb0);
    cycle(1, 1, 8'hb1);                                                               // push and pop together
    cycle(0, 1);
    cycle(0, 0);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe fixed FIFO is built 8 deep. After reset the bench pushes five words, pops two, then pushes and pops in the same cycle. It prints used, the pointer difference. What does it print?
// sync_fifo: a synchronous FIFO of DEPTH words (a power of two) of WIDTH bits.
// A write is accepted at an edge where write_en is 1 and the FIFO is not full; a read where
// read_en is 1 and it is not empty. Both decisions use the flags from before the edge. An
// accepted read returns its word in read_data after that edge, with read_valid 1 for one cycle.
module sync_fifo #(parameter int DEPTH = 16, parameter int WIDTH = 8) (
  input  logic             clk,
  input  logic             rst_n,         // asynchronous, active-low
  input  logic             write_en,
  input  logic [WIDTH-1:0] write_data,
  input  logic             read_en,
  output logic [WIDTH-1:0] read_data,
  output logic             read_valid,
  output logic             full,
  output logic             empty
);
  localparam int AW = $clog2(DEPTH);      // address bits: 4 for 16 words

  logic [WIDTH-1:0] mem [DEPTH];          // the storage (R5.1): no reset
  logic [AW:0]      wptr_q, rptr_q;       // the next slot to write, the next slot to read
  logic [AW:0]      used;                 // words held: the pointer difference

  assign used  = wptr_q - rptr_q;
  assign empty = (used == 0);
  assign full  = (used == (AW+1)'(DEPTH));   // DEPTH words: AW+1 bits

  wire take_w = write_en && !full;
  wire take_r = read_en && !empty;

  always_ff @(posedge clk)
    if (take_w) mem[wptr_q[AW-1:0]] <= write_data;

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      wptr_q <= '0; rptr_q <= '0; read_valid <= 1'b0; read_data <= '0;
    end else begin
      if (take_w) wptr_q <= wptr_q + 1'b1;
      if (take_r) begin
        rptr_q    <= rptr_q + 1'b1;
        read_data <= mem[rptr_q[AW-1:0]];
      end
      read_valid <= take_r;
    end
endmodule

module tb;
  logic clk = 0, rst_n = 1, write_en = 0, read_en = 0;
  logic [7:0] write_data = 0, read_data;
  logic read_valid, full, empty;
  sync_fifo #(.DEPTH(8)) dut (.*);
  task automatic cycle(input bit w, input bit r);
    write_en = w; read_en = r; write_data = write_data + 8'd1;
    #1 clk = 1; #1 clk = 0;
  endtask
  initial begin
    #1 rst_n = 0; #1 rst_n = 1;
    repeat (5) cycle(1, 0);
    repeat (2) cycle(0, 1);
    cycle(1, 1);
    $display("used=%0d", dut.used);
    $finish(0);
  end
endmodule
Spot the bugEach option is the full flag of the FIFO with AW + 1 bit pointers (empty is wptr_q == rptr_q). The bench fills a 4-deep FIFO, pushes once more, and drains it. Which one fails?
ConceptWhy does a 16-deep FIFO with pointer-difference flags need 5-bit pointers when 4 bits address the memory?
ConceptReset clears the pointers, read_valid and read_data, but not the memory. Why is that enough?
Read the waveformThis is the fixed FIFO, DEPTH 4, edges 2 to 7. At which edge does the FIFO first hold four words, and what tells you?
The wrap bit
012345wptr001010011100rptr000001

Pointers after edges 2 to 7, left to right.

Remember

  • A FIFO is a memory with a write pointer and a read pointer; the words held are their difference.
  • Pointers as wide as the address cannot tell full from empty; one extra bit counts laps: equal pointers mean empty, equal address with different wrap bits means full.
  • A count register is the other cure: it holds the occupancy and allows any depth, at the cost of another register to update.
  • Both accept decisions use the flags from before the edge: a push at full and a pop at empty are refused, a push and a pop together keep the count.
  • Reset clears the pointers and the read outputs, not the memory.