Skip to content

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

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

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

parameterized FIFO: generic storage indexed by write and read pointers producing full and emptywritereadwr_ptrclog2(DEPTH)+1 bitsmem: DEPTH × DATA_Wgeneric storage arrayrd_ptrclog2(DEPTH)+1 bitsfull / empty / countfrom pointer comparison12
A parameterized FIFO over generic storage. DATA_W and DEPTH generics size the memory array (DEPTH words of DATA_W bits) and, via clog2(DEPTH), the address and pointer widths. A write pointer and read pointer index the storage; comparing them (with an extra MSB) produces the full and empty flags, and their difference gives the occupancy count. Every size — width, depth, thresholds — is a generic, so one verified FIFO serves 16×8 and 1024×64 instances alike. This is a parameterized storage structure; the waveform below shows the write/read and full/empty behaviour.

5. Simulation interpretation — write, read, full, and empty

A generic FIFO (DEPTH=4 shown): fills to full, drains to empty

10 cycles
A generic FIFO (DEPTH=4 shown): fills to full, drains to emptyempty=1 at reset (count 0)empty=1 at reset (coun…after 4 writes: count=DEPTH → full=1after 4 writes: count=…after draining: count=0 → empty=1 againafter draining: count=…clkwr1111000000rd0000011110count0123443210full0000110000empty1000000001t0t1t2t3t4t5t6t7t8t9
A DEPTH=4 instance of the generic FIFO. Four writes raise count to DEPTH and assert full; subsequent reads drain it back toward empty. The same control logic — pointer increment, full/empty comparison — runs unchanged at any DATA_W and DEPTH; only the storage and pointer widths scale with the generics.

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

extra_pointer_bit.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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 when DEPTH changes.
  • Non-power-of-two depth assumptions. Pointer wrap logic that assumes power-of-two DEPTH misbehaves otherwise; handle wrap explicitly or constrain DEPTH.
  • 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.