VHDL · Chapter 20.3 · Capstone Projects
Capstone: Parameterized FIFO
The third capstone is the most reused block in all of RTL, a parameterized synchronous FIFO, built fully and verified. It pulls together everything from the storage and streaming modules. Width and depth generics let one source serve every size, a block-RAM-inferable storage array uses a registered read with no array-wide reset, and write and read pointers carry an extra bit, or a count, to tell full from empty unambiguously. Almost-full and almost-empty thresholds drive backpressure early, a valid and ready handshake wraps the flags, and overflow and underflow guards block writes into a full FIFO and reads from an empty one. The defining engineering question is disambiguating full from empty when the pointers coincide. Verification is the real lesson, with fill-to-full, drain-to-empty, simultaneous read and write, back-to-back, and random traffic checked against a scoreboard.
Intermediate16 min readVHDLCapstoneFIFOGenericsVerificationStreaming
1. Engineering intuition — two pointers chasing each other around a ring
A FIFO is a ring of storage with two pointers: a write pointer that advances on each accepted write and a read pointer that advances on each accepted read. Data lives between them. The whole design is bookkeeping around one tricky question: when the two pointers point at the same slot, is the FIFO empty (read caught up to write) or full (write wrapped all the way around to read)? The low bits alone cannot tell — they are equal in both cases — so you carry one extra pointer bit (or an explicit occupancy count): equal pointers with matching extra bits means empty, equal low bits with differing extra bits means full. Wrap that core in generics for size, block RAM for storage, handshake flags for flow control, and guards against overflow/underflow, and you have the universal buffer.
2. Formal explanation — the parameterized FIFO architecture
entity sync_fifo is
generic ( WIDTH : positive := 8; DEPTH : positive := 16 ); -- one source, any size
port ( clk, rst : in std_logic;
wr_valid : in std_logic; wr_ready : out std_logic; din : in std_logic_vector(WIDTH-1 downto 0);
rd_ready : in std_logic; rd_valid : out std_logic; dout : out std_logic_vector(WIDTH-1 downto 0);
almost_full, almost_empty : out std_logic );
end entity;
architecture rtl of sync_fifo is
type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal mem : mem_t; -- BRAM-inferable (registered read, no array reset)
-- pointers carry ONE EXTRA bit beyond clog2(DEPTH) to disambiguate full vs empty:
signal wr_ptr, rd_ptr : unsigned(clog2(DEPTH) downto 0) := (others => '0');
begin
-- empty: pointers fully equal. full: low bits equal, MSBs DIFFER.
-- wr_ready <= not full; rd_valid <= not empty; (valid/ready handshake, 18.2)
end architecture;The FIFO is generics (WIDTH/DEPTH) + a BRAM-inferable array + write/read pointers with an extra bit + flag logic (empty = pointers equal; full = low bits equal, MSBs differ) + a valid/ready wrapper. The extra pointer bit is what makes full and empty distinguishable.
3. Production usage — guarded write/read and handshake flags
process (clk) begin
if rising_edge(clk) then
if rst = '1' then wr_ptr <= (others => '0'); rd_ptr <= (others => '0');
else
-- WRITE guarded by not-full (overflow guard):
if wr_valid = '1' and full = '0' then
mem(to_integer(wr_ptr(clog2(DEPTH)-1 downto 0))) <= din;
wr_ptr <= wr_ptr + 1;
end if;
-- READ guarded by not-empty (underflow guard):
if rd_ready = '1' and empty = '0' then
dout <= mem(to_integer(rd_ptr(clog2(DEPTH)-1 downto 0))); -- registered read -> BRAM
rd_ptr <= rd_ptr + 1;
end if;
end if;
end if;
end process;
-- FLAGS + handshake:
empty <= '1' when wr_ptr = rd_ptr else '0';
full <= '1' when (wr_ptr(clog2(DEPTH)) /= rd_ptr(clog2(DEPTH)))
and (wr_ptr(clog2(DEPTH)-1 downto 0) = rd_ptr(clog2(DEPTH)-1 downto 0)) else '0';
wr_ready <= not full; rd_valid <= not empty;
almost_full <= '1' when (wr_ptr - rd_ptr) >= DEPTH - MARGIN else '0'; -- early backpressureWhat hardware does this become? A dual-port RAM (or registers) with two pointer counters and a little flag
logic — compact, and the buffer in every streaming design. The guards (not full on write, not empty on
read) prevent overflow/underflow; the extra pointer bit disambiguates full from empty; the valid/ready
wrapper lets it drop into any handshake fabric; and almost-full asserts backpressure before true full so the
producer stops with margin to spare (given the cycle or two ready takes to propagate). Generics make the same
verified source a 16×8 control FIFO or a 1024×64 data FIFO.
4. Structural interpretation — the FIFO top
5. Simulation interpretation — fill, drain, and simultaneous read/write
FIFO (DEPTH=4): fill to full, simultaneous r/w holds count, drain to empty
10 cycles6. Debugging example — full/empty ambiguity and the simultaneous read/write case
Expected: correct flags and no data loss. Observed: the FIFO reports full and empty as the same
condition (so it wraps wrongly and loses or duplicates data), or the occupancy is wrong after a simultaneous
read and write, or it overflows/underflows under bursty traffic. Root cause: the pointers were sized with
no extra bit (or no count), so equal pointers were ambiguous between full and empty; and/or the
simultaneous read+write case was mishandled (both should advance their own pointer, leaving occupancy
unchanged), and/or writes/reads were not guarded by full/empty. Fix: carry the extra pointer bit (or an
explicit count) so full and empty are distinct, handle a simultaneous read and write by advancing both
pointers (net occupancy unchanged), and guard every write with not full and read with not empty.
Engineering takeaway: a correct FIFO needs the extra pointer bit to separate full from empty, must handle
simultaneous read/write (both pointers move), and must guard accesses — these are exactly the cases
verification must hit, not just a clean fill and drain.
-- BUG: pointers exactly clog2(DEPTH) bits -> full and empty both look like 'pointers equal'.
-- FIX: one extra MSB; full = (low bits equal AND MSBs differ); empty = (all bits equal).
-- Simultaneous r/w: both guards pass independently -> wr_ptr++ and rd_ptr++ -> count unchanged.7. Common mistakes & what to watch for
- No extra pointer bit / count. Equal pointers are ambiguous between full and empty; carry the extra MSB (or a count) to disambiguate.
- Mishandling simultaneous read/write. Advance both pointers when both occur; occupancy stays the same — a key verification case.
- Unguarded accesses. Guard writes with
not fulland reads withnot empty, or bursty traffic overflows/ underflows and corrupts data. - Array-wide reset on the storage. Keep the storage BRAM-inferable (registered read, no array-wide reset); reset only the pointers/flags.
- Verifying only fill/drain. Test simultaneous r/w, almost-full backpressure, back-to-back, and random traffic against a scoreboard — not just a clean fill and drain.
8. Engineering insight & continuity
The FIFO capstone builds the canonical reusable component fully and verifiably: generics for any size, a BRAM-inferable array, write/read pointers with an extra bit to disambiguate full from empty, guarded accesses, almost-full backpressure, and a valid/ready wrapper — proven against fill, drain, simultaneous read/write, and scoreboard-checked random traffic. It is the buffer at the heart of streaming RTL. The next capstone shifts from storage to computation — a pipelined arithmetic unit — in the next lesson, Capstone: Pipelined ALU.