VHDL · Chapter 10.1 · Packages and Reuse
Packages and Package Bodies
Real designs are made of many units that must agree on shared things, like a bus width, a set of opcodes, a state type, or a helper function, and copying those into every file is how inconsistencies creep in. A package is VHDL's answer. It is a named, reusable container of declarations, including types, subtypes, constants, functions, procedures, and component declarations, that any design unit can import with a use clause, giving a single source of truth. A package splits into two parts, the package itself, which holds the public declarations or interface, and the package body, which holds the implementations of its subprograms, mirroring the entity and architecture separation you already know. This module opener establishes packages as the foundation of reuse that the rest of the module builds on.
Foundation14 min readVHDLPackagesReusePackage Bodyuse clauseDesign
1. Engineering intuition — one place for shared definitions
When several modules must agree on something — the width of a data bus, the encoding of a command, the type of a shared interface — that definition should exist in exactly one place, and everyone should refer to it. Copying it into each file guarantees that, eventually, one copy will drift and the design will mismatch. A package is that one place: declare the shared thing once, and every unit that needs it imports it. Change it once, and every user updates consistently. Packages turn "shared by convention" into "shared by reference."
2. Formal explanation — package and package body
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
-- PACKAGE: the public declarations (the interface) — what users see.
package my_pkg is
constant DATA_W : integer := 32; -- shared constant
subtype data_t is std_logic_vector(DATA_W-1 downto 0); -- shared type
type cmd_t is (NOP, READ, WRITE); -- shared enum
function parity(v : std_logic_vector) return std_logic; -- subprogram DECLARATION (no body here)
end package;
-- PACKAGE BODY: the implementations of the package's subprograms.
package body my_pkg is
function parity(v : std_logic_vector) return std_logic is
variable p : std_logic := '0';
begin
for i in v'range loop p := p xor v(i); end loop;
return p;
end function;
end package body;A package declares the public items — constants, types/subtypes, and the declarations (signatures) of any functions/procedures. The package body provides the implementations of those subprograms. (A package with no subprograms needs no body.) This split mirrors entity/architecture: the package is the contract, the body is the implementation, and users depend only on the package.
3. Production usage — import and use
library work; use work.my_pkg.all; -- import everything the package declares
-- (packages live in a library — 'work' is the current one; '.all' brings in all declarations)
entity datapath is
port ( clk : in std_logic;
din : in data_t; -- uses the shared subtype from the package
dout : out data_t );
end entity;
architecture rtl of datapath is
begin
process (clk) begin
if rising_edge(clk) then
dout <= din;
par <= parity(din); -- calls the shared function from the package
end if;
end process;
end architecture;What hardware does this become? The package itself is not hardware — it is shared declarations. The
data_t subtype sizes the ports and registers; the parity function synthesises (when called) into the XOR
tree it describes. The value is at the source level: this unit and every other unit that imports my_pkg share
the exact same width, types, command encoding, and helper, so they cannot disagree.
4. Structural interpretation — a single source of truth
5. Simulation / elaboration interpretation — compiled once, shared
A package is analysed (compiled) into a library once, and every unit that imports it refers to that single compiled declaration. There is no per-instance behaviour and nothing that varies over time — so, like the structural lessons earlier, a structural diagram (above) communicates a package far better than a waveform. The only run-time effect is that a subprogram called from the package body executes as ordinary code where it is invoked. (Because a package compiles before its users, compilation order matters — packages must be analysed first; the dedicated lesson on libraries and compilation order covers this.)
6. Debugging example — the drifted definition (and the missing use)
Expected: all modules agree on a shared definition. Observed: a width or type mismatch between two
modules, or a unit cannot see a package's items. Root cause: either the definition was copied into
multiple files and one drifted (e.g. one module used 31 downto 0 and another 32 bits), or a unit forgot the
use work.my_pkg.all; clause (or referenced the wrong library), so it did not import the shared declarations.
Fix: put the shared definition in one package and have every unit use it — never duplicate it; and
ensure each user has the correct library/use clauses. Engineering takeaway: mismatches between modules
usually mean a shared definition was duplicated instead of packaged; centralise it in a package and import it
everywhere.
-- BUG: each module declares its own DATA_W / data_t → they can drift apart.
-- FIX: ONE package declares them; every module imports it.
library work; use work.my_pkg.all; -- both modules share DATA_W and data_t from here7. Common mistakes & what to watch for
- Duplicating shared definitions instead of packaging them. Copies drift; declare once in a package and
useit everywhere. - Forgetting the
useclause (or wrong library). A unit that does not import the package cannot see its items. - Putting an implementation in the package instead of the body. Subprogram declarations go in the package; their bodies go in the package body.
- Ignoring compilation order. A package must be analysed before the units that use it (covered in the compilation-order lesson).
- Over-stuffing one giant package. Group related declarations into focused packages for clarity and dependency control.
8. Engineering insight & continuity
A package is how VHDL scales from one file to a maintainable system: shared types, constants, and subprograms live in one place and are imported by reference, so the design agrees with itself by construction. The package/package-body split mirrors entity/architecture — interface versus implementation — keeping users decoupled from how subprograms are written. This is the foundation of reuse. The module now builds on it: the next lessons cover Constants and Shared Definitions (parameters and tables in packages), Shared Types and Subtypes, Libraries and Compilation Order, the standard IEEE packages you already use, and an overall Reuse Strategy for organising a project's shared code.