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.
- Cycle 0
Edge 2: the first word, 0xa0, goes into slot 00 and the write pointer moves to 01.
- 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
emptyrises. - Cycle 4
Edge 6:
fullis 0, so the fifth push, 0xa4, is accepted into slot 00, over 0xa0. - 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.
- Write
An accepted write stores the word at the slot the low bits of
wptr_qname, then adds one to the pointer. - Read
An accepted read takes the word at
rptr_qintoread_dataat the edge and adds one to the read pointer.read_validis 1 after that edge. - 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.
- Cycle 3
Edge 5: the write pointer reaches 100. Its address bits, 00, equal the read pointer's, but its wrap bit is 1:
fullrises. - Cycle 4
Edge 6: the fifth push is refused, so the write pointer stays at 100 and nothing is overwritten.
- Cycle 8
Edge 10: the fourth read brings the read pointer to 100 as well. Equal pointers:
empty. - 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
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.
A pop when empty is refused
The read pointer does not move and
read_validstays 0 after the edge.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.
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
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| write_en | input | 1 | a write is accepted when write_en and not full |
| write_data | input | [WIDTH-1:0] | the word to write |
| read_en | input | 1 | a read is accepted when read_en and not empty |
| read_data | output | [WIDTH-1:0] | the word comes out in read_data after the accepting edge |
| read_valid | output | 1 | 1 for the cycle after the accepting edge |
| full | output | 1 | no room; read before the edge |
| empty | output | 1 | no word; read before the edge |
- Reset
- clears the pointers,
read_validandread_data; the memory keeps its contents - Parameters
DEPTHwords, a power of two, 16 by default;WIDTHbits, 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
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.
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.
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.
Synthesize the design to measure its cells and inspect the mapped gates.
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=1Original 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
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.