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
-- 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
-- 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
5. Simulation interpretation — burst fills, backpressure, drains
Producer burst fills the FIFO; almost_full backpressures; consumer drains
8 cycles6. 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.
-- 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.