Verilog has significant limitations around global declarations, especially functions and user-defined types:

  • Local Scope — every object declared inside a Verilog module is local to that module, so any function or task needed elsewhere has to be re-declared in every module that uses it, leading to redundant code and extra maintenance.
  • Hierarchical References — Verilog allows reaching into another module's objects hierarchically, but these references are for verification purposes only and don't represent real hardware, so they aren't synthesizable.
  • User-Defined Types — a type needed in multiple modules has to be redefined in each one, without a mechanism for declaring it once, globally.

A SystemVerilog package solves all three: a way to store and share data, methods, properties, and parameters reusable across multiple modules, interfaces, or programs. A package's scope sits at the same level as a top-level module, and keeping definitions between package and endpackage prevents clutter in the global namespace.

16 min read | Intermediate Level

What You'll Learn

  • The specific Verilog limitations packages solve
  • Basic package syntax, and what's allowed to go inside one
  • How wildcard imports differ from importing specific items
  • The exact order SystemVerilog uses to resolve an identifier, and why local declarations always win
  • Whether importing a package that itself imports another package gives you transitive access to both
  • What parts of a package are synthesizable, and which uses are simulation-only

SystemVerilog Package Syntax


package ;
  // Typedef declarations
  // Function/Task definitions
  // ...
endpackage

A package is a separate namespace, not embedded inside any module.

A package cannot contain hierarchical references, except those created within the package itself or through an import.

A Basic Package Example


package my_pkg;

    // Create typedef declarations that can be reused in multiple modules
	typedef enum bit [1:0] { RED, YELLOW, GREEN, RSVD } e_signal;

	typedef struct { bit [3:0]   signal_id;
                     bit         active;
                     bit [1:0]   timeout;
                   } e_sig_param;

    // Create function and task defintions that can be reused
    // Note that it will be a 'static' method if the keyword 'automatic' is not used
	function int calc_parity ();
      $display ("Called from somewhere");
   	endfunction
  
endpackage

Importing a Package

A package is brought into the current scope with import and the scope resolution operator :::


import ::*; // Imports all items
import ::; // Imports specific item

Wildcard Import

A wildcard import makes a package's declarations visible — but only the ones actually referenced in the importing scope get pulled in; anything unused stays unimported:


// Import the package defined above to use e_signal
import my_pkg::*;

class myClass;
	e_signal 	m_signal;
endclass

module tb;
	myClass cls;

	initial begin
		cls          = new();
		cls.m_signal = GREEN;
		$display ("m_signal = %s", cls.m_signal.name());
		calc_parity();
	end
endmodule

Output

Output
ncsim> run
m_signal = GREEN
Called from somewhere
ncsim: *W,RNQUIE: Simulation is complete.

Without the import statement, this same code fails to compile at all:

Output
	e_signal 	m_signal;
	       |
ncvlog: *E,NOIPRT (testbench.sv,15|8): Unrecognized declaration 'e_signal' could be an unsupported keyword, a spelling mistake or missing instance port list '()' [SystemVerilog].
		cls.m_signal = GREEN;
		                 |
ncvlog: *E,UNDIDN (testbench.sv,23|19): 'GREEN': undeclared identifier [12.5(IEEE)].

Importing Specific Items

Instead of a wildcard, individual items can be imported one at a time — more explicit, but more overhead as more items are needed:


import my_pkg::GREEN;
import my_pkg::e_signal;
import my_pkg::common;

Omitting any one of these causes a compile error, since the compiler has no other way to know where that identifier comes from. Note that this applies per-label, not per-enum — importing the type doesn't automatically import every value that belongs to it:


// Although e_signal is made visible, enumerated labels will not be made visible
import my_pkg::e_signal;

// Import each enumerated label instead, if you are not importing using wildcard ::*
import my_pkg::RED;
import my_pkg::GREEN;
import my_pkg::YELLOW;
// etc

Quick Check

Q: If only import my_pkg::e_signal; is written, and code later tries to use RED unqualified, will that compile?

Show Answer

No. Importing the type e_signal doesn't import its individual enum labels — RED, GREEN, and the rest each need their own explicit import statement (or a wildcard import instead).

Search Order: Why Local Always Wins

Local definitions inside a module or interface always take priority over a wildcard import. An import naming a specific item also takes precedence over a wildcard import. A wildcard import just extends the search: the tool looks for local declarations first, then wildcard-imported packages, and finally $unit's own declaration space — matching the same local-shadows-import rule covered in SystemVerilog Scope Resolution Operator.


package my_pkg;
	typedef enum bit { READ, WRITE } e_rd_wr;
