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
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:
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
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
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
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, fullyautomatictasks/functions, and import statements inside a package are synthesizable; plain variables are simulation-only