SystemVerilog Package Tutorial: Types, import & Scope ::

As a design grows past a handful of files, the same enum, the same bus width, and the same helper function get copied into module after module. Every copy is a place a bug can hide. The SystemVerilog package fixes this: it is a named, self-contained namespace that holds declarations you want to reuse across modules, interfaces, programs, and testbenches. You compile the package once and then pull its contents into any scope that needs them. This tutorial shows how to declare a package and the three distinct ways to reach what lives inside it.

What a package can hold

A package is a container that sits at the top level of a compilation unit – it is never nested inside a module. Inside it you can declare:

  • Parameters and localparams – shared constants such as a bus width or an address map size.
  • Typedefs – enums, packed structs, unions, and aliases that describe the data your blocks exchange.
  • Functions and tasks – reusable logic such as a parity calculator or a saturating adder.
  • Classes, covergroups, and other verification constructs – useful when the package serves a testbench.

What a package deliberately cannot hold is hardware: no always blocks, no module instances, no ports. It is a library of declarations, not a block of logic.

Declaring a package and using it

The example below declares comms_pkg with one parameter, one enumerated type, and one function. The testbench then imports everything from the package with a wildcard import and uses all three. Open the Simulation log tab to see the output.

package comms_pkg;
  parameter int BUS_WIDTH = 8;

  typedef enum logic [1:0] {IDLE, START, DATA, STOP} state_t;

  function automatic logic parity(input logic [BUS_WIDTH-1:0] d);
    return ^d;  // XOR reduction = parity bit
  endfunction
endpackage

module tb;
  import comms_pkg::*;   // wildcard import: types, params, functions, enum labels

  logic [BUS_WIDTH-1:0] frame;
  state_t st;

  initial begin
    frame = 8'b1011_0010;
    st    = START;
    $display("BUS_WIDTH    = %0d", BUS_WIDTH);
    $display("current state = %s", st.name());
    $display("parity(0x%02h) = %b", frame, parity(frame));
    $finish;
  end
endmodule
BUS_WIDTH    = 8
current state = START
parity(0xb2) = 0
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp4mj23mc_/comms_pkg.sv:23: $finish called at 0 (1s)

One subtlety worth internalizing early: importing the type state_t on its own does not bring in the enumeration labels IDLE, START, and so on – those are separate symbols. The wildcard import above pulls them in together, which is why st = START; compiles. With an explicit import you would need to import each label you use.

Three ways to reach a package member

Once a package exists, SystemVerilog gives you three mechanisms to access its members. They differ in how many names enter your current namespace.

// 1. Explicit import: bring in exactly one symbol
import math_pkg::PI;

// 2. Wildcard import: make every package symbol a candidate
import math_pkg::*;

// 3. No import at all - fully qualify with the :: scope operator
real area = math_pkg::PI * r * r;

Explicit import

Writing import pkg::name; copies a single named declaration into the current scope. This is the most disciplined option: a reader sees precisely which symbols the module depends on, and nothing unexpected leaks in. For reusable IP that others will instantiate, explicit imports keep your dependencies honest.

Scope resolution with ::

The :: operator reaches directly into a package without importing anything. math_pkg::PI names PI inside math_pkg at the point of use. Nothing enters the local namespace, so there is zero risk of a clash – at the cost of more typing. It is the right tool when you touch a package member once or twice, or when two packages expose the same name and you must be unambiguous.

Wildcard import

Writing import pkg::*; makes all of a package’s members candidates for name lookup in the current scope. It is convenient, especially in testbenches, but a wildcard import does not blindly override anything – it only supplies a name when nothing else in scope already defines it. The resolution rules below make this precise.

Scope resolution and two packages at once

This example declares two packages that both define a parameter called WIDTH. The testbench explicitly imports add_sat and WIDTH from alu_pkg, then uses the :: operator to reach into fmt_pkg without importing it – so both WIDTH values coexist without conflict.

package alu_pkg;
  parameter int WIDTH = 4;
  function automatic logic [WIDTH-1:0] add_sat(logic [WIDTH-1:0] a, b);
    logic [WIDTH:0] sum = a + b;                 // one extra bit
    return sum[WIDTH] ? '1 : sum[WIDTH-1:0];     // saturate on overflow
  endfunction
endpackage

package fmt_pkg;
  parameter int WIDTH = 8;    // deliberately different WIDTH
  function automatic int to_decimal(logic [WIDTH-1:0] v);
    return int'(v);
  endfunction
endpackage

