When write and read pointers are equal, the FIFO is either empty or full. That ambiguity is the entire pointer design problem. Solve it on one clock first.
Address = low ADDR_W bits of the pointer. After the last location, the pointer wraps. Occupancy is the number of unread beats, not “how far the pointer is from 0”.
Occupied slots sit strictly between rd and wr in ring order.
| Method | Empty | Full | Notes |
|---|---|---|---|
| Count register | count == 0 | count == DEPTH | Increment on write-only, decrement on read-only, hold on both. Width = $clog2(DEPTH)+1. |
| Extra MSB (n+1 bit pointers) | wptr == rptr | wptr == {~rptr[MSB], rptr[MSB-1:0]} | Address bits equal, wrap bits differ → full. Industry default for async FIFOs too. |
| Leave one slot empty | wptr == rptr | next write would equal rptr | Usable depth = DEPTH−1. Simple, wastes one entry. |
empty = (wptr == rptr); full = (wptr == rptr); is always a bug. Equal pointers cannot mean both.
ADDR_W = 2. Pointers are 3 bits. Address = ptr[1:0]. Wrap bit = ptr[2].
| Event | wptr | rptr | addr equal? | MSB differ? | Flag |
|---|---|---|---|---|---|
| Reset | 000 | 000 | yes | no | EMPTY |
| 4 writes | 100 | 000 | yes (addr 00) | yes | FULL |
| 1 read | 100 | 001 | no | — | neither |
| 4 more writes (wrap) | 000 after 8 writes total vs 4 reads… | track wrap bit |
After four writes from reset: wptr=100, rptr=000. Addresses match (00), MSBs differ → full, occupancy = 4. After four reads: both 100 → equal including MSB → empty.
// DEPTH power of two. PTR_W = ADDR_W + 1
wire empty = (wptr == rptr);
wire full = (wptr == {~rptr[PTR_W-1], rptr[PTR_W-2:0]});
Count method: count holds. Pointer method: both pointers increment; difference stays the same. If empty, a same-cycle write+read is a design choice (FWFT vs standard): this course uses standard — you cannot read while empty, so rd_fire is 0 even if wr_en=1 on that cycle.
Standard (this course): rdata is a flop loaded on rd_fire. After reset, empty=1 and rdata is don’t-care until the first successful read. Downstream must pulse rd_en to obtain each beat, then wait a cycle.
FWFT / show-ahead: the RAM read port is combinational or a lookahead pointer, so rdata presents the next byte whenever the FIFO is not empty. empty (often named !data_valid) falls as soon as the first write is visible. FWFT is nicer for ready/valid but creates a combinational path from write to read data in a sync FIFO, and in an async FIFO it needs a small output register stage in the read domain. Do not mix the two contracts in one testbench.
A saturating counter of occupancy looks easy: +1 on write-only, −1 on read-only, hold on both. Width must be $clog2(DEPTH)+1 so DEPTH itself is representable. Bugs: updating the count from ungated wr_en; using a WIDTH that wraps at DEPTH (so full looks like empty); computing full/empty from the count in the same cycle as the pointer update without a consistent blocking assignment order. Extra-MSB pointers avoid a third sequential element that can disagree with the RAM addresses.
Async FIFOs almost never share a count register across clocks — that would be another multi-bit CDC. Occupancy in one domain is reconstructed from local binary minus converted synced Gray.
With only ADDR_W bits, write and read addresses match both when the FIFO is empty and when it has wrapped and is full. You add a wrap/MSB bit or a separate occupancy counter to distinguish those two states.
empty when the entire (ADDR_W+1)-bit pointers are equal. full when the address bits match and the wrap bits differ: wptr == {~rptr[MSB], rptr[MSB-1:0]}.
Yes on a sync FIFO when it is neither empty nor full: occupancy holds. This course’s standard FIFO still forbids a read while empty, even if a write is happening on that cycle.
That method is simple (full when the next write address would equal the read address) but wastes one location. Extra-MSB uses the whole DEPTH and is what Gray async FIFOs generalize.
wr_fire = wr_en && !full, rd_fire = rd_en && !empty.