Skip to content

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

fifo_architecture.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

fifo_logic.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 backpressure

What 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

FIFO: write and read pointers over a block-RAM array producing full/empty flags and valid/ready handshakewritereadwrite sidedin, wr_valid; guardnot-fullBRAM array (DEPTH xWIDTH)registered readread sidedout, rd_ready; guardnot-emptyfull / empty / almostextra-bit pointer compare-> valid/ready12
A parameterized synchronous FIFO is a block-RAM storage array indexed by a write pointer and a read pointer, each carrying one extra bit beyond the address so full and empty are distinguishable when the pointers coincide. Writes are guarded by not-full and reads by not-empty, preventing overflow and underflow. The flags map to a valid/ready handshake — write-ready is not-full, read-valid is not-empty — and almost-full and almost-empty thresholds drive backpressure early. Width and depth generics let one verified source serve any size. This is the integrated FIFO structure; the waveform below shows filling to full, draining to empty, and a simultaneous read and write holding the occupancy steady.

5. Simulation interpretation — fill, drain, and simultaneous read/write

FIFO (DEPTH=4): fill to full, simultaneous r/w holds count, drain to empty

10 cycles
FIFO (DEPTH=4): fill to full, simultaneous r/w holds count, drain to emptyafter 4 writes: count = DEPTH -> full asserts (extra pointer bit disambiguates)after 4 writes: count …simultaneous write+read this cycle -> count stays at DEPTH (one in, one out)simultaneous write+rea…drained: count = 0 -> empty asserts againdrained: count = 0 -> …clkwr1111100000rd0000111110count0123443210full0000110000empty1000000001t0t1t2t3t4t5t6t7t8t9
Four writes fill the FIFO to DEPTH, asserting full — distinguishable from empty only because the pointers carry the extra MSB. The cycle with a simultaneous write and read keeps the occupancy steady (one enters as one leaves), exercising the trickiest case. Reads then drain it back to empty. Verifying full, empty, and the simultaneous read/write case is what proves the pointer and flag logic correct.

6. 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.

fifo_extra_bit_and_simul.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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 full and reads with not 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.