Skip to content

SystemVerilog · Module 16

Package Declaration & Usage

SystemVerilog package — the named compilation scope that bundles parameters, types, functions, tasks, and classes for sharing across modules, interfaces, and classes. The :: scope-resolution operator, what is legal vs illegal inside a package, the function automatic discipline, and the canonical protocol-package and verification-utility patterns that all production VIPs (including UVM) are built on.

Module 16 · Page 16.1

A SystemVerilog package is a named compilation scope that bundles parameters, types, functions, tasks, and classes so they can be shared by any number of modules, interfaces, classes, and programs without re-declaration. It is not a hardware block — it has no ports, no always blocks, no instances — it is purely a compile-time namespace container. Every modular SystemVerilog codebase, every production protocol VIP, and every UVM environment is built on packages: get the package model wrong and the whole codebase becomes a maintenance trap of ``includecycles, redefinition errors, and silent static-locals bugs. This lesson covers the declaration syntax, the::scope-resolution operator, what is legal inside a package, thefunction automatic` discipline, and the canonical patterns (protocol package, verification utility package) that every senior engineer has memorised.

1. Engineering Problem — Why Packages Exist

Before SystemVerilog had packages, every module that needed a shared type or constant had two bad options:

  • **``includethe same header file from every consumer** — text substitution at the point of inclusion. Identical type declarations appear in every compilation unit. Tools cannot tell that the twoaxi_ar_ttypedefs in modulesAandB` are meant to be the same type; a small drift between two copies becomes a silent semantic bug. Worse, including the same file from two compile units in the same scope triggers a redefinition error.
  • Repeat the declaration verbatim in every module — pure copy-paste. Maintenance becomes a search-and-replace exercise across the entire codebase; one missed file is a real-silicon bug.

The Verilog community lived with these workarounds for years. SystemVerilog (IEEE 1800, §26) replaced them with a proper language construct: a package is compiled once, lives in its own named namespace, and any consumer accesses its members through the unambiguous package_name::member form. There is exactly one definition of axi4_pkg::axi_ar_t in the entire elaboration — tools track it, consumers reference it, and a change to the package automatically propagates to every importer at the next compile.

