VHDL · Chapter 10.2 · Packages and Reuse
Constants and Shared Definitions
The fastest way to make a design inconsistent is to scatter magic numbers, a bus width here, a FIFO depth there, an address somewhere else, across many files. Put them in a package as constants and they become a single source of truth: one place to read and one place to change, with every unit importing the same value. Constants in packages do more than name numbers, though. A constant array is a lookup table, and indexing it at runtime synthesizes to a ROM, so packages also hold coefficient tables, character maps, and microcode. This lesson covers shared parameters, constant tables and the ROM they become, and deferred constants, which are declared in the package and given their value in the body.
Foundation13 min readVHDLConstantsPackagesROMParametersReuse
1. Engineering intuition — name the numbers once
Every design has numbers that several modules must agree on: how wide the data bus is, how deep a FIFO goes,
where a register sits in the address map, what threshold a comparator uses. If those live as literals sprinkled
through the code, changing one means hunting down every copy — and missing one means a silent mismatch. A
package constant gives each number a name and a single home: write DATA_W once, import it everywhere, and
the whole design changes consistently when you edit that one line. Named constants also make the code read like
its intent rather than a pile of magic numbers.
2. Formal explanation — shared constants and constant tables
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
package cfg_pkg is
-- shared scalar parameters (single source of truth)
constant DATA_W : integer := 32;
constant FIFO_DEPTH : integer := 16;
constant BASE_ADDR : unsigned(31 downto 0) := x"4000_0000";
-- a constant TABLE: an array constant → a lookup ROM when indexed at runtime.
type rom_t is array (0 to 7) of std_logic_vector(7 downto 0);
constant SINE : rom_t := (x"80", x"B0", x"DA", x"F5", x"FF", x"F5", x"DA", x"B0");
-- a DEFERRED constant: declared here, value given in the package body.
constant VERSION : std_logic_vector(7 downto 0);
end package;
package body cfg_pkg is
constant VERSION : std_logic_vector(7 downto 0) := x"21"; -- value deferred to the body
end package body;A package can hold scalar constants (parameters), constant arrays (tables), and deferred constants (declared in the package with the value supplied in the body — useful when the value is implementation detail or must be hidden from the package interface). All are shared by importing the package.
3. Production usage — parameters and a ROM table
library work; use work.cfg_pkg.all;
library ieee; use ieee.numeric_std.all;
-- Parameters size the design consistently.
signal data : std_logic_vector(DATA_W-1 downto 0); -- 32 from the package
-- A constant array indexed by a runtime value becomes a ROM.
process (clk) begin
if rising_edge(clk) then
rom_out <= SINE(to_integer(unsigned(addr(2 downto 0)))); -- 8-entry sine ROM lookup
end if;
end process;What hardware does this become? The scalar constants (DATA_W, etc.) are compile-time parameters — they
size signals and logic but are not hardware themselves. The constant array SINE, indexed by a runtime addr,
synthesises to a ROM (or LUTs) holding those eight bytes, with a registered output here. So package constants
serve two roles: parameterising the design and providing the contents of read-only tables.
4. Hardware / structural interpretation — one definition, many users; tables become ROMs
5. Simulation interpretation — the constant table as a ROM
A constant array indexed by addr behaves as a ROM lookup
8 cycles6. Debugging example — the duplicated parameter that drifted
Expected: two modules agree on the data width. Observed: a width mismatch error, or truncated/garbled data between modules. Root cause: the width was written as a literal (or a local constant) in each module rather than imported from a package, and one was changed without the other — classic magic-number drift. Fix: declare the width once as a package constant and have every module use it; never repeat the literal. Engineering takeaway: mismatched parameters between modules almost always mean a constant was duplicated instead of packaged — centralise every shared number in a package and import it.
-- BUG: each module hard-codes its own width → they drift.
-- signal a : std_logic_vector(31 downto 0); -- module 1
-- signal b : std_logic_vector(15 downto 0); -- module 2 (was meant to match!)
-- FIX: one package constant, imported by both.
signal a : std_logic_vector(DATA_W-1 downto 0); -- DATA_W from work.cfg_pkg7. Common mistakes & what to watch for
- Hard-coding magic numbers in modules. Put shared parameters in a package constant and import them; never duplicate literals.
- Forgetting a deferred constant's body value. A deferred constant must get its value in the package body, or it is incomplete.
- Huge constant tables expecting cheap ROMs. Indexed constant arrays become ROM/LUTs; very large tables may belong in inferred block RAM instead (later module).
- Putting per-instance parameters in a package. Values that vary by instance belong in generics (Module 12), not a shared package constant.
- Naming constants poorly. A package constant should read as intent (
DATA_W,FIFO_DEPTH), not an opaque number.
8. Engineering insight & continuity
Package constants turn a design's parameters and tables into named, shared, single-source definitions: scalars that size the whole design consistently, and constant arrays that become ROMs. Centralising them prevents the magic-number drift that causes silent mismatches, and deferred constants let a package expose a name while hiding its value. The next lesson extends sharing from values to structure: Shared Types and Subtypes in Packages — putting enums, records, and sized subtypes in a package so modules that must interoperate (master, slave, monitor) agree on their interface types by reference, not by convention.