SystemVerilog Series — SV-18: Hierarchy — VLSI Trainers
VLSI Trainers SV Series · 32 / 44
SystemVerilog Series · SV-18

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

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

What cannot go in a package: hierarchical references — items inside packages cannot reference items outside the package via hierarchical paths. Variable declaration assignments in a package execute before any 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

pkg::name (qualified)
Always works from any scope without any import statement. Unambiguous. Takes precedence over all local and imported names.
import pkg::name (explicit)
Brings specific name into scope. Overrides wildcard imports of the same name. Illegal if name already declared locally or explicitly imported from another package.
import pkg::* (wildcard)
Candidates imported on demand. Overridden by local declarations. Conflict between two wildcard imports of the same name causes an error on reference.

Name search order within a scope

  1. Local declarations in the current scope (including package import declarations).
  2. Compilation-unit scope (including package imports in that scope).
  3. Instance hierarchy (upward hierarchical search).
Key rule: A qualified reference (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.

📋 $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.
$unit vs a package: both hold shared declarations. The difference is that a package has a name, can be imported, and is visible across all compilation units. The compilation-unit scope is anonymous, cannot be imported, and is only visible within the same unit. Use packages for shared types you want everywhere; use compilation-unit scope for unit-local shared declarations.

📌 $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;
timeunit/timeprecision vs `timescale: The Verilog-2001 `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

  1. If nested module/interface — inherited from the enclosing scope.
  2. Else if a `timescale directive was previously active in the compilation unit — that value is used.
  3. Else if the compilation-unit scope specifies a timeunit — that value is used.
  4. 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

📄 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

📋 Quick Reference

Three ways to use a package item

MethodSyntaxScope neededNotes
Qualified referencePkg::itemAny — no import neededAlways unambiguous; ignores local names
Explicit importimport Pkg::item;Current scopeIllegal if item already locally declared or imported from another package
Wildcard importimport Pkg::*;Current scopeImports on demand; local declarations override; two wildcards with same name → error on use

Compilation-unit and root names

Nested module rules summary

timeunit / timeprecision

Coming next: SV-19 covers Interfaces — interface declarations, bundling signals, modports for direction control, parameterised interfaces, tasks and functions inside interfaces, and generic interface ports.
Properties & Concurrent Assertions☰ SV Series IndexPorts, Module Instances & Name Spaces
Scroll to Top