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
endmoduleBUS_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
endmodulesaturated 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
endmoduleaddr=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:
| Situation | What the compiler does |
|---|---|
| A local declaration has the same name | The local name wins; any wildcard candidate is hidden. |
| An explicit import names it | The explicit import wins over every wildcard import. |
| Two wildcard imports both export it | Neither is chosen; using the bare name is an error. Qualify it with ::. |
| The name is used, then re-declared later | Once 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
_pkgsuffix so its role is obvious at a glance. - Declare package functions and tasks as
automaticso 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.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
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