module tb;
  import alu_pkg::add_sat;     // bring one function into scope
  import alu_pkg::WIDTH;       // use alu_pkg's WIDTH unqualified

  logic [WIDTH-1:0] r;

  initial begin
    r = add_sat(4'd9, 4'd12);                 // 21 -> saturates to 15
    $display("saturated sum      = %0d", r);
    // scope resolution :: reaches fmt_pkg without importing it
    $display("fmt_pkg::WIDTH     = %0d", fmt_pkg::WIDTH);
    $display("decimal of 8'hA5   = %0d", fmt_pkg::to_decimal(8'hA5));
    $finish;
  end
endmodule
saturated sum      = 15
fmt_pkg::WIDTH     = 8
decimal of 8'hA5   = 165
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpw_vsvvzs/alu_fmt_pkg.sv:28: $finish called at 0 (1s)

Had you tried to wildcard-import both packages into the same module and then write a bare WIDTH, the tool would reject it as ambiguous. The :: operator is your escape hatch in exactly that situation.

Sharing a struct type across a design

The most valuable thing a package carries is often a type. Define a packed struct once, and every module that handles that data agrees on its layout. Here pkt_pkg publishes a packet_t and a factory function; the testbench builds a packet and prints both its fields and its raw packed bit pattern.

package pkt_pkg;
  parameter int ADDR_W = 4;
  parameter int DATA_W = 8;

  // a reusable packed struct shared by many modules
  typedef struct packed {
    logic [ADDR_W-1:0] addr;
    logic [DATA_W-1:0] data;
    logic              valid;
  } packet_t;

  function automatic packet_t make_packet(logic [ADDR_W-1:0] a,
                                           logic [DATA_W-1:0] d);
    packet_t p;
    p.addr  = a;
    p.data  = d;
    p.valid = 1'b1;
    return p;
  endfunction
endpackage

module tb;
  import pkt_pkg::*;

  packet_t pkt;

  initial begin
    pkt = make_packet(4'hA, 8'h3C);
    $display("addr=0x%0h data=0x%0h valid=%b", pkt.addr, pkt.data, pkt.valid);
    $display("raw packed bits = %b", pkt);   // 4 + 8 + 1 = 13 bits
    $finish;
  end
endmodule
addr=0xa data=0x3c valid=1
raw packed bits = 1010001111001
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpui9d7j__/pkt_pkg.sv:31: $finish called at 0 (1s)

How name resolution really works

Understanding the precedence rules keeps wildcard imports from biting you. For a given name in a scope:

SituationWhat the compiler does
A local declaration has the same nameThe local name wins; any wildcard candidate is hidden.
An explicit import names itThe explicit import wins over every wildcard import.
Two wildcard imports both export itNeither is chosen; using the bare name is an error. Qualify it with ::.
The name is used, then re-declared laterOnce a name has been resolved from a package in a scope, a later conflicting declaration of that name is illegal.

Best practices

  • Prefer explicit imports in reusable IP; keep wildcard imports for testbenches and tightly coupled blocks where convenience outweighs leakage.
  • Give each package a single clear responsibility and a _pkg suffix so its role is obvious at a glance.
  • Declare package functions and tasks as automatic so they are re-entrant when called from concurrent processes.
  • When two packages might expose the same name, reach for the :: operator instead of hoping the import order saves you.
  • A package must be analyzed before any file that imports it – order your compile or file list accordingly.

Packages turn scattered, copy-pasted declarations into a single source of truth. Start by moving your shared widths and types into one, import only what each block needs, and let the :: operator settle any remaining ambiguity.

Quick check

Does importing a package’s enum type also import its enum labels?

No. Importing the type with import pkg::state_t; brings in only the type name. The labels such as IDLE or START are separate symbols – import them individually, or use a wildcard import to pull them all in at once.

Two packages you wildcard-import both define WIDTH. You write a bare WIDTH. What happens?

It is a compile error: the reference is ambiguous because neither wildcard import takes precedence. Resolve it by fully qualifying the name, for example alu_pkg::WIDTH, or by adding an explicit import of the one you mean.

When should you use the :: scope operator instead of an import?

Use :: when you touch a package member only once or twice, or when you need an unambiguous reference while another name of the same spelling is in scope. It adds nothing to the local namespace, so it never causes a clash.

Why can a package not contain an always block or a module instance?

A package is a namespace of declarations – types, parameters, and subroutines – not hardware. Procedural blocks and instances describe behaviour and structure, which belong inside modules. A package exists purely to be shared and imported.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD