Skip to content

VHDL · Chapter 18.3 · Advanced RTL Design

FIFO Design

The FIFO is the buffer that makes streaming RTL practical, decoupling a producer from a consumer, absorbing bursts, and bridging rate mismatches. Architecturally it is dual-port storage, using block RAM or registers, with a write pointer and a read pointer: writes advance one, reads advance the other, and the full and empty flags are derived by comparing them, using an extra pointer bit or an explicit count to tell a full ring from an empty one. Practical FIFOs add almost-full and almost-empty thresholds that drive valid and ready backpressure before the buffer saturates, a choice of first-word-fall-through versus standard read latency, and overflow and underflow guards. Sizing the depth is an engineering decision. This lesson covers the synchronous FIFO architecture, the flag logic, read timing, handshake integration, and sizing.

Foundation15 min readVHDLRTLFIFOBufferBackpressureStreaming

1. Engineering intuition — a shock absorber for data

Producers and consumers rarely run in perfect lockstep: one bursts, the other occasionally stalls, or they want slightly different average rates. A FIFO is the shock absorber between them — a queue that stores words written faster than they are read and supplies words when the producer pauses. As long as it neither overflows (write into a full FIFO → data lost) nor underflows (read from an empty FIFO → garbage), the two sides can run with local irregularity while staying globally matched. The whole design is bookkeeping: two pointers tracking where to write and read, and flags that say "full, don't write" and "empty, don't read" — wired to valid/ready so the FIFO throttles both sides automatically.

2. Formal explanation — synchronous FIFO architecture

sync_fifo.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Single-clock FIFO: dual-port storage + write/read pointers; flags from an EXTRA pointer bit or a COUNT.
type mem_t is array (0 to DEPTH-1) of std_logic_vector(W-1 downto 0);
signal mem : mem_t;
signal wr_ptr, rd_ptr : unsigned(clog2(DEPTH) downto 0);   -- ONE extra bit → distinguishes full/empty
signal count : unsigned(clog2(DEPTH) downto 0);            -- (or track an explicit occupancy count)
 
empty <= '1' when wr_ptr = rd_ptr else '0';                -- pointers fully equal → empty
full  <= '1' when (wr_ptr(clog2(DEPTH)) /= rd_ptr(clog2(DEPTH)))      -- MSBs differ...
                  and (wr_ptr(clog2(DEPTH)-1 downto 0) = rd_ptr(clog2(DEPTH)-1 downto 0))  -- ...low bits equal → full
         else '0';
 
process (clk) begin
  if rising_edge(clk) then
    if wr_en = '1' and full  = '0' then mem(to_integer(wr_ptr(clog2(DEPTH)-1 downto 0))) <= din; wr_ptr <= wr_ptr + 1; end if;
    if rd_en = '1' and empty = '0' then dout <= mem(to_integer(rd_ptr(clog2(DEPTH)-1 downto 0)));   rd_ptr <= rd_ptr + 1; end if;
  end if;
end process;
 
-- ALMOST-FULL / ALMOST-EMPTY thresholds drive backpressure EARLY (18.2):
almost_full <= '1' when count >= DEPTH - MARGIN else '0';

A synchronous FIFO is dual-port storage with write/read pointers; empty = pointers equal, full = low bits equal but the extra MSB differs (or use a count). Almost-full/empty thresholds let it assert backpressure before saturating. Writes/reads are guarded by full/empty.

3. Production usage — handshake-wrapped FIFO, FWFT, sizing

fifo_in_use.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- HANDSHAKE wrapping (18.2): map FIFO flags to valid/ready so it composes with streaming interfaces.
--   wr_ready <= not full;    wr_en <= wr_valid and wr_ready;     -- producer side
--   rd_valid <= not empty;   rd_en <= rd_valid and rd_ready;     -- consumer side
--
-- READ TIMING:
--   STANDARD: dout valid the cycle AFTER rd_en (registered read, like BRAM, 17.2).
--   FWFT (first-word-fall-through): the head word is present on dout BEFORE rd_en (read = "pop") —
--     friendlier for valid/ready streaming (rd_valid + dout both valid together).
--
-- SIZING the depth:
--   • absorb the worst producer BURST while the consumer is stalled, OR
--   • cover the consumer's maximum stall latency at the producer's rate.
--   • use ALMOST-FULL margin so backpressure stops writes before true full (pipeline latency in ready).

What hardware does this become? A dual-port RAM (or register file) plus two pointer counters and a little flag logic — compact, and the standard buffer in every streaming design. Wrapping the flags as valid/ready (wr_ready = not full, rd_valid = not empty) makes it drop straight into a handshake fabric, throttling both sides. FWFT presents the head word before the pop, aligning naturally with valid/ready (data and rd_valid together), while standard read has the one-cycle latency of a registered RAM read. Depth is sized to the worst burst or stall you must absorb, with an almost-full margin so backpressure (which takes a cycle or two to propagate) stops writes before the FIFO truly fills.

