Overflow is a write that commits when the FIFO already holds DEPTH unread beats. Underflow is a read that commits when occupancy is 0. Both are functional bugs, not βretry laterβ. A correct FIFO may stall (full/empty); it must never overwrite or pop garbage.
Overflow: wr_fire while occupancy is already DEPTH β new wdata overwrites an unread location (or the write pointer laps the read pointer). Lost data, often silent.
Underflow: rd_fire while occupancy is 0 β rdata is stale, X, or another beatβs leftover. Downstream FSM consumes a fake packet.
| Overflow | Underflow | |
|---|---|---|
| When | Write while full | Read while empty |
| Memory | Unread word destroyed | No valid word at rptr |
| Pointers | wptr advances anyway β occupancy looks legal, data is not | rptr advances past wptr β later βvalidβ reads are wrong |
| Typical cause | Producer ignores full; combo-loop flag late; Gray CDC too optimistic | Consumer ignores empty; FWFT vs standard mismatch; empty registered one cycle late without gating |
A blocked write (wr_en=1 and full=1 and wr_fire=0) is healthy. Occupancy stays at DEPTH. Overflow is only if the RAM write and pointer increment still happen.
wire wr_fire = wr_en & ~full; // no RAM write, no wptr++ if full wire rd_fire = rd_en & ~empty; // no rptr++, no rdata capture if empty
Upstream must treat full as βdo not present a new beatβ (or keep wdata stable and wait). Downstream must treat empty as βrdata is not a new beatβ.
The 2FF on Gray pointers hides recent opposite-side traffic. That must make flags pessimistic:
full while true occupancy is still < DEPTH (wasted slots, never overflow).empty while RAM still holds unread writes that have not synced yet (extra latency, never underflow).If you compared live binary pointers across clocks, you could think the FIFO is not full when it is β that is how CDC invents overflow.
A read completed on rd_clk. Until that Gray pointer finishes 2FF on wr_clk, the writer still believes the FIFO is full and blocks wr_fire. That is stall, same as lesson 2.1 β never a wrap of the write pointer into live data.
Golden check in any testbench: a software queue of the same DEPTH. Every wr_fire push, every rd_fire pop. If the model queue overflows or underflows, the RTL is wrong. Lesson 5.1βs lab tracks blocked writes/reads; those counters must rise instead of the golden queue breaking.
// Sync FIFO (one clock). Track occupancy in the TB, not with a DUT count // if you already use extra-MSB pointers. int occ; always @(posedge clk or negedge rst_n) begin if (!rst_n) occ <= 0; else occ <= occ + wr_fire - rd_fire; end assert property (@(posedge clk) disable iff (!rst_n) !(wr_fire && (occ == DEPTH))); // overflow assert property (@(posedge clk) disable iff (!rst_n) !(rd_fire && (occ == 0))); // underflow assert property (@(posedge clk) disable iff (!rst_n) occ >= 0 && occ <= DEPTH); // Dual-clock: keep two occupancy views, or a mailbox in the TB. // Never sample both pointers in one always @(posedge wr_clk) without Gray.
full never 1, but occupancy hits DEPTH β pointer/flag formula bug (wrong MSB invert on Gray).empty 0 at reset β pointers not cleared together.rdata on rd_en instead of on a valid pop.Capture wr_en, full, wr_fire, rd_en, empty, rd_fire, and both pointers. If wr_en is high while full is high and wptr still increments, the gate is broken. If pointers wrap and RAM contents look rotated, you overflowed. If the reader gets old beats after empty pulsed, you underflowed or used FWFT timing in a standard-FIFO TB.
On async FIFOs also dump the 2FF outputs. If full is 0 but converted occupancy from live pointers (illegal in RTL, OK in a well-timed TB model that cheats with a hierarchical force) says DEPTH, you have a CDC compare bug.
AXI-Stream: if the master keeps tvalid high when tready is low, the master is protocol-illegal; the FIFO should not have sampled. If your wrapper samples tdata on tvalid alone, you built overflow into the wrapper even though the FIFO core is correct. Always AND with tready (i.e. !full).
A write commit (wr_fire=1) while occupancy is already DEPTH. Unread data is overwritten. It is a bug. Stall (wr_en && full && !wr_fire) is the correct behavior.
A read commit while occupancy is 0, returning stale or X data. Stall on empty is correct.
If you compare live binary pointers across clocks, reconvergence can clear full while the RAM is still full. Gray + conservative 2FF flags exist to prevent that class of overflow.
A golden queue of DEPTH, SVA that forbids wr_fire at occ==DEPTH and rd_fire at occ==0, and a blocked-write counter that must rise instead of the golden queue growing past DEPTH.