Hierarchy
Packages and the three ways to use them, compilation-unit scope and $unit, the $root top-level reference, module declaration enhancements including closing names and timeunit/timeprecision, nested modules for structural partitioning, and extern module declarations for separate compilation.
💡 What SV Adds to Hierarchy
Verilog-2001 kept all declarations inside modules, with only global system tasks/functions outside. SystemVerilog adds several constructs that break this limitation and improve large-design reuse and separate compilation.
- Packages — named shared scopes for types, tasks, functions, parameters, sequences, properties, and classes.
- Compilation-unit scope — an anonymous package local to a compilation unit; accessed via
$unit. - $root — unambiguous reference to the top of the instantiation hierarchy.
- Nested module declarations — declare a module inside another module’s body for local reuse.
- Extern modules — forward-declare a module’s port list to enable separate compilation.
- timeunit / timeprecision — portable replacement for
`timescale, bound to the module. - Simplified port connections —
.nameand.*shortcuts (covered in module instances).
📚 Packages
A package is a named scope at the outermost level of the source text — the same level as top-level modules. Items declared inside a package are shared among any number of compilation units, modules, interfaces, and programs that import or reference the package.
package ComplexPkg; typedef struct { real i, r; } Complex; function Complex add(Complex a, b); add.r = a.r + b.r; add.i = a.i + b.i; endfunction function Complex mul(Complex a, Complex b); mul.r = (a.r * b.r) - (a.i * b.i); mul.i = (a.r * b.i) + (a.i * b.r); endfunction endpackage : ComplexPkg
📋 What Goes in a Package
- Net declarations
- Data declarations (variables, typedefs, enums, structs, unions)
- Task and function declarations
- DPI import/export
- Class declarations (including constructors)
- Parameter and local-parameter declarations
- Covergroup declarations
- Sequence and property declarations (concurrent assertion items)
initial/always blocks are started, like module-level variables.
:: Qualified Access — the :: Operator
Any package item can be accessed from any scope using PackageName::item without an import statement. The package must have been compiled, but no explicit import is required.
// Direct qualified reference — works everywhere, no import needed ComplexPkg::Complex c1, c2, cout; cout = ComplexPkg::mul(c1, c2); // In port declarations, parameter values, etc — all valid with :: // p::c always refers to c in package p, regardless of local scope
📋 Explicit Import
Explicit import brings specific identifiers from a package into the current scope, removing the need to qualify them with the package name on every use.
import ComplexPkg::Complex; // import only the type import ComplexPkg::add; // import only the function // Now can use without qualification: Complex a, b, sum; sum = add(a, b); // no ComplexPkg:: prefix needed // Restrictions: // — Illegal if the identifier is already declared in the same scope // — Illegal if the same identifier is already explicitly imported from another package // — Importing the same identifier from the same package multiple times is allowed
📋 Wildcard Import — *
A wildcard import makes all package identifiers candidates for import. Each identifier is only actually imported when it is referenced and is not already defined in the scope.
import ComplexPkg::*; // ALL items in ComplexPkg are candidates // Identifiers are imported on demand — only when referenced // and not already declared/imported in scope Complex x; // triggered import of 'Complex' from ComplexPkg // A local declaration OVERRIDES an identifier from a wildcard import int add; // shadows ComplexPkg::add for direct references // Wildcard conflict: same name from two packages → undefined → error if referenced import p::*; import q::*; // Both p and q have 'c' → direct reference to c is an error // Use p::c or q::c to disambiguate // Triggering a wildcard import then making a conflicting explicit import → error module foo; import q::*; wire a = c; // forces import of q::c import p::c; // ERROR: q::c already imported into this scope endmodule
📋 Import Search Order Rules
Name search order within a scope
- Local declarations in the current scope (including package import declarations).
- Compilation-unit scope (including package imports in that scope).
- Instance hierarchy (upward hierarchical search).
p::c) is always visible in any scope regardless of local declarations. It bypasses all search order rules. Use it when name conflicts would otherwise make a direct reference ambiguous.
📄 Compilation Unit Support
A compilation unit is a set of one or more source files compiled together. Each compilation unit has its own compilation-unit scope — an anonymous namespace that contains declarations placed outside all module/package/interface declarations within that unit.
- The tool defines which files constitute a compilation unit — this is tool-specific, but tools must provide a mechanism to specify it.
- Two extreme mappings: all files in one compilation unit (declarations globally shared) or each file is its own compilation unit (declarations file-private).
'includedirectives expand into the compilation unit of the including file.- Compiler directives from one separately-compiled unit do not affect other units.
- The following are visible across all compilation units: modules, macromodules, primitives, programs, interfaces, and packages.
- Items in the compilation-unit scope are not accessible by name from outside the unit — only via the PLI iterator.
📋 $unit — The Compilation-Unit Scope Name
$unit is the explicit name of the compilation-unit scope. It lets you unambiguously refer to declarations at the outermost level of a compilation unit when local names would shadow them. It uses the same :: scope resolution operator as packages.
bit b; // declared in the compilation-unit scope task foo; int b; // local b — shadows the outer b inside foo b = 5; // references the local int b b = 5 + $unit::b; // $unit::b explicitly references the compilation-unit bit b endtask // $unit cannot be used with import — it has no package name. // It is analogous to an anonymous package — shared within the unit, // invisible outside it, not portable across units.
📌 $root — Top-Level Instance Reference
$root is the root of the instantiation hierarchy. It provides an unambiguous absolute path to top-level instances, bypassing the precedence that local paths normally have over hierarchical paths.
// Absolute paths starting from the design root $root.A.B // item B within top instance A $root.A.B.C // item C within instance B within top instance A // Without $root: A.B.C is ambiguous if the current module also has // a local instance A that contains a B — local path wins. // With $root.A.B.C: always refers to the top-level path.
📄 Module Declarations
SV adds two enhancements to module declarations: closing names and timeunit/timeprecision declarations as a portable replacement for `timescale.
Closing name
// Verilog-2001: no closing name module my_module(...); endmodule // SV: optional closing name — must match the opening name if given module my_module(...); endmodule : my_module
timeunit and timeprecision
// File-order-independent time specification — bound to this module module fast_design; timeunit 100ps; // time unit = 100 picoseconds timeprecision 10fs; // precision = 10 femtoseconds // ... design code endmodule // Combined on one line (legal) timeunit 1ns; timeprecision 1ps;
`timescale directive is global and order-dependent — the last `timescale seen by the compiler applies to all subsequent modules until changed. timeunit/timeprecision are declarations inside the module, binding the time specification to that module regardless of file order. This eliminates the “who included what last” class of compile-order bugs.
Time unit inheritance rules
- If nested module/interface — inherited from the enclosing scope.
- Else if a
`timescaledirective was previously active in the compilation unit — that value is used. - Else if the compilation-unit scope specifies a timeunit — that value is used.
- Else — the tool’s default time unit.
🔁 Nested Modules
A module can be declared inside another module’s body. The nested module’s name is in the outer module’s namespace, not the global namespace. This enables structural partitioning without ports, local library modules, and name hiding.
Before — flat DFF (6 NAND gates)
module dff_flat(input d, ck, pr, clr, output q, nq); wire q1, nq1, q2, nq2; nand g1b(nq1, d, clr, q1); nand g1a(q1, ck, nq2, nq1); nand g2b(nq2, ck, clr, q2); nand g2a(q2, nq1,pr, nq2); nand g3a(q, nq2,clr, nq); nand g3b(nq, q1, pr, q); endmodule
After — nested DFF (3 RS latches)
module dff_nested(input d, ck, pr, clr, output q, nq); wire q1, nq1, nq2; module ff1; // nested — shares outer scope nand g1b(nq1,d,clr,q1); nand g1a(q1,ck,nq2,nq1); endmodule ff1 i1(); module ff2; wire q2; // private to ff2 nand g2b(nq2,ck,clr,q2); nand g2a(q2,nq1,pr,nq2); endmodule ff2 i2(); module ff3; nand g3a(q,nq2,clr,nq); nand g3b(nq,q1,pr,q); endmodule ff3 i3(); endmodule
Local module library
module part1(...); // Local and2 — only visible inside part1 module and2(input a, b, output z); assign z = a & b; endmodule module or2(input a, b, output z); assign z = a | b; endmodule and2 u1(...), u2(...); // instantiates LOCAL and2 — not any global and2 endmodule
Nested module rules
- The outer module’s namespace is visible inside the nested module — any name declared in the outer scope is accessible unless shadowed locally.
- The nested module’s name is in the outer module’s namespace, not the global definitions namespace.
- Nested modules with no ports that are not explicitly instantiated are implicitly instantiated once with an instance name identical to the module name.
- Nested modules with ports that are not explicitly instantiated are simply ignored.
- This is also an alternative to configurations for managing module name conflicts — the same name can represent different modules in different scopes.
📄 Extern Modules
An extern module declaration announces a module’s port list (and optional parameters) without providing the module body. This supports separate compilation — the module definition can be in a different compilation unit, compiled separately.
// extern declarations: ports/params without the body extern module m (a, b, c, d); extern module a #(parameter size = 8, parameter type TP = logic[7:0]) (input [size:0] a, output TP b); // With extern declarations, .* can be used as the port list module top (); wire [8:0] a; logic [7:0] b; m m (.*); // ports inferred from extern: (a,b,c,d) a a (.*); // ports inferred from extern: (a,b) endmodule // The module definitions (can be in separate files): module m (.*); // .* gets ports from the extern declaration input a, b, c; output d; // ... body ... endmodule module a (.*); // .* expands to (input[size:0] a, output TP b) // ... body ... endmodule
Extern module rules
- An extern declaration can appear at any level of the instantiation hierarchy, but is visible only within the level where it is declared.
- The module definition must exactly match the extern declaration — port list, parameter list, types, and directions must agree.
- When an extern declaration exists,
.*on both the module definition and its instantiation sites uses the ports from the extern declaration. - Extern declarations enable type-checking at instantiation time even before the module body is compiled.
📋 Quick Reference
Three ways to use a package item
| Method | Syntax | Scope needed | Notes |
|---|---|---|---|
| Qualified reference | Pkg::item | Any — no import needed | Always unambiguous; ignores local names |
| Explicit import | import Pkg::item; | Current scope | Illegal if item already locally declared or imported from another package |
| Wildcard import | import Pkg::*; | Current scope | Imports on demand; local declarations override; two wildcards with same name → error on use |
Compilation-unit and root names
$unit::name— explicitly accesses the compilation-unit scope, bypassing local shadows.$root.path— absolute path from the top of the instantiation hierarchy, bypassing local path precedence.
Nested module rules summary
- Nested module name is in outer module’s namespace — not globally visible.
- Outer scope is visible inside the nested module.
- No-port, uninstantiated nested modules are implicitly instantiated once.
- Port-having, uninstantiated nested modules are ignored.
timeunit / timeprecision
- Bound to the module/interface/package — no file-order dependency.
- Must precede all other items in the current time scope.
- Nested module/interface inherits from enclosing scope if not specified.
- Global time precision = minimum of all
timeprecisiondeclarations and`timescaleprecisions in the design.