4. Structural interpretation — FIFO architecture

FIFO: write pointer and read pointer over dual-port storage producing full/empty and valid/readywritereadwrite sidedin, wr_en, wr_ptr;wr_ready=not fulldual-port storageDEPTH × W wordsread sidedout, rd_en, rd_ptr;rd_valid=not emptyfull / empty / almostpointer compare →backpressure12
A synchronous FIFO buffers a stream between a producer and consumer. Dual-port storage holds the words; a write pointer advances on each accepted write and a read pointer on each accepted read. Comparing the pointers (with an extra bit or an explicit count) produces empty and full, and almost-full/almost-empty thresholds drive backpressure early. The flags map to valid/ready so the FIFO composes with handshake interfaces: write-ready is not-full, read-valid is not-empty. Read timing is either standard (registered, one-cycle latency) or first-word-fall-through. This is a buffer-architecture structure; the waveform below shows a burst filling the FIFO and asserting backpressure.

5. Simulation interpretation — burst fills, backpressure, drains

Producer burst fills the FIFO; almost_full backpressures; consumer drains

8 cycles
Producer burst fills the FIFO; almost_full backpressures; consumer drainsburst fills toward depth → almost_full asserts (early backpressure)burst fills toward dep…consumer starts reading; writes ease off under backpressureconsumer starts readin…drained: count=0 → empty againdrained: count=0 → emp…clkwr_en11110000rd_en00011111count01233210almost_full00111000empty10000001t0t1t2t3t4t5t6t7
A producer burst raises count toward the depth; almost_full asserts before true full so backpressure (valid/ready) can stop new writes with margin to spare. As the consumer reads, count falls and the FIFO drains back to empty. The almost-full threshold — not waiting for full — is what makes backpressure safe given the cycle or two it takes ready to propagate. This burst-absorb-and-drain behavior is exactly why FIFOs sit between mismatched producers and consumers.

6. Debugging example — overflow/underflow or full/empty ambiguity

Expected: lossless buffering with correct flags. Observed: data lost (writes into a full FIFO) or garbage read (reads from an empty FIFO), or full and empty are indistinguishable so the FIFO wraps wrongly. Root cause: writes/reads were not guarded by full/empty (or backpressure asserted too late, after true full), causing overflow/underflow; or the pointers were sized without the extra bit/count, so equal pointers were ambiguous between full and empty. Fix: guard every write with not full and read with not empty (and wire wr_ready=not full, rd_valid=not empty to the handshake), size pointers clog2(DEPTH)+1 (or track a count) to disambiguate full/empty, and use an almost-full margin so backpressure stops writes before saturation. Engineering takeaway: guard reads/writes with empty/full, add the extra pointer bit (or a count) to separate full from empty, and backpressure on almost-full — unguarded access or ambiguous flags cause overflow, underflow, and lost data.

guard_and_disambiguate.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- BUG: unguarded write + pointers with no extra bit → overflow + full/empty ambiguous.
-- mem(wr) <= din; wr_ptr <= wr_ptr + 1;   -- writes even when full
-- FIX: guard with full, extra pointer bit (or count) for unambiguous flags.
if wr_en='1' and full='0' then mem(to_integer(wr_ptr(...))) <= din; wr_ptr <= wr_ptr + 1; end if;

7. Common mistakes & what to watch for

  • Unguarded reads/writes. Guard with empty/full (and expose them as valid/ready); otherwise overflow or underflow corrupts data.
  • No extra pointer bit / count. Equal pointers are ambiguous; add the MSB (or a count) to separate full from empty.
  • Backpressure only at true full. Use an almost-full threshold so writes stop before saturation given ready latency.
  • FWFT vs standard confusion. Pick a read timing and match the consumer; FWFT presents the head before pop, standard has one-cycle latency.
  • Undersized depth. Size for the worst burst/stall; too shallow drops data or stalls throughput.

8. Engineering insight & continuity

A synchronous FIFO is the streaming buffer: dual-port storage with write/read pointers, full/empty from an extra pointer bit or a count, almost-full/empty thresholds driving valid/ready backpressure, a choice of FWFT vs standard read timing, and a depth sized to absorb bursts and stalls — all with overflow/underflow guards. It is the shock absorber between any mismatched producer and consumer (and the single-clock sibling of the CDC FIFO in 17.6). When several producers contend for one shared resource or FIFO, you need a policy to choose among them — which is the next lesson, Arbiters and Resource Arbitration.