Skip to content

VHDL · Chapter 12.2 · Generics

Width-Generic Design

By far the most common use of generics is parameterizing data-path width. A single width generic sizes every port and internal signal, and the trick is to write the logic width-agnostically so it scales without edits. That means an all-zeros aggregate for reset regardless of width, the range and length attributes for loops and slices, and numeric-standard arithmetic that automatically matches the operand width. Done well, one adder, multiplexer, or register entity serves a 4-bit and a 64-bit instance identically. Related widths can also be derived from one generic, so an address size stays consistent with a depth. This lesson shows how to build width-generic blocks and the idioms that keep the logic independent of any hard-coded width.

Foundation14 min readVHDLGenericsWidthData Pathnumeric_stdReuse

1. Engineering intuition — write the logic, not the width

A good width-generic block is written as if you do not know — and do not care — how wide it is. The reset is "all zeros" (whatever that means at this width), the loop runs "over the whole vector," the add is "these two numbers." Each of those phrasings has a width-independent VHDL form, and when you use them, the same source compiles to a 4-bit block or a 64-bit block with only the WIDTH generic changing. The skill is reflexively reaching for the width-agnostic idiom — (others => '0'), 'range, numeric_std arithmetic — instead of hard-coding a number that would pin the design to one size.

2. Formal explanation — width-agnostic idioms

width_generic_adder.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
entity adder is
  generic ( WIDTH : positive := 8 );
  port ( a, b : in  std_logic_vector(WIDTH-1 downto 0);     -- ports sized by the generic
         sum   : out std_logic_vector(WIDTH downto 0) );     -- one extra bit for the carry
end entity;
architecture rtl of adder is
begin
  -- numeric_std arithmetic scales to WIDTH automatically; resize keeps the carry bit.
  sum <= std_logic_vector( resize(unsigned(a), WIDTH+1) + resize(unsigned(b), WIDTH+1) );
end architecture;
 
-- Width-agnostic idioms used across width-generic code:
--   (others => '0')           -- zero/reset at ANY width
--   for i in a'range loop     -- iterate the whole vector, whatever its width
--   a'length, a'high, a'low   -- query the actual width/bounds
--   clog2(DEPTH)              -- DERIVE an address width from a depth generic

The pattern: size ports/signals with WIDTH-1 downto 0, then write logic with width-independent constructs — (others => '0') for constants, 'range/'length/'high for iteration and bounds, and numeric_std arithmetic that adapts to the operand width. Derived sizes (clog2) keep related widths consistent with one generic.

3. Production usage — one block, any width

any_width_instances.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- The SAME adder entity, two widths — identical source, different generic.
u_n4  : entity work.adder generic map (WIDTH => 4)  port map (a => a4,  b => b4,  sum => s5);
u_n32 : entity work.adder generic map (WIDTH => 32) port map (a => a32, b => b32, sum => s33);
 
-- A width-generic register reset width-agnostically:
process (clk) begin
  if rising_edge(clk) then
    if rst = '1' then q <= (others => '0');   -- correct at WIDTH=4 or 32 — no literal
    else              q <= d;
    end if;
  end if;
end process;

What hardware does this become? u_n4 elaborates a 4-bit ripple/carry adder with a 5-bit result; u_n32 a 32-bit adder with a 33-bit result — the same description, scaled. (others => '0') becomes a 4- or 32-bit constant zero as appropriate. Because the arithmetic and constants are width-agnostic, the synthesizer simply generates more or fewer bits of the identical structure. One verified source covers every width you instantiate.

4. Structural interpretation — the data path scales with WIDTH

width-generic adder whose data path width scales with the WIDTH genericWIDTH+1a, b : WIDTH bitsports sized by genericnumeric_std +(scales)resize to WIDTH+1 keepscarrysum : WIDTH+1 bitsresult with carry-out12
A width-generic adder scales its structure with the WIDTH generic. Ports a and b are WIDTH bits and the sum is WIDTH+1 to hold the carry; the numeric_std addition and the resize that preserves the carry both adapt to WIDTH automatically, and width-agnostic idioms like (others => '0') keep constants correct at any size. Instantiated at WIDTH=4 it is a 4-bit adder; at WIDTH=32 a 32-bit adder — the same source, more or fewer bits of identical structure. This is a parameterized data-path structure; the waveform below shows the same block behaving correctly at two widths.

5. Simulation interpretation — the same block at two widths

One width-generic adder: WIDTH=4 vs WIDTH=32, carry-out at the top bit either way

4 cycles
One width-generic adder: WIDTH=4 vs WIDTH=32, carry-out at the top bit either wayWIDTH=4: F+1 = 0x10 → carry into bit 4WIDTH=4: F+1 = 0x10 → …WIDTH=32: same source, carry into bit 32WIDTH=32: same source,…a (hex)F8FFFFFFFF80000000b (hex)180000000180000000sum (W+1)1010100000000100000000carry (msb)t0t1t2t3
The identical width-generic adder, instantiated at WIDTH=4 and WIDTH=32. Because the result is WIDTH+1 bits and the arithmetic scales, the carry-out always lands in the top bit at whatever width was chosen — 0xF+0x1 overflows 4 bits exactly as 0xFFFFFFFF+1 overflows 32. One source, verified once, correct at every width.

6. Debugging example — the hard-coded width that did not scale

Expected: a block that works at any width. Observed: it works at 8 bits but breaks at 16/32 — truncated results, a stuck reset value, or a width-mismatch error. Root cause: a hard-coded width leaked into the otherwise-generic code: a literal (7 downto 0), an x"00" instead of (others => '0'), a fixed loop bound 0 to 7, or arithmetic sized to a constant rather than to WIDTH. Fix: replace every literal width with the generic or a width-agnostic idiom — WIDTH-1 downto 0, (others => '0'), a'range, resize(..., WIDTH+1). Engineering takeaway: a width-generic block must contain no hard-coded widths — use the generic and (others=>...)/'range/numeric_std so the logic scales; a single literal pins it to one size.

no_hard_coded_widths.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- BUG: a literal width and a fixed constant defeat genericity.
-- q <= x"00";            -- only correct at 8 bits
-- for i in 0 to 7 loop   -- only correct at 8 bits
-- FIX: width-agnostic forms.
q <= (others => '0');     for i in q'range loop -- correct at any WIDTH

7. Common mistakes & what to watch for

  • Hard-coded widths or constants. A literal (7 downto 0), x"00", or 0 to 7 pins the design to one size; use WIDTH-1 downto 0, (others => '0'), 'range.
  • Forgetting the extra result bit. An add/accumulate needs WIDTH+1 (and resize) to keep the carry; otherwise it truncates.
  • Mixing widths in arithmetic. numeric_std requires operands to match width — resize to a common width before operating.
  • Not deriving related widths. Use clog2(DEPTH) to size addresses/counts from one generic so they stay consistent.
  • Width-1 / zero-width edge cases. Very small generic values can create degenerate ranges; constrain generics (e.g. positive) and consider boundaries.

8. Engineering insight & continuity

Width-generic design is parameterization's flagship use: one WIDTH generic sizes the ports and signals, and width-agnostic idioms — (others => '0'), 'range/'length, scaling numeric_std arithmetic, derived clog2 widths — let the same verified source serve every width from 4 to 64 bits. The rule is to write the logic, never the width. With width-generic blocks in hand, the next lesson focuses on the instantiation side — Generic Maps and Instantiation — how to pass generic values cleanly (positional, named, defaults) and connect parameterized components into a larger design.