SystemVerilog Series · SV-18

SystemVerilog Series — SV-18: Hierarchy — VLSI Trainers
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 — 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.name and .* 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)
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.

  • 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).
  • 'include directives 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.
$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

  • 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

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

  • $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 timeprecision declarations and `timescale precisions in the design.
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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top