Loading header...

VHDL Packages, numeric_std, and Project Structure

Small VHDL examples can live in one file. Real FPGA projects quickly need shared constants, reusable types, clear folder boundaries, and a build order that does not depend on memory. Packages and numeric_std are the foundation of that discipline.

Learning Objectives

By the end of this lesson, you should be able to:

  • explain why numeric_std is preferred for VHDL arithmetic;
  • place constants, subtypes, records, and enumerations in a package;
  • compile packages before files that depend on them;
  • separate RTL, testbench, constraints, scripts, and generated artifacts;
  • avoid package patterns that make reuse and review harder.

Why numeric_std Matters

std_logic_vector is a bundle of bits. It does not say whether the bits represent an unsigned number, a signed number, an address, a status field, or unrelated control flags. numeric_std makes that intent explicit:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

signal addr : unsigned(7 downto 0);
signal temp : signed(11 downto 0);

addr <= addr + 1;
temp <= temp - 16;

Use unsigned for natural binary values such as counters, lengths, and addresses. Use signed for two's-complement values. Convert to or from std_logic_vector at module boundaries when a port is intentionally bit-oriented.

Avoid Nonstandard Arithmetic Packages

Older code sometimes uses:

use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;

These packages are not the portable standard style. They can hide type mistakes and produce different behavior across tools. For beginner projects, a strict rule works well: arithmetic signals are unsigned or signed, and arithmetic comes from ieee.numeric_std.

A Simple Package

Packages are useful for names that multiple files must share. A UART project, for example, can keep its common widths and state type in one place:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package uart_pkg is
    constant CLK_HZ    : natural := 12_000_000;
    constant BAUD_RATE : natural := 115_200;
    constant DATA_BITS : positive := 8;

    subtype baud_count_t is unsigned(15 downto 0);
    subtype data_byte_t  is std_logic_vector(DATA_BITS - 1 downto 0);

    type uart_tx_state_t is (
        IDLE,
        START_BIT,
        DATA_BITS_STATE,
        STOP_BIT
    );
end package uart_pkg;

Use the package in RTL like this:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.uart_pkg.all;

The work library is the default library where analyzed design units are stored by many VHDL tools, including GHDL.

What Belongs In A Package

Good package contents are stable and shared:

Good candidates Poor candidates
common constants board pin names for one top module
reusable subtypes temporary debug signals
record types generated build output
enumerated states used by several files one-off local state names
function declarations vendor-specific constraints

Keep board-specific constraints in constraint files, not in generic RTL packages. Keep package scope narrow enough that a reviewer can understand why each name is shared.

Compile Order

VHDL is order-sensitive. A file that uses a package must be analyzed after that package:

ghdl -a rtl/uart_pkg.vhd
ghdl -a rtl/baud_tick.vhd
ghdl -a rtl/uart_tx.vhd
ghdl -a tb/uart_tx_tb.vhd
ghdl -e uart_tx_tb
ghdl -r uart_tx_tb --vcd=uart_tx.vcd

If uart_tx.vhd uses work.uart_pkg.all, analyzing uart_tx.vhd first should fail. That failure is helpful because it exposes a real dependency.

Suggested Project Structure

fpga-uart/
  rtl/
    uart_pkg.vhd
    baud_tick.vhd
    uart_tx.vhd
    uart_rx.vhd
    top.vhd
  tb/
    uart_tx_tb.vhd
    uart_rx_tb.vhd
  constraints/
    board.pcf
  sim/
    run_ghdl.sh
    waves.gtkw
  build/
    generated files

rtl/ should be the synthesizable design. tb/ should be excluded from synthesis. constraints/ maps the design to a board. build/ should be disposable and usually ignored by version control.

Project Flow

flowchart LR A["Shared package"] --> B["Reusable RTL"] B --> C["Top module"] A --> D["Testbench"] B --> D C --> E["Constraints"] D --> F["Simulation"] C --> G["Synthesis"]

This order keeps shared definitions visible before dependent code and keeps simulation-only files out of synthesis.

Worked Example: Width From A Package

Package:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package counter_pkg is
    constant COUNT_WIDTH : positive := 8;
    subtype count_t is unsigned(COUNT_WIDTH - 1 downto 0);
end package counter_pkg;

Counter:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.counter_pkg.all;

entity counter is
    port (
        clk   : in  std_logic;
        rst   : in  std_logic;
        en    : in  std_logic;
        count : out std_logic_vector(COUNT_WIDTH - 1 downto 0)
    );
end entity counter;

architecture rtl of counter is
    signal count_r : count_t := (others => '0');
begin
    process(clk)
    begin
        if rising_edge(clk) then
            if rst = '1' then
                count_r <= (others => '0');
            elsif en = '1' then
                count_r <= count_r + 1;
            end if;
        end if;
    end process;

    count <= std_logic_vector(count_r);
end architecture rtl;

The width appears in one package constant, and the counter uses the package type consistently.

Practical Checks

Before committing a VHDL project, verify:

  • every arithmetic file imports ieee.numeric_std.all;
  • arithmetic signals are unsigned or signed, not plain vectors;
  • packages are compiled before dependent design units;
  • testbench files are not included in synthesis file lists;
  • generated files are outside source folders or ignored;
  • package names are specific, such as uart_pkg, not vague names like common;
  • changing a package constant triggers simulation and synthesis reruns.

Common Mistakes

  • Putting unrelated constants into one giant package.
  • Using std_logic_vector for arithmetic and relying on nonstandard packages.
  • Hiding board assumptions inside reusable modules.
  • Forgetting that VHDL compile order matters.
  • Letting simulator-generated files pollute rtl/.
  • Reusing a package name that conflicts with another library.

Summary

Good VHDL structure is not cosmetic. numeric_std makes arithmetic intent explicit, packages make shared definitions reviewable, and a repeatable compile order prevents hidden dependencies. Keep RTL, testbenches, constraints, and generated files separated so the project can grow without becoming fragile.

Next: Simulation and Testbenches.

Further Reading

Mind Map

mindmap root((VHDL Structure)) Core Packages share names numeric_std arithmetic Compile order matters Folders show intent Types unsigned counters signed values vectors for bits subtypes for width Flow package first RTL next testbench after constraints separate Applications UART constants state enums shared records reusable counters Checks no std_logic_unsigned TB not synthesized build files ignored package scope narrow Mistakes giant common pkg hidden board data wrong compile order vector arithmetic