endpackage

import my_pkg::*;

module tb;
  typedef enum bit { WRITE, READ } e_wr_rd;

	initial begin
        e_wr_rd  	opc1 = READ;
        e_rd_wr  	opc2 = READ;
      $display ("READ1 = %0d READ2 = %0d ", opc1, opc2);
	end
endmodule

Output

Output
ncsim> run
READ1 = 1 READ2 = 1
ncsim: *W,RNQUIE: Simulation is complete.

Even though opc2 is declared with the package's type (e_rd_wr), the unqualified READ assigned to it still resolves to the module's own locally-declared READ (value 1, from e_wr_rd's reversed order), not the package's READ (value 0). The variable's declared type doesn't change which READ gets resolved — that's still governed by the search order above.

Forcing the package's own value requires an explicit :::


module tb;
	initial begin
        e_wr_rd  	opc1 = READ;
        e_rd_wr  	opc2 = my_pkg::READ;
      $display ("READ1 = %0d READ2 = %0d ", opc1, opc2);
	end
endmodule

Output

Output
ncsim> run
READ1 = 1 READ2 = 0
ncsim: *W,RNQUIE: Simulation is complete.

Quick Check

Q: In the first version above, why does opc2 get 1 instead of the package's READ = 0, given that opc2's type comes from the package?

Show Answer

Because a variable's declared type and the resolution of an unqualified identifier used as its value are two separate things. e_rd_wr (the type) isn't shadowed locally, so it resolves from the package fine — but the literal READ on the right-hand side is a separate identifier lookup, and the module's own local READ shadows the package's, regardless of what type is being assigned to.

Nested Package References

Packages can import other packages. Here, package Y imports package X, and package Z imports package Y:


// Define a new package called X
package X;
  byte    lb     = 8'h10;
  int     word   = 32'hcafe_face;
  string  name = "X";

  function void display();
    $display("pkg=%s lb=0x%0h word=0x%0h", name, lb, word);
  endfunction
endpackage

// Define a new package called Y, use variable value inside X within Y
package Y;
  import X::*;

  byte    lb   = 8'h10 + X::lb;
  string  name = "Y";

  function void display();
    $display("pkg=%s lb=0x%0h word=0x%0h", name, lb, word);
  endfunction
endpackage

// Define a new package called Z, use variable value inside Y within Z
package Z;
  import Y::*;  

  byte    lb   = 8'h10 + Y::lb;
  string  name = "Z";
  
  function void display();
    // Note that 'word' exists in package X and not in Y, but X
    // is not directly imported here in Z, so the statement below
    // will result in a compilation error
    //$display("pkg=%s lb=0x%0h word=0x%0h", name, lb, word);  // ERROR !
  
    $display("pkg=%s lb=0x%0h", name, lb);
  endfunction
endpackage

module tb;
  // import only Z package
  import Z::*;
  
  initial begin
    X::display();   // Direct reference X package
    Y::display();   // Direct reference Y package
    display();      // Taken from Z package because of import
  end
endmodule

Output

Output
	Top level design units:
		X
		Y
		Z
		tb
Loading snapshot worklib.tb:sv .................... Done
...
xcelium> run
pkg=X lb=0x10 word=0xcafeface
pkg=Y lb=0x20 word=0xcafeface
pkg=Z lb=0x30
xmsim: *W,RNQUIE: Simulation is complete.

Y::display() successfully prints word (0xcafeface) even though Y never declares its own word — it resolves through Y's own wildcard import of X. But Z::display() can't reach word at all, even though Z imports Y, which itself imports X. A wildcard import isn't transitive: importing Y::* gives Z access to what Y itself declares, not to whatever Y only sees because of its own separate import of X.

Quick Check

Q: Could Z::display() reach word if Z added its own import X::*; statement directly?

Show Answer

Yes. Since imports aren't transitive, Z needs to import X itself to see X's declarations directly — importing Y alone never gives Z anything beyond what Y declares on its own.

Where Are Packages Used?

Packages are the standard way a verification environment shares enums, structs, parameters, and utility functions across every component and test in a project — exactly the kind of global reuse Verilog couldn't offer. UVM itself is built and delivered as a package for this reason.

Design Notes: Packages and Synthesis

The synthesizable constructs a package can include are: parameters, local parameters, and constant variables; user-defined types via typedef; fully automatic task and function definitions; and import statements for other packages.


// Define the package for ALU instructions
package alu_pkg;

typedef enum logic [2:0] {
    ADD = 3'b000,
    SUB = 3'b001,
    AND = 3'b010,
    OR  = 3'b011,
    XOR = 3'b100
} alu_op_t;

endpackage

Using a wildcard import to bring everything in:


// Import everything inside the package containing ALU operations
import alu_pkg::*;

module alu (
    input logic [2:0]   opcode,  // ALU operation code
    input logic [31:0]  a,       // First operand
    input logic [31:0]  b,       // Second operand
    output logic [31:0] result   // Result of the operation
);

always_comb begin
    // Enumerations defined in 'alu_pkg' can be directly used here
    // because they have been imported
    case (opcode)
        ADD: result = a + b;   // Perform addition
        SUB: result = a - b;   // Perform subtraction
        AND: result = a & b;   // Perform bitwise AND
        OR:  result = a | b;   // Perform bitwise OR
        XOR: result = a ^ b;   // Perform bitwise XOR
        default: result = 32'b0; // Default case (optional)
    endcase
end
endmodule

Or referencing the package directly, with no import at all:


// Note that package import is not done here

module alu (
    input logic [2:0]   opcode,  // ALU operation code
    input logic [31:0]  a,       // First operand
    input logic [31:0]  b,       // Second operand
    output logic [31:0] result   // Result of the operation
);

always_comb begin
    // Enumerations defined in 'alu_pkg' is directly referenced using ::
    case (opcode)
        alu_pkg::ADD  : result = a + b;   // Perform addition
        alu_pkg::SUB  : result = a - b;   // Perform subtraction
        alu_pkg::AND  : result = a & b;   // Perform bitwise AND
        alu_pkg::OR   : result = a | b;   // Perform bitwise OR
        alu_pkg::XOR  : result = a ^ b;   // Perform bitwise XOR
        default       : result = 32'b0;   // Default case (optional)
    endcase
end

When a module calls a task or function defined in a package, synthesis replicates that functionality as if it were defined directly inside the module. For a package's tasks and functions to be synthesizable, they must be declared automatic and cannot include static variables.

Synthesis does not support variable declarations inside a package. In simulation, though, a variable declared in a package is shared across every module that imports it — one module can write to it, and another can read the updated value. This is a form of inter-module communication that bypasses module ports entirely, and it's simulation-only, not synthesizable.

Common Beginner Mistakes

Mistake #1: Assuming a wildcard import always wins over a local declaration

As the search-order example shows, it's exactly the opposite — a local declaration always shadows a same-named wildcard-imported identifier. Why? This means a package's value can be silently overridden by an unrelated local declaration sharing the same name, with no warning at compile time.

Mistake #2: Assuming importing a package gives transitive access to what it imports

As the nested package example shows, import Z::*; doesn't give access to whatever Z only sees through Z's own import of Y. Why? Each package's imports are private to that package's own namespace — to reach something two levels deep, you need to import that specific package directly, not just the one that happens to import it.

Mistake #3: Importing a type without importing its individual enum labels

import my_pkg::e_signal; makes the type visible, but not any of its enum values. Why? When importing specific items instead of using a wildcard, every individual label needs its own import statement — the type and its values are treated as entirely separate importable items.

Mistake #4: Assuming any package content synthesizes

Plain variable declarations inside a package work fine in simulation but aren't synthesizable at all. Why? Only parameters, typedefs, fully automatic tasks/functions with no static variables, and import statements are synthesizable — a package used for simulation-only utility state (like the nested X/Y/Z example) won't carry over to a synthesizable design.

Practice Exercise

Challenge: Extend the Chain to Four Packages

Given the X → Y → Z chain from this article, suppose a fourth package, W, does import Z::*; and tries to reference lb (declared in Z) and word (declared only in X). Which reference compiles, and which doesn't?

Hint

Apply the same non-transitivity rule demonstrated by Z trying (and failing) to reach X's word through Y.

Solution

lb compiles fine — it's declared directly inside Z, which W imports directly. word fails to compile, for the exact same reason Z couldn't reach it through Y: W only imports Z, and Z never itself imports X — there's no chain of visibility reaching all the way back to where word is actually declared.

Summary

In this tutorial, you learned:

  • Packages solve Verilog's local-scope, non-synthesizable-hierarchical-reference, and type-duplication limitations by providing a shared, reusable namespace
  • A wildcard import (::*) only pulls in items actually referenced; importing specific items requires naming each one, including every individual enum label
  • Local declarations always take priority over wildcard-imported ones, regardless of the declared type of the variable being assigned
  • Package imports are not transitive — importing a package that itself imports another doesn't give you access to that second package's declarations
  • Only parameters, typedefs, fully automatic tasks/functions, and import statements inside a package are synthesizable; plain variables are simulation-only
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5