Skip to content

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

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

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

package constants as shared parameters and a constant array as a ROMuse clausecfg_pkg constantsDATA_W, FIFO_DEPTH, SINEtablescalar → parameterssize signals/logic acrossunitsconstant array → ROMindexed at runtimemany design unitsall import the same values12
Constants in a package serve two roles. Scalar constants (DATA_W, FIFO_DEPTH, BASE_ADDR) are compile-time parameters imported by many units, giving a single source of truth that sizes the whole design consistently. Constant arrays are lookup tables: indexed by a runtime address they synthesise to a ROM (or LUTs). Deferred constants declare a name in the package but supply the value in the body, hiding the value from the interface. Centralising these in a package is what keeps a multi-module design self-consistent and its tables in one place.

5. Simulation interpretation — the constant table as a ROM

A constant array indexed by addr behaves as a ROM lookup

8 cycles
A constant array indexed by addr behaves as a ROM lookupaddr=0 → SINE(0)=0x80 (registered output, one cycle later)addr=0 → SINE(0)=0x80 …addr=3 → SINE(3)=0xF5 — reading the constant table as a ROMaddr=3 → SINE(3)=0xF5 …clkaddr01234567rom_out--80B0DAF5FFF5DAt0t1t2t3t4t5t6t7
Indexing the constant array SINE with a runtime addr produces the stored byte at that index — a ROM. The values come straight from the package constant, so the table has one definition shared by every user. The registered output lags addr by one clock (the read register). Scalar package constants, by contrast, are compile-time and have no waveform.

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

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

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