Equally important: packages give SystemVerilog the modular-design foundation that classes alone cannot provide. Classes live inside packages; protocol constants live in packages; transaction types live in packages; UVM itself is shipped as a single package (uvm_pkg). Without packages, every multi-file SystemVerilog project would collapse into the same ``include` chaos the language was supposed to escape.

2. Mental Model — Package = Named Library

The picture every engineer carries:

A package is a library with a name on the door. Inside the library: any number of parameters, type aliases, functions, tasks, and classes. The door is the package name. Any consumer in the design — a module, an interface, a class, a program, another package — reaches in by writing package_name::member. No imports are needed for this form; it always works as long as the package has been compiled. The import statement (covered in 16.2) is a shorthand that lets a consumer omit the package_name:: prefix.

Four invariants this picture preserves:

  • The package is the only owner of its members. There is exactly one axi4_pkg::burst_e in the entire elaboration. Two modules that reference axi4_pkg::INCR are guaranteed to be talking about the same enum literal of the same type — no copy-paste drift possible.
  • Packages have no hardware footprint. Synthesis tools elaborate package types and parameters into the consumers that use them; the package itself produces no gates, no flip-flops, no nets. A package is purely a compile-time namespace, exactly like a C++ namespace or a Java package.
  • Packages are compiled before their consumers. This is a hard build-order constraint enforced by every simulator and synthesis tool. A file that references axi4_pkg::axi_ar_t cannot be elaborated until axi4_pkg.sv has been compiled. (Module 16.4 covers this in detail.)
  • The scope-resolution operator :: is the universal access mechanism. It works from any context — module, interface, class, program, another package — and is unambiguous even when two packages export the same name. Use it explicitly even after a wildcard import when you want the reader to see exactly which package a name comes from.

3. Visual Explanation — Package Scope Diagram

The figure below shows the package as a named scope boundary, the items it owns, and the ::-qualified access pattern from a consumer module.

The package axi4_pkg owns a parameter, two typedefs, and a function. A consumer module dut below reaches into the package using axi4_pkg::member scope resolution.:: scope-resolution access::scope-resolutio…package axi4_pkgparameter int AXI_DATA_W = 64parameter int AXI_DATA_W = 64typedef enum { INCR, WRAP } burst_etypedef enum { INCR, WRAP } burst_etypedef struct packed {…} axi_ar_ttypedef struct packed {…} axi_ar_tfunction automatic int crc32(…)function automatic int crc32(…)module dutaxi4_pkg::axi_ar_t req;axi4_pkg::axi_ar_t req;axi4_pkg::burst_e mode = axi4_pkg::INCR;axi4_pkg::burst_e mode = axi4_pkg::INCR;axi4_pkg::crc32(…)axi4_pkg::crc32(…)
Figure 1 — a package is a named scope that owns its members; a consumer reaches in with the :: scope-resolution operator. The package name is the door; every member is reachable as pkg_name::member.

The :: operator is the explicit, unambiguous way to reach any package member. It works from any context — module, interface, class, program, or another package. The import statement (16.2) is the shorthand that lets you drop the package_name:: prefix.

Three properties of this picture worth carrying forward:

  • The boundary is the package name. Two packages can declare identical member names (pkg_a::txn_t and pkg_b::txn_t) and neither shadows the other — the :: form keeps them distinct.
  • There is no extern / forward problem for package contents. Anything inside a package is reachable by :: immediately once the package is compiled — no forward declarations, no header/implementation split.
  • The arrow goes one way. Consumers reach into the package; the package never reaches out. A package never references a module or signal — that would couple it to a specific elaboration and destroy its reusability.

4. Syntax — Package Declaration Skeleton

A package body is wrapped in package ... endpackage. The trailing label after endpackage : is optional but strongly recommended — tools and linters use it to verify the block is closed correctly and to catch typos when the label is misspelled.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Minimal package ──────────────────────────────────────────────
package my_pkg;
    // declarations go here
endpackage : my_pkg   // trailing label is optional but recommended
 
// ── Full anatomy of a real package ──────────────────────────────
package axi4_pkg;
 
    // 1. Parameters / localparams
    parameter int  AXI_DATA_W = 64;
    parameter int  AXI_ADDR_W = 32;
    localparam int AXI_STRB_W = AXI_DATA_W / 8;
 
    // 2. Typedef — enum
    typedef enum logic [1:0] {
        FIXED = 2'b00,
        INCR  = 2'b01,
        WRAP  = 2'b10
    } burst_type_e;
 
    // 3. Typedef — struct
    typedef struct packed {
        logic [AXI_ADDR_W-1:0] addr;
        logic [7:0]            len;
        burst_type_e           burst;
    } axi_ar_t;
 
    // 4. Automatic function — pure utility, one stack frame per call
    function automatic logic [3:0] byte_enables(input logic [2:0] size);
        return (1 << (1 << size)) - 1;
    endfunction
 
    // 5. Class definition
    class Axi4Transaction;
        axi_ar_t ar_chan;
        function new(); ar_chan = '0; endfunction
    endclass
 
endpackage : axi4_pkg

The five categories above (parameter, typedef, function, task, class) are the only declarations a package body can contain. Anything procedural, anything that allocates hardware, and any module/interface instance is a compile error inside a package. The full restriction list:

5. What Can Go Inside a Package?

The LRM (IEEE 1800-2017, §26.2) gives the legal items. The table below covers every category you will encounter in practice.

CategoryConstructTypical use
Parametersparameter, localparamBus widths, depths, protocol constants shared across DUT and TB
Type aliasestypedef (enum, struct, union, class)Shared data types — transaction structs, opcode enums, scoreboard types
Functionsfunction automaticPure utility functions — CRC computation, encoding, field extraction
Taskstask automaticShared simulation helpers — delay routines, print formatting
Classesclass ... endclassReusable transaction objects, base classes, scoreboards
Nested importsimport other_pkg::*;Pulling a dependency package's names into the current package scope
Text includes`include "file.svh"Splitting a large package across files while keeping a single compile unit

