VHDL · Chapter 12.6 · Generics
Parameterized Memories and FIFOs
Storage is where parameterization pays off most. A memory is naturally generic. A data-width generic and a depth generic size the storage array, with the address width derived from the depth so it always matches. A FIFO adds write and read pointers over that same storage, full and empty flags distinguished with an extra pointer bit or a count, and an occupancy count, and every one of width, depth, and the almost-full threshold can be a generic. The result is the single most reused block in RTL, one verified FIFO instantiated at many different sizes across a design. This lesson builds a generic synchronous memory and a generic FIFO, showing the derived-width and pointer and flag idioms that make them reusable.
Foundation15 min readVHDLGenericsFIFOMemoryRAMReuse
1. Engineering intuition — storage you size at the door
You never want "the 16-deep 8-bit FIFO" hard-coded; you want the FIFO, sized where you drop it in. Storage is
defined by two numbers — how wide each word is and how many words there are — so those are exactly the generics.
Everything else follows: the address width is whatever it takes to index DEPTH words (clog2(DEPTH)), the
memory array is DEPTH words of DATA_W bits, and the FIFO's pointers and flags are derived from the same two
numbers. Parameterize those, write the control logic width- and depth-agnostically, verify once, and you have a
component that fits every storage need in the design.
2. Formal explanation — a generic synchronous memory
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
entity sync_ram is
generic ( DATA_W : positive := 8;
DEPTH : positive := 256 );
port ( clk : in std_logic;
we : in std_logic;
addr : in std_logic_vector(clog2(DEPTH)-1 downto 0); -- DERIVED address width
din : in std_logic_vector(DATA_W-1 downto 0);
dout : out std_logic_vector(DATA_W-1 downto 0) );
end entity;
architecture rtl of sync_ram is
type mem_t is array (0 to DEPTH-1) of std_logic_vector(DATA_W-1 downto 0); -- DEPTH × DATA_W
signal mem : mem_t;
begin
process (clk) begin
if rising_edge(clk) then
if we = '1' then mem(to_integer(unsigned(addr))) <= din; end if; -- synchronous write
dout <= mem(to_integer(unsigned(addr))); -- synchronous read
end if;
end process;
end architecture;The storage is an array generic in both dimensions: DEPTH words of DATA_W bits, with the address width
derived from DEPTH via clog2. A synchronous read/write process gives the inferable RAM shape (covered fully
in the memory-modeling module); here the point is that width and depth are generics, so one entity is every RAM.
3. Production usage — a generic FIFO over the same storage
-- One FIFO entity: width + depth generic; pointers, full/empty, and count all derived.
entity sync_fifo is
generic ( DATA_W : positive := 8; DEPTH : positive := 16 );
port ( clk, rst : in std_logic;
wr, rd : in std_logic;
din : in std_logic_vector(DATA_W-1 downto 0);
dout : out std_logic_vector(DATA_W-1 downto 0);
full, empty : out std_logic );
end entity;
-- Internals (sketch): mem(0..DEPTH-1) of DATA_W bits; wr_ptr/rd_ptr are clog2(DEPTH)+1 bits so the
-- MSB distinguishes full from empty; full = (ptrs equal, MSBs differ); empty = (ptrs fully equal).
-- Instantiated at any size from the SAME source:
u_small : entity work.sync_fifo generic map (DATA_W => 8, DEPTH => 16) port map (/* ... */);
u_big : entity work.sync_fifo generic map (DATA_W => 64, DEPTH => 1024) port map (/* ... */);What hardware does this become? u_small elaborates a 16×8 FIFO; u_big a 1024×64 FIFO — the same control
logic (pointer increment, wrap, full/empty comparison) over differently-sized storage. The extra pointer bit
(clog2(DEPTH)+1) is the classic trick to tell full from empty when the read and write pointers point at
the same slot: equal pointers with differing MSBs means full, fully-equal means empty. Width and depth are fixed
at elaboration; the behaviour is identical at every size.
4. Structural interpretation — the parameterized FIFO
5. Simulation interpretation — write, read, full, and empty
A generic FIFO (DEPTH=4 shown): fills to full, drains to empty
10 cycles6. Debugging example — full/empty ambiguity and address-width drift
Expected: the FIFO distinguishes full from empty and indexes its full depth. Observed: full and empty
both look the same (the FIFO wraps incorrectly), or the address cannot reach all DEPTH words. Root cause:
pointers were sized clog2(DEPTH) bits with no extra MSB, so equal pointers are ambiguous between full and
empty; or the address width was hard-coded instead of derived, so it did not match DEPTH when the generic
changed. Fix: size pointers clog2(DEPTH)+1 so the MSB disambiguates full vs empty (or track an explicit
count), and derive the address width as clog2(DEPTH) everywhere. Engineering takeaway: generic FIFOs
need one extra pointer bit (or a count) to separate full from empty, and all address/pointer widths must be
derived from DEPTH so they track the generic.
-- BUG: pointers exactly clog2(DEPTH) bits → full and empty both look like "ptrs equal".
-- signal wr_ptr, rd_ptr : unsigned(clog2(DEPTH)-1 downto 0);
-- FIX: one extra MSB → full = (low bits equal, MSB differs); empty = (all bits equal).
signal wr_ptr, rd_ptr : unsigned(clog2(DEPTH) downto 0);7. Common mistakes & what to watch for
- No extra pointer bit / count. Without it, full and empty are indistinguishable when pointers coincide; add an MSB or an occupancy count.
- Hard-coded address width. Derive it as
clog2(DEPTH)so it tracks the depth generic; a fixed width breaks whenDEPTHchanges. - Non-power-of-two depth assumptions. Pointer wrap logic that assumes power-of-two
DEPTHmisbehaves otherwise; handle wrap explicitly or constrainDEPTH. - Read-during-write / first-word behaviour unspecified. Decide and document the read/write collision and first-word-fall-through behaviour; it affects correctness.
- Async vs sync flags. Keep full/empty generation synchronous to the FIFO clock(s); for dual-clock FIFOs use proper CDC (later module).
8. Engineering insight & continuity
Parameterized memories and FIFOs are the canonical reusable components: width and depth generics size the storage
array, clog2(DEPTH) derives the address and pointer widths, and pointer comparison (with an extra bit or a
count) yields full/empty/occupancy — so one verified FIFO serves every size in the design. This is parameterization
delivering its biggest dividend. The module closes by distilling the recurring techniques behind blocks like this
into reusable form: Parameterized Design Patterns — the idioms (derived widths, generic+generate, optional
logic, configuration records) that make any block cleanly parameterizable.