5b. The :: Operator — Universal Access

Every item inside a package is reachable via the scope-resolution operator ::. You write package_name::member_name. No import is required for this form — it always works as long as the package has been compiled before the consumer.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Package definition (compile this first) ──────────────────────
package apb_pkg;
    parameter int APB_ADDR_W = 16;
    parameter int APB_DATA_W = 32;
 
    typedef enum logic {
        APB_IDLE  = 1'b0,
        APB_SETUP = 1'b1
    } apb_phase_e;
 
    typedef struct packed {
        logic [APB_ADDR_W-1:0] paddr;
        logic [APB_DATA_W-1:0] pwdata;
        logic                  pwrite;
        logic                  psel;
        logic                  penable;
    } apb_txn_t;
 
    function automatic logic [APB_DATA_W-1:0] mask_data(
        input logic [APB_DATA_W-1:0] data,
        input logic [3:0]            strb
    );
        for (int i = 0; i < 4; i++)
            if (!strb[i]) data[i*8 +: 8] = 8'h00;
        return data;
    endfunction
endpackage : apb_pkg
 
// ── Module consuming the package — no import, pure :: access ────
module apb_slave #(
    parameter int ADDR_W = apb_pkg::APB_ADDR_W,   // :: in parameter default
    parameter int DATA_W = apb_pkg::APB_DATA_W
) (
    input  apb_pkg::apb_txn_t  req,               // :: in port type
    input  logic               clk,
    output logic [DATA_W-1:0]  prdata,
    output logic               pready
);
    apb_pkg::apb_phase_e  phase;                  // :: in signal declaration
    apb_pkg::apb_txn_t    captured;
 
    always_ff @(posedge clk) begin
        if (req.psel && !req.penable) begin
            phase    <= apb_pkg::APB_SETUP;
            captured <= req;
        end else begin
            phase <= apb_pkg::APB_IDLE;
        end
    end
 
    // Call a package function directly with ::
    assign prdata = apb_pkg::mask_data(captured.pwdata, 4'hF);
    assign pready = (phase == apb_pkg::APB_SETUP);
endmodule

Notice every use site of the package:

  • Parameter defaultapb_pkg::APB_ADDR_W
  • Port typeapb_pkg::apb_txn_t
  • Internal signal typeapb_pkg::apb_phase_e
  • Enum literalapb_pkg::APB_SETUP
  • Function callapb_pkg::mask_data(...)

The :: form is legal in every one of these positions and reads identically regardless of how deep you nest packages. The companion Class Scope Resolution page (Module 9.16) covers the related uses of :: for static class members, super::, and parameterised-class specialisations.

6. Waveform — Omitted (Architectural Lesson)

This lesson is architectural, not timing-specific. Packages produce no simulation events — they are pure compile-time constructs. Adding a waveform here would be misleading. The timing behaviour of the constructs inside a package (clocked logic that consumes package types, classes that schedule events) is covered in the relevant timing pages: program-block for the Reactive-region scheduling, clocking-blocks-deep-dive for the sampling timing of clocked package types. This section is intentionally omitted; the topic does not warrant it.

7. Synthesis View — Compile-Time Only

A package itself produces no hardware. Synthesis tools treat the package as a type and constant catalog: when a module references axi4_pkg::axi_ar_t, the synthesis tool elaborates the struct into the bit-vector signals that the consuming module needs, then forgets the package. There is no flip-flop, no net, no gate that traces back to the package declaration.

The members of a package, however, may or may not be synthesizable depending on what they are:

Package memberSynthesizable?Notes
parameter / localparamYesResolved to a constant during elaboration
typedef (enum, struct, union)YesElaborated into bit-vector types in the consumer
function automatic (combinational)YesInlined into the consumer's combinational logic
task automaticNo (sim-only)Tasks may contain # delays, wait, @ — never synthesisable
classNo (sim-only)Classes are dynamic objects — verification only
function automatic with #/waitNoSame reason as tasks

The takeaway: a package is universally synthesisable as a construct. Whether a given member synthesises depends on the standard synthesisable-subset rules, applied at the consumer's elaboration site. A protocol-constant package shared by DUT and testbench is a textbook example of safe dual-use: the DUT pulls in only the synthesisable parameters and types; the testbench additionally pulls in the classes and tasks.

8. Package vs Other Sharing Mechanisms

Three older mechanisms exist for sharing declarations in SystemVerilog. The decision matrix below is what every code review applies.

✅ package — the modern default

How it works: named language scope; compiled once; referenced with :: or import.

Namespace: isolated — pkg::name. Two packages with the same member name never collide.

Best for: types, parameters, functions shared across DUT + TB. Anything you want a name on.

Avoid when: never — always prefer this.

○ `include — legacy, situational

How it works: textual paste at the point of inclusion. Like C's #include. No language-level scope.

Namespace: pollutes the surrounding scope. Same identifier in two includers in the same compile unit is a redefinition error.

Best for: legacy code; tool-generated headers not yet ported to packages; splitting a single package across multiple files.

Avoid when: new code declaring shared types — use a package instead. The order of ``include` directives changes the semantics; trivially broken by refactoring.

❌ `define — text macros, no type

How it works: global text macro — preprocessor-level substitution. No type, no scope, no namespace.

Namespace: global. A ``define WIDTH 32` in any file affects every later file in the compile unit.

Best for: conditional compilation flags (``ifdef SIMULATION`), tool-version guards, header-include guards.

Avoid when: declaring constants that have a type — use a parameter in a package instead. ``define` constants don't appear in the symbol table, don't show up in waveforms, and silently corrupt expressions through unexpected text expansion.

❌ $unit declarations — rarely correct

How it works: items declared outside any module at the top of a file — visible to the whole compilation unit ("$unit scope").

Namespace: file-level, no named prefix. Order-dependent on the file-list ordering.

Best for: almost nothing in modern code. Sometimes used as a quick-and-dirty cross-file constant during prototyping.

Avoid when: any shared code that ships. Different simulators handle $unit scope differently; the same source can produce different results between tools.

The rule that follows: prefer package for every named, typed, shared declaration; reserve `include for legacy and for splitting a package across files; reserve `define for conditional compilation only; avoid $unit entirely.

9. Real-World Patterns

The two canonical patterns every production verification environment uses.

9.1 Protocol Parameter Package

A shared protocol package consumed by both the DUT and the testbench. The same constants, types, and utility functions live in one file; the DUT pulls them in for synthesis, the testbench pulls them in for stimulus and checking.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── File: i2c_pkg.sv ──────────────────────────────────────────────
package i2c_pkg;
 
    // Protocol constants
    parameter int ADDR_BITS    = 7;
    parameter int DATA_BITS    = 8;
    parameter int SCL_FREQ_KHZ = 400;
 
    // Derived localparam — computed once, used everywhere
    localparam int FRAME_BITS = ADDR_BITS + 1 + DATA_BITS + 1; // addr+RW+data+ACK
 
    // Opcode enum
    typedef enum logic {
        I2C_WRITE = 1'b0,
        I2C_READ  = 1'b1
    } i2c_dir_e;
 
    // Transfer descriptor
    typedef struct packed {
        logic [ADDR_BITS-1:0] addr;
        i2c_dir_e             dir;
        logic [DATA_BITS-1:0] data;
    } i2c_frame_t;
 
    // Utility function — pack 7-bit addr + R/W into the on-wire byte
    function automatic logic [7:0] addr_byte(
        input logic [ADDR_BITS-1:0] a,
        input i2c_dir_e             dir
    );
        return {a, dir};
    endfunction
 
endpackage : i2c_pkg
 
// ── DUT references the package ────────────────────────────────────
module i2c_master (
    input  logic                  clk, rst_n,
    input  i2c_pkg::i2c_frame_t   req,
    input  logic                  req_valid,
    output logic                  scl, sda
);
    // Implementation uses i2c_pkg::ADDR_BITS, i2c_pkg::I2C_WRITE, etc.
endmodule
 
// ── Testbench references the SAME package ─────────────────────────
module tb_i2c;
    i2c_pkg::i2c_frame_t  stim;
 
    initial begin
        stim.addr = 7'h50;
        stim.dir  = i2c_pkg::I2C_WRITE;
        stim.data = 8'hA5;
        $display("Address byte: %02h", i2c_pkg::addr_byte(stim.addr, stim.dir));
    end
endmodule

Why this pattern matters: a change to ADDR_BITS or to addr_byte() is automatically reflected on both sides of the DUT-TB boundary. Without a package, you would have to remember to update both copies — a class of bug that has caused real-silicon respins.

9.2 Verification Utility Package

A shared utility package — logging, checking, summary reporting — consumed by every component of a testbench. This is the pattern UVM generalises into uvm_pkg (just with a much larger surface area).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── File: tb_utils_pkg.sv ─────────────────────────────────────────
package tb_utils_pkg;
 
    // Severity type
    typedef enum { SEV_INFO, SEV_WARN, SEV_ERROR, SEV_FATAL } sev_e;
 
    // Deliberately shared counters — package-scope (not function-local)
    int error_count = 0;
    int check_count = 0;
 
    // Log function — formats and prints
    function automatic void log_msg(
        input string  tag,
        input string  msg,
        input sev_e   sev = SEV_INFO
    );
        string prefix;
        case (sev)
            SEV_INFO  : prefix = "[INFO ]";
            SEV_WARN  : prefix = "[WARN ]";
            SEV_ERROR : prefix = "[ERROR]";
            SEV_FATAL : prefix = "[FATAL]";
        endcase
        $display("%0t %s [%s] %s", $time, prefix, tag, msg);
        if (sev == SEV_FATAL) $fatal(1);
    endfunction
 
    // Checker — accumulates errors
    function automatic void check_eq(
        input string  tag,
        input longint got,
        input longint exp
    );
        check_count++;
        if (got !== exp) begin
            error_count++;
            log_msg(tag, $sformatf("MISMATCH got=%0h exp=%0h", got, exp), SEV_ERROR);
        end
    endfunction
 
    // Final report — call from test end
    function automatic void report_summary(input string test_name);
        if (error_count == 0)
            $display("[PASS] %s%0d checks passed", test_name, check_count);
        else
            $display("[FAIL] %s%0d/%0d checks failed",
                     test_name, error_count, check_count);
    endfunction
 
endpackage : tb_utils_pkg
 
// ── Any testbench component uses it with :: ───────────────────────
module test_top;
    initial begin
        tb_utils_pkg::log_msg("TEST", "Starting reset sequence");
        // apply_reset();
        tb_utils_pkg::check_eq("DUT", 32'h0000_0001, 32'h0000_0001);
        tb_utils_pkg::report_summary("smoke_test");
    end
endmodule

The key discipline this example shows: shared state (error_count, check_count) is at package scope, not function-local static. The intent is explicit and the reader can see the shared state at a glance. Functions are automatic so each call gets its own stack frame — the only shared part is the explicit counter.

10. Debug Lab — Six Package Bugs

The bugs every reviewer flags. Each follows the post-mortem pattern: symptom → root cause → fix → guardrail.

Package declaration & usage — bugs every engineer makes once

1. Procedural code in package body

Symptom: Compile error — "initial / always not allowed in package" on the line that opens the procedural block.

Cause: A package is a compile-time namespace, not a simulation context. There are no simulation events at package scope — no time wheel to schedule initial or always against.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
package bad_pkg;
    initial begin           // not allowed in package
        $display("hello");
    end
endpackage

Fix: Move the procedural code to a module, program, or testbench. Keep only declarations in the package.

Guardrail: a package body should contain only parameter / localparam / typedef / function automatic / task automatic / class / nested import / ``include`. Any other top-level keyword is a red flag.

2. Non-automatic function — shared static locals

Symptom: Two concurrent testbench components calling the same package function get interleaved results. A counter inside the function does not reset between calls.

Cause: Package-scope functions default to static lifetime when the keyword is omitted. A single copy of all local variables is shared across every caller in the entire elaboration.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG — silent corruption under concurrent calls
package bad_pkg2;
    function int get_id();   // static by default — ONE copy of all locals
        int cnt = 0;         // BUG: cnt persists across all callers
        return cnt++;
    endfunction
endpackage

Fix: Write function automatic int get_id() — each call gets its own stack frame with a fresh cnt.

Guardrail: every function and task in a package must be automatic. Make this a lint rule; it costs nothing and catches a real class of bug.

3. endpackage label mismatch

Symptom: Compile error — "label 'utils_pkg' does not match package name 'util_pkg'". Usually a one-letter typo.

Cause: The trailing endpackage : <name> label must match the opening package name exactly — SystemVerilog identifiers are case-sensitive.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
package util_pkg;
    parameter int MAX = 64;
endpackage : utils_pkg    // label 'utils_pkg' != 'util_pkg'

Fix: endpackage : util_pkg — match the opening name character-for-character.

Guardrail: always include the trailing label. The compile error is exactly what catches you when you rename the package and forget to update the label.

4. Declaring a net or signal in a package

Symptom: Compile error — "net declarations not allowed inside package" on the wire or logic line.

Cause: Packages hold types and constants, not signal instances. A wire or logic declaration creates a hardware net — but a package has no module to host the net, no scope to drive it, no connectivity to receive it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
package bad_pkg3;
    wire clk;              // net declarations not allowed
    logic [7:0] reg_data;  // signal-style variable also illegal
endpackage

Fix: Declare a typedef for the type, then have the consuming module instantiate the signal:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package good_pkg;
    typedef logic [7:0] reg_data_t;   // type alias is fine
endpackage
 
module dut;
    good_pkg::reg_data_t reg_data;    // signal lives in the module
endmodule

Guardrail: if you find yourself reaching for wire or signal-typed logic in a package, you are putting hardware in the wrong place. The package owns the type; the consumer owns the signal.

5. Instantiating a module inside a package

Symptom: Compile error — "module instance not allowed inside package". Often surfaces when someone tries to share a clock generator or a memory model via a package.

Cause: An instance ties a piece of hardware to a specific point in the elaboration hierarchy. Packages have no hierarchy — they are flat namespaces. There is no place in the design for the instance to live.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
package bad_pkg4;
    clk_gen u_clk();    // module instance not allowed
endpackage

Fix: Instances belong in a parent module (or a bind construct for verification helpers). The package can still hold the parameters and types the instance needs, just not the instance itself.

Guardrail: "instance" and "package" are mutually exclusive concepts. If you need an instance, the right container is a module.

6. Case-sensitive package-name typo

Symptom: Compile error — "package 'apb_Pkg' not found" despite apb_pkg.sv existing on the file list.

Cause: SystemVerilog identifiers are case-sensitive. apb_Pkg and apb_pkg are two different names — the compiler does not auto-correct.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
logic [31:0] data = apb_Pkg::APB_DATA_W;  // 'apb_Pkg' vs 'apb_pkg'

Fix: Match the exact case of the package name as declared. The convention is all-lowercase with underscores (apb_pkg, axi4_pkg, i2c_pkg) — pick this convention and stick to it across the codebase.

Guardrail: industry convention is one package per file, file name matches package name, all lowercase. Following this convention eliminates the entire class of case-mismatch errors.

11. Q & A

The questions that come up in code review and interviews.

12. Cross-References & What's Next

This lesson covered the foundational mechanics of a SystemVerilog package — declaration, the :: operator, what is legal inside, and the canonical protocol-package / utility-package patterns. The rest of Module 16 takes the package model to production depth.

  • Next: 16.2 — import (Explicit & Wildcard) — the shorthand that lets you omit the pkg_name:: prefix, the difference between explicit (import pkg::name) and wildcard (import pkg::*), the name-resolution rules when two packages export the same name, and the in-port-list vs in-body placement choices.
  • 16.3 — Package-Level Parameters & Types — the canonical pattern for protocol constants (bus widths, depths, opcodes) and shared transaction types; how parameter packages flow through parameterised modules and parameterised classes.
  • 16.4 — Package Dependencies & Compilation Order — the build-order discipline that keeps multi-package projects deterministic; the dependency graph as a DAG, circular dependencies and how to break them with a shared base package, and common file-list patterns.

Related material elsewhere in the curriculum:

  • Class Scope Resolution (::) (Module 9.16) — the same :: operator applied to static class members, super, and parameterised-class specialisations.
  • typedef class — Forward Declarations (Module 9.15) — breaking circular references between mutually-dependent classes within one scope; 16.4 explains why this differs from breaking circular dependencies between packages.
  • What UVM Is — and What It Is Not — UVM is shipped as the single uvm_pkg package; without the package construct, UVM literally could not exist.

13. Quick Reference

The cheat sheet to keep open while writing your first package.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Declaration skeleton ─────────────────────────────────────────
package pkg_name;
    parameter int          CONST  = 42;
    typedef enum   { A, B } myenum_e;
    typedef struct packed { logic [7:0] f; } mystruct_t;
    function automatic int my_fn(input int x); return x*2; endfunction
    class MyClass; /* ... */ endclass
endpackage : pkg_name   // trailing label is optional but best practice
 
// ── Accessing members with :: (no import needed) ─────────────────
pkg_name::CONST            // parameter
pkg_name::myenum_e         // type
pkg_name::A                // enum literal
pkg_name::mystruct_t       // struct type
pkg_name::my_fn(3)         // function call
 
// ── What IS allowed in a package ────────────────────────────────
// parameter / localparam
// typedef (enum, struct, union, class)
// function automatic / task automatic
// class definitions
// import other_pkg::*;  (nested import — see 16.2)
// `include "file.svh"
 
// ── What is NOT allowed in a package ────────────────────────────
// initial / always / final blocks
// module / interface instances
// wire / logic signal declarations (only type aliases)
// port declarations
// generate constructs
 
// ── Eight rules to live by ──────────────────────────────────────
// 1.  Packages are compile-time only — no hardware, no simulation events.
// 2.  Compile packages BEFORE any file that references them.
// 3.  Always use 'function automatic' — static functions share locals globally.
// 4.  :: works from any context; import (wildcard or explicit) is a shorthand.
// 5.  Package names are case-sensitive — pkg_name != Pkg_Name.
// 6.  Trailing endpackage label must match the opening name exactly.
// 7.  Prefer packages over `include for all shared type/param declarations.
// 8.  One package per file, file name matches package name — industry convention.

14. Summary

A SystemVerilog package is the named compilation scope that bundles parameters, types, functions, tasks, and classes for sharing across the entire design and verification hierarchy. It is purely a compile-time construct — no hardware, no simulation events, no instances — and that restriction is what makes it the only correct way to share typed declarations in modern SystemVerilog. The :: scope-resolution operator gives any consumer unambiguous access from any context (module, interface, class, program, another package); the import statement (covered in 16.2) is a shorthand on top of ::.

The discipline that follows from the model:

  • function automatic always — static package functions share locals globally and silently corrupt concurrent simulations.
  • Trailing endpackage : pkg_name label — costs nothing, catches typos at compile time.
  • One package per file, file name matches package name, all-lowercase identifiers — the industry convention that eliminates case-mismatch errors and makes file-list ordering predictable.
  • Prefer package over `include and `define for every named, typed, shared declaration. The older mechanisms still have a place (legacy code; conditional compilation; splitting a large package across files), but they are not the default.

This page established the foundation. The next three pages of Module 16 build on it: import semantics (16.2), the canonical protocol-parameter and shared-type patterns (16.3), and the compilation-order discipline that keeps multi-package builds deterministic (16.4). By the end of the module you will have the package model that every production VIP and every UVM environment is built on.