UVM
Package Structure & Compilation
Standard VIP layout, multi-agent directory structure, eliminating circular dependencies.
UVM Fundamentals · Module 3
Why Compilation Order Matters
The SystemVerilog compiler processes files in the order you supply them. When it encounters a class name it has not yet seen, it throws an error and stops. This makes compilation order a hard constraint, not a preference.
In a single-file testbench, the constraint is obvious — put uvm_pkg first, then your code. In a production project with multiple agents, a shared environment, a register model, and dozens of test files, wrong compilation order produces failures that look like this:
** Error: apb_driver.sv(12): Identifier 'apb_seq_item' has not been declared.
** Error: my_env.sv(8): Identifier 'apb_agent_pkg' is not a package.
** Error: my_env.sv(31): 'apb_agent' has not been declared.
** Error: test_pkg.sv(4): Package 'my_env_pkg' is not defined.
Compilation aborted after 4 error(s).
// Root cause: files were compiled in alphabetical order.
// apb_driver.sv compiled before apb_seq_item.sv was available.
// my_env.sv compiled before apb_agent_pkg.sv was imported.Every one of these errors would be fixed by compiling files in the correct order. None of them indicate a bug in the code itself. Understanding package structure eliminates this entire class of errors permanently.
SystemVerilog Packages — A Focused Refresher
A SystemVerilog package is a named compilation unit. Everything declared inside it — classes, typedefs, functions, parameters — lives in that package's namespace. To use those names elsewhere, you import the package.
// ── apb_agent_pkg.sv ──────────────────────────────────────────
package apb_agent_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*; // everything in uvm_pkg is now visible inside this package
`include "apb_seq_item.sv" // classes are `include'd, not compiled separately
`include "apb_sequence.sv"
`include "apb_driver.sv"
`include "apb_monitor.sv"
`include "apb_agent.sv"
endpackage
// ── my_env_pkg.sv — importing the agent package ───────────────
package my_env_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
import apb_agent_pkg::*; // apb_seq_item, apb_driver etc. now visible here
`include "my_scoreboard.sv"
`include "my_env.sv"
endpackageWhat Belongs Inside a Package
Classes, typedef, parameters, functions, tasks, enums, and structs. Everything that defines types and behaviour.
What Does NOT Belong Inside
interface declarations, module instantiations, and clock/reset generators. These are module-scope constructs — they compile outside any package.
Wildcard vs. Explicit Import
import pkg::* imports all names (standard in UVM). import pkg::my_class imports one name. For UVM packages, wildcard import is the convention — there is no ambiguity risk.
The Standard VIP Package Layout
Every production VIP at Tier-1 companies follows a consistent internal layout. It has one package file that acts as a wrapper, individual class files that are ``include`d into it, and one interface file that sits outside the package entirely.
The Three Layers of a VIP Directory
apb_agent/
├── apb_if.sv ← Interface file — compiled as a standalone SV module, NOT inside any package
├── apb_agent_pkg.sv ← Package wrapper — the ONLY file the compiler is given directly
├── apb_seq_item.sv ← `include'd by apb_agent_pkg.sv (not compiled directly)
├── apb_sequence.sv ← `include'd by apb_agent_pkg.sv
├── apb_driver.sv ← `include'd by apb_agent_pkg.sv
├── apb_monitor.sv ← `include'd by apb_agent_pkg.sv
└── apb_agent.sv ← `include'd by apb_agent_pkg.sv| Layer | File | Compiled By | Key Rule |
|---|---|---|---|
| Interface | apb_if.sv | Direct — given to compiler as-is | Must be compiled before any package that uses virtual apb_if as a type |
| Package wrapper | apb_agent_pkg.sv | Direct — given to compiler | Imports uvm_pkg, then ``include`s all class files in dependency order |
| Class files | apb_driver.sv etc. | Indirect — via ``include` inside the package | Never compile these directly. They have no package keyword — they rely on the wrapper's context |
Inside the Package Wrapper
The package wrapper contains only two things: imports from dependencies, and include`s for every class file. The includeorder within the package must also respect dependencies —apb_seq_item.svmust come beforeapb_driver.sv` because the driver references the seq_item type.
// apb_agent_pkg.sv — the package wrapper
package apb_agent_pkg;
// ── Step 1: Pull in UVM ────────────────────────────────────
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── Step 2: Include class files in dependency order ────────
// seq_item first — driver and monitor both reference this type
`include "apb_seq_item.sv" // extends uvm_sequence_item
`include "apb_sequence.sv" // extends uvm_sequence #(apb_seq_item)
`include "apb_driver.sv" // extends uvm_driver #(apb_seq_item)
`include "apb_monitor.sv" // extends uvm_monitor
`include "apb_agent.sv" // extends uvm_agent — references all of the above
endpackageCompilation Order — The Five Rules
These five rules cover every project structure you will encounter. Follow them in order and compilation errors disappear.
Figure 1 — UVM project compilation order. Each stage can only reference types declared in stages to its left. Reversing any two stages causes "identifier not declared" errors.
- 01 Compile uvm_pkg first — always, without exception Every UVM class inherits from something in
uvm_pkg. Nothing in your project can compile untiluvm_pkgis loaded. Most simulators load it automatically, but make it explicit in your compile script. - 02 Compile interface files before any package that uses them as types Interfaces are not packages — they compile in module scope. If
apb_driver.svhas avirtual apb_ifmember, the compiler must have seenapb_ifbefore it compilesapb_agent_pkg. - 03 Compile agent packages in dependency order — leaf packages first If your AXI agent uses types from a common bus package, compile the common bus package before the AXI agent package. Map the dependency graph and always compile the deepest node first.
- 04 Compile the environment package after all agent packages it imports
my_env_pkgimportsapb_agent_pkgandaxi_agent_pkg. Both must be fully compiled beforemy_env_pkgis given to the compiler. - 05 Compile the top module last The top module (
tb_top.sv) imports test packages, instantiates interfaces, and connects to the DUT. It references every other construct in the project — so it compiles last.
Multi-Agent Project Directory Structure
A project with two protocol agents (APB and AHB), a shared environment, a register model, and a test suite follows this canonical layout. This structure is used — with minor naming variations — at every major semiconductor company.
tb/
├── agents/
│ ├── apb_agent/
│ │ ├── apb_if.sv ← compiled as interface, NOT inside package
│ │ ├── apb_agent_pkg.sv ← package wrapper, `includes all classes below
│ │ ├── apb_seq_item.sv
│ │ ├── apb_sequence.sv
│ │ ├── apb_driver.sv
│ │ ├── apb_monitor.sv
│ │ └── apb_agent.sv
│ └── ahb_agent/
│ ├── ahb_if.sv
│ ├── ahb_agent_pkg.sv
│ └── ...
│
├── env/
│ ├── my_env_pkg.sv ← imports uvm_pkg + apb_agent_pkg + ahb_agent_pkg
│ ├── my_scoreboard.sv ← `include'd by my_env_pkg.sv
│ ├── my_coverage.sv
│ └── my_env.sv
│
├── reg_model/
│ ├── reg_model_pkg.sv ← imports uvm_pkg only
│ └── dut_reg_block.sv
│
├── tests/
│ ├── test_pkg.sv ← imports my_env_pkg + reg_model_pkg + uvm_pkg
│ ├── base_test.sv
│ ├── smoke_test.sv
│ └── regression_test.sv
│
└── tb_top.sv ← top module: imports test_pkg, instantiates DUT + interfacesCircular Dependencies — The Deadlock and How to Break It
A circular dependency occurs when Package A imports Package B, and Package B also imports Package A. The compiler cannot resolve this — it needs A to compile B, and B to compile A. It is an unbreakable deadlock.
// PROBLEM — This causes a compilation deadlock:
package apb_agent_pkg;
import my_env_pkg::*; // ← references my_env_pkg
// ...
endpackage
package my_env_pkg;
import apb_agent_pkg::*; // ← references apb_agent_pkg
// ... // Deadlock: each needs the other compiled first
endpackageFix 1: Shared Types Package
Extract the shared type into a third package that both can import. This is the cleanest long-term solution and is used in most large projects.
// ── Step 1: Extract shared types into a leaf package ──────────
package apb_types_pkg;
import uvm_pkg::*;
`include "apb_seq_item.sv" // the shared type that both packages needed
endpackage
// ── Step 2: Both packages import the shared types package ──────
package apb_agent_pkg;
import uvm_pkg::*;
import apb_types_pkg::*; // apb_seq_item now available
// ... driver, monitor, agent
endpackage
package my_env_pkg;
import uvm_pkg::*;
import apb_types_pkg::*; // apb_seq_item now available here too
import apb_agent_pkg::*; // no circular dependency — apb_agent_pkg does NOT import my_env_pkg
// ... scoreboard, env
endpackage
// Compile order: apb_types_pkg → apb_agent_pkg → my_env_pkg ✓Fix 2: typedef class (Forward Declaration)
When you only need to declare a handle of a type (not access its members), a forward declaration removes the need for the full package import.
// If pkg_a only needs a handle of a type from pkg_b (not the full type):
package pkg_a;
import uvm_pkg::*;
// Forward-declare the class — no import of pkg_b needed
typedef class my_scoreboard; // tells compiler "this type exists"
class my_agent extends uvm_agent;
my_scoreboard scb_h; // handle OK — we declared it above
// scb_h.check() would FAIL — we cannot access members of a forward-declared type
endclass
endpackage
// Use typedef class when:
// - You only store a handle (no method calls on it in this package)
// - The full type will be connected later (in connect_phase or from outside)Include Guards — Preventing Double-Inclusion
When two package wrappers both ``include` the same file, the file gets compiled twice. In the best case, you get a "redefiniton" error. In the worst case — when the file has no declarations, just macros — the double-inclusion silently changes behaviour. Include guards prevent this.
// apb_seq_item.sv — every class file should start with a guard
`ifndef APB_SEQ_ITEM_SV // if this macro is NOT defined yet...
`define APB_SEQ_ITEM_SV // ...define it and process the file content
class apb_seq_item extends uvm_sequence_item;
`uvm_object_utils(apb_seq_item)
rand bit [31:0] addr;
rand bit [7:0] data;
rand bit write;
function new(string name = "apb_seq_item");
super.new(name);
endfunction
endclass
`endif // APB_SEQ_ITEM_SV
// If `include "apb_seq_item.sv" appears a second time anywhere,
// APB_SEQ_ITEM_SV is already defined, so the `ifndef block is skipped entirely.
// The class is NOT redeclared. No error.| Guard Macro Naming Convention | Example |
|---|---|
| Class files | APB_SEQ_ITEM_SV (filename in uppercase, dots → underscores) |
| Package files | APB_AGENT_PKG_SV |
| Interface files | APB_IF_SV |
| Macro header files | UVM_MACROS_SVH (note: already guarded inside uvm_macros.svh) |
Quick Reference — A Real Compilation Script
This is the compile command sequence for a two-agent project on a typical simulator. Adapt the flags for your tool (vlog for Questa, vcs for VCS, xvlog for Xcelium).
# compile.sh — run in order, top to bottom
# ── STEP 1: UVM library (simulator usually handles this automatically) ──
# vlog -sv -work work $UVM_HOME/src/uvm_pkg.sv
# ── STEP 2: SystemVerilog interfaces ─────────────────────────────────────
vlog -sv tb/agents/apb_agent/apb_if.sv
vlog -sv tb/agents/ahb_agent/ahb_if.sv
vlog -sv tb/dut/dut_if.sv # DUT interface if separate
# ── STEP 3: Agent packages ───────────────────────────────────────────────
# Supply the package wrapper — NOT the individual class files
vlog -sv tb/agents/apb_agent/apb_agent_pkg.sv
vlog -sv tb/agents/ahb_agent/ahb_agent_pkg.sv
# ── STEP 4: Register model package ──────────────────────────────────────
vlog -sv tb/reg_model/reg_model_pkg.sv
# ── STEP 5: Environment package ─────────────────────────────────────────
# Imports uvm_pkg + apb_agent_pkg + ahb_agent_pkg + reg_model_pkg
vlog -sv tb/env/my_env_pkg.sv
# ── STEP 6: Test package ─────────────────────────────────────────────────
# Imports uvm_pkg + my_env_pkg
vlog -sv tb/tests/test_pkg.sv
# ── STEP 7: DUT RTL ──────────────────────────────────────────────────────
vlog -sv rtl/my_dut.sv
# ── STEP 8: Top module (very last) ──────────────────────────────────────
# Imports test_pkg, instantiates DUT and interfaces, calls run_test()
vlog -sv tb/tb_top.sv
# ── Simulate ─────────────────────────────────────────────────────────────
vsim -sv_seed random -GEN_SEED=0 \
+UVM_TESTNAME=smoke_test \
+UVM_VERBOSITY=UVM_MEDIUM \
work.tb_top// ── apb_agent_pkg.sv ──────────────────────────────────────────
package apb_agent_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "apb_seq_item.sv" // seq_item before driver
`include "apb_sequence.sv"
`include "apb_driver.sv"
`include "apb_monitor.sv"
`include "apb_agent.sv" // agent last — references all above
endpackage
// ── my_env_pkg.sv ─────────────────────────────────────────────
package my_env_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
import apb_agent_pkg::*; // provides apb_seq_item, apb_agent etc.
`include "my_scoreboard.sv"
`include "my_env.sv" // env last — creates agents, scoreboard
endpackage
// ── test_pkg.sv ───────────────────────────────────────────────
package test_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
import my_env_pkg::*; // everything from env and agent packages cascades in
`include "base_test.sv"
`include "smoke_test.sv"
endpackage
// ── tb_top.sv — NOT a package ─────────────────────────────────
module tb_top;
`include "uvm_macros.svh"
import uvm_pkg::*;
import test_pkg::*;
apb_if apb_vif(); // interface instances
my_dut dut(.apb(apb_vif)); // DUT connection
initial begin
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.*", "vif", apb_vif);
run_test(); // test name from +UVM_TESTNAME plusarg
end
endmoduleCode Examples — Building a Real Package Structure
Abstract rules about compilation order become real the moment you try to compile a 12-file VIP with circular dependencies and watch the simulator throw "identifier not found" on the third file. These examples show the exact patterns production teams use — and the ones that silently break.
Example 1 — Beginner: Minimal Compilable VIP Package
// File 1: apb_txn.sv — Transaction (no UVM dependency other than uvm_pkg)
// Compile first — everything else depends on it
class apb_txn extends uvm_sequence_item;
`uvm_object_utils(apb_txn)
rand bit[31:0] addr;
rand bit[31:0] data;
rand bit write;
function new(string name="apb_txn"); super.new(name); endfunction
endclass
// File 2: apb_driver.sv — Driver depends on apb_txn
// Must compile after apb_txn.sv
class apb_driver extends uvm_driver#(apb_txn);
`uvm_component_utils(apb_driver)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
apb_txn txn;
forever begin
seq_item_port.get_next_item(txn);
`uvm_info("DRV",$sformatf("Driving %s",txn.convert2string()),UVM_MEDIUM)
seq_item_port.item_done();
end
endtask
endclass
// File 3: apb_pkg.sv — Package wrapper (compile LAST, after all files above)
package apb_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "apb_txn.sv" // ← transaction first
`include "apb_driver.sv" // ← driver second (depends on txn)
endpackage
// Compile command (Questa):
// vlog -sv -L uvm_1_2 apb_pkg.sv
// ↑ One command. The package handles the include order internally.Example 2 — Intermediate: Multi-Package Environment with Dependencies
// ── Layer 1: VIP package (no env dependencies) ────────────────────
package apb_vip_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "apb_txn.sv"
`include "apb_seq_lib.sv"
`include "apb_driver.sv"
`include "apb_monitor.sv"
`include "apb_agent.sv"
endpackage
// ── Layer 2: Scoreboard package (depends on VIP for transaction type) ─
package apb_sb_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
import apb_vip_pkg::*; // ← depends on VIP for apb_txn
`include "apb_scoreboard.sv"
endpackage
// ── Layer 3: Environment package (depends on both above) ──────────────
package apb_env_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
import apb_vip_pkg::*;
import apb_sb_pkg::*;
`include "apb_env.sv"
`include "apb_base_test.sv"
endpackage
// ── Makefile compile order (each layer compiled independently) ─────────
// make:
// vlog -sv -L uvm_1_2 apb_vip_pkg.sv # Layer 1
// vlog -sv -L uvm_1_2 apb_sb_pkg.sv # Layer 2 (imports VIP)
// vlog -sv -L uvm_1_2 apb_env_pkg.sv # Layer 3 (imports both)
// vlog -sv -L uvm_1_2 tb_top.sv # Top module
// ↑ Reverse this order and you get "identifier not found" errorsExample 3 — Verification: Compile Script with Dependency Checking
# Makefile — production compile script for SoC APB testbench
# Demonstrates explicit dependency tracking
# Tool settings
VLOG = vlog -sv -timescale 1ns/1ps
UVM_LIB = -L uvm_1_2
VLOG_OPTS = $(UVM_LIB) +define+UVM_NO_DEPRECATED
# Step 1: UVM (provided by simulator — always first)
# Step 2: APB VIP package (no env dependencies)
apb_vip.done: src/vip/apb_pkg.sv
$(VLOG) $(VLOG_OPTS) $< && touch $@
# Step 3: Scoreboard (needs VIP for transaction type)
apb_sb.done: src/env/apb_sb_pkg.sv apb_vip.done
$(VLOG) $(VLOG_OPTS) $< && touch $@
# Step 4: Environment (needs VIP + scoreboard)
apb_env.done: src/env/apb_env_pkg.sv apb_vip.done apb_sb.done
$(VLOG) $(VLOG_OPTS) $< && touch $@
# Step 5: Tests (need everything above)
tests.done: src/tests/test_pkg.sv apb_env.done
$(VLOG) $(VLOG_OPTS) $< && touch $@
# Step 6: Top module (instantiates DUT + interface)
tb.done: tb/tb_top.sv tests.done
$(VLOG) $(VLOG_OPTS) $< && touch $@
compile: tb.done
@echo "Compilation complete"
clean:
rm -f *.done work/_* && vdel -allExample 4 — Tricky Corner Case: Circular Dependency Broken With typedef
// ── Problem: Driver needs Sequencer, Sequencer needs Driver type ───
// This creates a circular dependency that breaks compilation
// ── Solution 1: Forward declaration with typedef class ─────────────
package apb_pkg;
import uvm_pkg::*;
`include "uvm_macros.svh"
// Forward declare — tells compiler "this class exists" without full definition
typedef class apb_driver;
class apb_sequencer extends uvm_sequencer#(apb_txn);
`uvm_component_utils(apb_sequencer)
apb_driver driver_h; // ← OK: forward decl allows handle declaration
function new(string n, uvm_component p); super.new(n,p); endfunction
endclass
class apb_driver extends uvm_driver#(apb_txn);
`uvm_component_utils(apb_driver)
apb_sequencer seqr_h; // ← full definition now available
function new(string n, uvm_component p); super.new(n,p); endfunction
endclass
endpackage
// ── Solution 2 (preferred): Restructure to eliminate the coupling ──
// Sequencer should not hold a handle to the driver.
// Pass shared data through config_db or a shared config object.
// Circular deps in design = architectural problem, not a compile problem.Common Bugs and Debugging — Package Compilation Failures That Waste Days
Package and compilation order bugs are uniquely frustrating because the error messages are often misleading — the compiler points at the wrong line, or gives a generic "identifier not found" with no hint about which include was missing. These are the exact patterns to recognize.
⚠️ Bug 1 — Including a File Directly Instead of Through Its Package
Symptom: The class compiles in isolation but type_id::create() silently returns null at runtime. Factory overrides don't work. get_type_name() returns "uvm_component" instead of your class name.
Root cause: When you include` a class file directly in tb_top.sv without going through the package, the class is compiled in a different scope. The factory registration (from uvm_component_utils`) runs in the wrong scope and the lookup fails at runtime.
// ❌ WRONG — including class files directly in top module
module tb_top;
import uvm_pkg::*;
`include "uvm_macros.svh"
`include "apb_txn.sv" // ← compiled in module scope
`include "apb_driver.sv" // ← also module scope
// Factory registration happens in module scope, not package scope
// Result: type_id::create() silently returns null
initial run_test("apb_test");
endmodule
// ✓ CORRECT — compile through the package, import in top module
// apb_pkg.sv compiled first (via Makefile/filelist)
module tb_top;
import uvm_pkg::*;
import apb_pkg::*; // ← bring package into scope
`include "uvm_macros.svh"
initial run_test("apb_test");
endmodule
// Debug check: if get_type_name() returns "uvm_component" or "uvm_sequence_item"
// instead of your class name → you have a scope/include order problem⚠️ Bug 2 — Missing Include Guard Causes Double-Definition Errors
Symptom: Compiler error: "class apb_txn already declared in this scope" or "redefinition of apb_txn". Happens when two packages both include the same file, or a file is included both directly and through a package.
Root cause: No ifndef` guard around the class definition. Every include` re-inserts the full text, causing duplicate declarations.
// ❌ WRONG — apb_txn.sv without include guard
class apb_txn extends uvm_sequence_item;
// ... class body
endclass
// If two packages both `include "apb_txn.sv" → compiler error
// ✓ CORRECT — every file must have an include guard
`ifndef APB_TXN_SV
`define APB_TXN_SV
class apb_txn extends uvm_sequence_item;
`uvm_object_utils(apb_txn)
rand bit[31:0] addr;
rand bit[31:0] data;
rand bit write;
function new(string name="apb_txn"); super.new(name); endfunction
endclass
`endif // APB_TXN_SV
// Now safe to include from multiple locations — second include is a no-op🔍 Debug Insight — Diagnosing Compilation Order Failures
- Error "identifier 'apb_txn' not found" at line 3 of apb_driver.sv → apb_txn compiled after apb_driver. Fix: move apb_txn.sv earlier in filelist or `include earlier in the package.
- Error "class apb_agent already defined" → missing include guard on apb_agent.sv. Two packages or two filelists both including it.
- Runtime:
type_id::create()returns null → class included in wrong scope (module vs package). Move to package. get_type_name()returns base class name →uvm_component_utils oruvm_object_utils macro missing from the class.
Ready-to-Run: Complete Package Structure Demo
This is a complete, multi-file project demonstrating correct package structure. Every file has an include guard. The package respects compilation order. The top module imports correctly. You can compile and simulate this as-is.
// ================================================================
// pkg_demo.sv — Package Structure Demonstration
// Single file for easy testing; real projects split into multiple files
//
// Compile (Questa):
// vlog -sv -L uvm_1_2 pkg_demo.sv
// vsim -c -L uvm_1_2 pkg_demo_top -do "run -all; quit"
//
// Compile (VCS):
// vcs -sverilog -ntb_opts uvm-1.2 pkg_demo.sv && ./simv
//
// Compile (Xcelium):
// xrun -sv -uvm pkg_demo.sv
// ================================================================
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── Simulating apb_txn.sv content (with include guard) ────────────
`ifndef APB_TXN_SV
`define APB_TXN_SV
class apb_txn extends uvm_sequence_item;
`uvm_object_utils_begin(apb_txn)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(write, UVM_ALL_ON)
`uvm_object_utils_end
rand bit[31:0] addr;
rand bit[31:0] data;
rand bit write;
constraint c_aligned { addr[1:0] == 2'b00; }
function new(string name="apb_txn"); super.new(name); endfunction
function string convert2string();
return $sformatf("%s addr=%0h data=%0h", write?"WR":"RD", addr, data);
endfunction
endclass
`endif
// ── Simulating apb_seq.sv content ─────────────────────────────────
`ifndef APB_SEQ_SV
`define APB_SEQ_SV
class apb_write_seq extends uvm_sequence#(apb_txn);
`uvm_object_utils(apb_write_seq)
int num_txns = 3;
function new(string n="apb_write_seq"); super.new(n); endfunction
task body();
apb_txn txn;
repeat(num_txns) begin
txn = apb_txn::type_id::create("txn");
start_item(txn);
void'(txn.randomize() with {write==1;});
finish_item(txn);
end
endtask
endclass
`endif
// ── Simulating apb_driver.sv content ──────────────────────────────
`ifndef APB_DRV_SV
`define APB_DRV_SV
class apb_driver extends uvm_driver#(apb_txn);
`uvm_component_utils(apb_driver)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
apb_txn txn;
forever begin
seq_item_port.get_next_item(txn);
`uvm_info("DRV",txn.convert2string(),UVM_LOW)
#5ns;
seq_item_port.item_done();
end
endtask
endclass
`endif
// ── Test: uses all classes above ───────────────────────────────────
class pkg_demo_test extends uvm_test;
`uvm_component_utils(pkg_demo_test)
apb_driver drv;
uvm_sequencer#(apb_txn) seqr;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = apb_driver::type_id::create("drv", this);
seqr = uvm_sequencer#(apb_txn)::type_id::create("seqr", this);
`uvm_info("TEST", $sformatf("driver type: %s", drv.get_type_name()), UVM_NONE)
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(seqr.seq_item_export);
endfunction
task run_phase(uvm_phase phase);
apb_write_seq seq;
phase.raise_objection(this);
seq = apb_write_seq::type_id::create("seq");
seq.num_txns = 3;
seq.start(seqr);
phase.drop_objection(this);
endtask
endclass
module pkg_demo_top;
initial run_test("pkg_demo_test");
endmodule
// Expected Output:
// UVM_INFO: driver type: apb_driver ← factory resolved correctly
// UVM_INFO [DRV] WR addr=8c data=f3a2b1c0
// UVM_INFO [DRV] WR addr=14 data=726a1eb3
// UVM_INFO [DRV] WR addr=20 data=9d4f2380
// UVM_INFO: UVM_ERROR: 0 UVM_FATAL: 0 → PASSThe Dependency Graph Is the Compile Order
Compile order is not a convention to memorise. It is a topological sort of the dependency graph, and once you can draw the graph the order is the only thing it can be.
Two properties of the graph do all the work.
It must be acyclic. A cycle is not a difficult ordering — it is the absence of any ordering, which is why the circular-dependency section above is about restructuring rather than about finding the right sequence.
Arrows point from provider to consumer, and compilation follows the arrows. Interfaces sit at the source because packages name them as virtual apb_if types, and a virtual interface type must exist before a package that declares one can elaborate. The top module sits at the sink because it depends on everything.
Executable Failure — Four Compile Errors and What the Compiler Actually Saw
Compile errors in UVM package structures are famously unhelpful, because the message names the symptom line and the cause is usually in a different file. Each case below is reproducible, and the point is the gap between the two.
// apb_agent_pkg.sv
package apb_agent_pkg;
import uvm_pkg::*; // ← uvm_pkg imported, macros NOT included
`include "apb_seq_item.sv"
endpackage
// apb_seq_item.sv
class apb_seq_item extends uvm_sequence_item;
`uvm_object_utils(apb_seq_item) // ← error reported HERE
...
endclass
Error: Unknown macro or compiler directive: 'uvm_object_utils'
The error names the class file; the cause is the missing `include "uvm_macros.svh" in the package file. import and `include are not alternatives — import uvm_pkg::* makes type names visible, while macros are text substitutions resolved by the preprocessor and require the header. Every UVM package needs both, and the include must come first.
// my_env_pkg.sv
package my_env_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "my_scoreboard.sv" // ← uses apb_seq_item
import apb_agent_pkg::*; // ← imported AFTER the include
endpackage
Error: 'apb_seq_item' is not declared
Imports and includes are processed in source order. The scoreboard's text is pulled in and compiled before the import that makes apb_seq_item visible. All imports go at the top of the package, before any `include — the ordering inside the wrapper is as real as the ordering of files on the compile line.
// Both packages `include the same file:
package apb_agent_pkg;
`include "common_types.svh" // defines apb_mode_e
endpackage
package apb_test_pkg;
`include "common_types.svh" // the SAME text, compiled again
import apb_agent_pkg::*; // now two distinct apb_mode_e types exist
endpackage
Error: Type 'apb_mode_e' is ambiguous— or a type mismatch on an apparently identical assignment
This one is worse than a plain error, because it sometimes compiles. `include copies text; a type defined inside two packages is two different types with the same name. An include guard does not help — guards prevent double inclusion within one compilation unit, not across separate packages. Shared types belong in one package that both others import.
package apb_agent_pkg;
import my_env_pkg::*; // agent needs the env's config type
endpackage
package my_env_pkg;
import apb_agent_pkg::*; // env needs the agent's transaction
endpackage
Error: Package 'my_env_pkg' not found— whichever one you compile first
No compile order exists. The fix is structural, not sequential: hoist the shared declarations into a third package both import, or break the direction so the agent never depends on the environment. An agent that needs to know about its environment is usually a design problem before it is a compile problem — it means the agent is not independently reusable, which was the reason for packaging it separately.
A VIP compiled standalone and failed in the integration build with an error in a file nobody had touched
PKG-INCLUDE-ORDERAn APB VIP compiled cleanly in its own regression for months. When a second team pulled it into an SoC testbench, compilation failed with an unknown-macro error inside apb_driver.sv — a file neither team had modified.
The VIP owners could not reproduce it. The integrators could not compile without it.
// apb_agent_pkg.sv — as shipped
package apb_agent_pkg;
import uvm_pkg::*;
`include "apb_seq_item.sv"
`include "apb_driver.sv"
endpackageThe package never included uvm_macros.svh.
The two builds differed in one line of their compile scripts. The VIP's own regression compiled a local test file before the package:
vlog -sv tb/apb_smoke_test.sv # ← this file had `include "uvm_macros.svh"
vlog -sv tb/apb_agent_pkg.svThe SoC build compiled the package first. Adding -v to the failing compile and comparing preprocessor output showed uvm_object_utils defined in one run and undefined in the other, with identical package source.
`include and `define operate on the compilation unit, and many simulators treat a set of files given on one command line as a single unit. The smoke test's include of uvm_macros.svh therefore leaked its definitions into the package that followed it.
The VIP had a latent missing include that its own build order accidentally satisfied. Nothing about the VIP was correct — it was compiling by coincidence, and the coincidence did not survive being integrated into a build with a different file order.
This is the general hazard with compilation-unit scope: a file's correctness can depend on what was compiled beside it, so a package can be simultaneously broken and passing.
Make every package self-sufficient. The first two lines of any UVM package wrapper, in this order:
package apb_agent_pkg;
`include "uvm_macros.svh" // preprocessor: macro definitions
import uvm_pkg::*; // compiler: type names
`include "apb_seq_item.sv"
`include "apb_driver.sv"
endpackageThe guard against recurrence is a single-package compile check in CI: compile each package on its own command line, in isolation, with nothing else in the compilation unit. Any package that only builds in company has an accidental dependency, and this is the one test that finds it while it is still cheap.
It is worth being explicit about why both lines are needed and neither substitutes for the other. `include is preprocessor text substitution and supplies the macros; import is a compiler-visible namespace operation and supplies the type names. A package that imports without including gets types and no macros — exactly this failure.
Two packages agreed on a type name and the compiler refused to accept the assignment
PKG-DUPLICATE-TYPEA scoreboard assigned a mode enum received from the agent into its own variable of what appeared to be the same type. The compiler rejected it:
Error: Cannot assign 'apb_agent_pkg::apb_mode_e' to 'env_pkg::apb_mode_e'
Both names resolved to the same source file, and grep found exactly one definition of apb_mode_e in the repository.
// common_types.svh — ONE file, with a guard
`ifndef COMMON_TYPES_SVH
`define COMMON_TYPES_SVH
typedef enum bit [1:0] { APB_IDLE, APB_SETUP, APB_ACCESS } apb_mode_e;
`endif
// apb_agent_pkg.sv
package apb_agent_pkg;
`include "common_types.svh"
endpackage
// env_pkg.sv
package env_pkg;
`include "common_types.svh" // same text, second package
import apb_agent_pkg::*;
endpackageThe error message itself was the evidence, once it was read carefully: the two type names were package-qualified differently. apb_agent_pkg::apb_mode_e and env_pkg::apb_mode_e are distinct fully-qualified names, so the compiler was reporting exactly what was true.
The include guard was present and working. It had simply never been relevant.
`include copies text. Including the same file into two packages creates two independent declarations, each scoped to its own package, and SystemVerilog treats distinct type declarations as distinct types even when they are textually identical.
Include guards prevent a file being pulled twice into the same compilation unit. They have no visibility across separate packages, so the guard was defending against a different problem than the one occurring.
The team's mental model — "the guard means it is only defined once" — was the actual defect. The guard means it is only defined once per unit.
Give shared declarations one owning package and import it everywhere:
// apb_types_pkg.sv — the single owner
package apb_types_pkg;
typedef enum bit [1:0] { APB_IDLE, APB_SETUP, APB_ACCESS } apb_mode_e;
endpackage
// Both consumers import rather than include
package apb_agent_pkg;
import apb_types_pkg::*;
endpackage
package env_pkg;
import apb_types_pkg::*;
import apb_agent_pkg::*;
endpackageNow one declaration exists and both packages refer to it, so the assignment is between a type and itself.
The rule this generalises to is worth stating as policy: `include is for class implementations that belong to exactly one package; import is for anything shared between packages. A .svh of shared typedefs, parameters or enums that is included in more than one place is a latent instance of this bug, whether or not it has surfaced yet — which makes "grep for .svh files included by two packages" a productive one-off audit on any established codebase.
Questions Engineers Actually Ask
Because classes must live inside a package, and a file compiled on its own command line is not inside anything.
apb_driver.sv has no package keyword — it is a class body, meant to be textually inserted into a package wrapper. Compiling it directly puts the class in the compilation-unit scope instead, where the rest of the package cannot see it and where it collides with anything else of the same name.
So the compile script only ever names package wrappers and interfaces, never individual class files. If your build lists class files, either they are not really package members or the build is producing something other than what the packaging intended.
They operate at different stages and neither substitutes for the other.
`include is a preprocessor directive: it copies file text in before the compiler sees it. That is how macro definitions arrive, which is why uvm_macros.svh must be included and cannot be imported.
import is a compiler operation on namespaces: it makes names that already exist in a compiled package visible in the current scope. It does not recompile anything and it does not bring in macros.
Every UVM package needs both, in this order: `include "uvm_macros.svh" for the macros, then import uvm_pkg::* for the types. Case 1 in the section above is what a missing include looks like from the error log.
Where This Is Specified
- IEEE 1800-2023 §26 — Packages. Package declaration,
importsemantics, wildcard versus explicit import, and the scoping of names declared inside a package. - IEEE 1800-2023 §26.3 — Referencing data in packages. Package-qualified names, and why identical declarations in two packages are distinct types.
- IEEE 1800-2023 §22.4 —
`include. Preprocessor text substitution, and its distinction from namespace import. - IEEE 1800-2023 §3.12 — Compilation and elaboration; compilation units. The
$unitscope, and the rule that makes`defineand`includeeffects depend on which files share a compilation unit. - IEEE 1800-2023 §22.5 —
`define. Macro definition scope, which is what allows a macro to leak from one file into another compiled beside it. - IEEE 1800.2-2020 (UVM) —
uvm_pkganduvm_macros.svh. The library package that supplies UVM types, and the separate header that supplies the macros.
Related Reading
- UVM Class Hierarchy — the inheritance structure your package's
`includeorder has to respect: a base class must be included before anything that extends it. - Environment Architecture — how the environment package sits above the agent packages in the dependency graph.
- Agent Architecture — the unit that a VIP package normally wraps.
- UVM Testbench Architecture — the whole structure the compile order assembles.
- Environment Reuse and Reuse Methodology — why an agent that imports its environment is a reuse failure, not just a compile failure.
- The UVM Factory — why every class needs its registration macro, and therefore why every package needs
uvm_macros.svh.
Interview Questions — From Fresher to Principal
Beginner Level
- Q1 What is the purpose of a UVM package and why is it important for compilation? Answer: A UVM package groups related class definitions into a named compilation unit. It's important because: (1) it creates a namespace that prevents class name collisions across multiple VIPs, (2) it allows the simulator to compile all related classes in one step with the correct internal ordering, and (3) it enables other packages to import the classes with a single
import pkg_name::*statement rather than including individual files. Without packages, large projects quickly suffer from circular include dependencies and class redefinition errors. - Q2 What is an include guard and when do you need one? Answer: An include guard is a preprocessor pattern using ``ifndef /
define /endif` that prevents a file from being compiled more than once. You need it on every single class file in your testbench — without exception. When two packages include the same file, or when a file is included both through a package and directly in a top module, the compiler sees the class definition twice and throws a "class already declared" error. Include guards are the standard fix.
Intermediate Level
- Q3 Explain the compilation order for a VIP with transaction, sequence, driver, monitor, and agent classes. Answer: The rule is: compile dependencies before dependents. Correct order: (1) Transaction — no class dependencies beyond uvm_sequence_item; (2) Sequence — depends on transaction; (3) Driver — depends on transaction; (4) Monitor — depends on transaction; (5) Agent — depends on driver, monitor, and sequencer. Inside the package, `include files in this exact order. Reversing any step gives "identifier not found" errors pointing at the wrong line.
- Q4 **What is the difference between
include inside a package and import package::*?** **Answer:** ``includeinside a package physically inserts the file's contents at that location during preprocessing — the included class becomes part of the package's namespace.import package::*makes names from an already-compiled package visible in the current scope without including any source code.include` is for building the package; `import` is for using it. A common mistake is usingincludewhereimport` should be used, which leads to double-definition errors.
Senior / Principal Level
- Q5 Your scoreboard needs the DUT's register model (which is in a separate package), but the register model also uses your scoreboard's reference model class for expected value prediction. How do you resolve this circular dependency? Answer: This is a real architectural problem seen on complex SoC projects. Solutions in order of preference: (1) Refactor — pull the shared "reference model" class into a third package that both the scoreboard and register model packages import. (2) Interface/virtual class — define a virtual base class or interface for the prediction behavior in a common package; both sides depend on the interface, not each other's full implementation. (3) typedef forward declaration — allows declaring a handle to a class before its full definition, breaking the compile-time dependency while preserving the runtime link. Option 3 is a workaround; options 1 and 2 fix the underlying design problem.
Best Practices — What Gets Enforced in Real Code Review
Package structure is one of those areas where discipline early saves weeks later. Every practice below exists because a project violated it and paid the price in compilation failures, regression timeouts, or non-reusable VIPs.
| Rule | Correct Pattern | Violation Consequence |
|---|---|---|
| Include guard on every file | ``ifndef NAME_SV / define NAME_SV / ... / endif` | "class X already declared" errors when two compilation units include the same file |
| Never `include classes in module scope | Always compile through the package; import in modules | Factory registration in wrong scope → type_id::create() returns null silently |
| One class per file | apb_txn.sv, apb_driver.sv separate | One change recompiles the whole file; diff/blame becomes unreadable |
| Package file names match their content | apb_pkg.sv defines package apb_pkg | Confusion in large teams; wrong file gets included in wrong project |
| Import, not include, at top level | import apb_pkg::* in tb_top.sv | Classes included at module scope; factory and config_db break |
| Encode dependency order in Makefile/filelist | Explicit vip_pkg → sb_pkg → env_pkg ordering | Random compilation order works locally, fails on different machine or CI |
| No circular package imports | Extract shared types into a base package | Unresolvable compile dependency; teams get blocked for days |
💡 Pro Tip — The Filelist Approach for Large Projects
On projects with 50+ files, maintain a filelist (often called flist or vf) that declares every source file in the correct compilation order. Every simulator supports this. The filelist becomes the single source of truth for your project's compile order — and it can be version-controlled, reviewed, and updated independently of the source files themselves. If you change the dependency structure between packages, update the filelist. Compilation failures then become instantly obvious rather than mysterious.
Summary — Why Getting This Right Saves Projects
Package structure isn't glamorous. Nobody writes blog posts about it. But in every large SoC project, it's one of the first things that breaks when someone adds a new VIP without understanding the dependency chain, or when a fresher copies a class file into the top module and spends two days wondering why the factory returns null.
| Concept | Rule in One Line | What Breaks If Violated |
|---|---|---|
| Compilation Order | Compile dependencies before dependents — always | "identifier not found" at compile time |
| Include Guards | Every .sv file gets ifndef`…endif` | "class already declared" when included twice |
| Package Scope | Classes go in packages, never in modules directly | Factory, config_db, and overrides silently fail |
| Circular Dependencies | Extract shared types into a base package | Unresolvable compile loop; team blocked |
| Import vs Include | ``includebuilds packages;import` uses them | Double definitions and scope confusion |
- 1 The package is the compilation unit. One package per VIP layer (transaction, agent, environment, test). Every file in the package has an include guard. The package handles internal ordering. External users just import.
- 2 Dependency order is not optional. If package B imports package A, package A must be compiled first. This must be encoded explicitly in your Makefile or filelist — never rely on the simulator to figure it out.
- 3 Circular dependencies are architecture bugs. If you find yourself reaching for typedef forward declarations or workarounds to break a compile cycle, stop and fix the design. The two packages are too tightly coupled. Extract the shared type and the problem disappears.
📌 The Test of a Well-Structured Project
A well-structured UVM project should pass this test: can any package in the project be compiled in complete isolation (with only uvm_pkg as a dependency) and replaced with an updated version without touching any other package's source files? If yes — you have correct package separation. If changing one file requires touching five others to fix compilation — you have a dependency architecture problem.
Continue learning
Related tutorials
- Prerequisite
What UVM Is — and What It Is Not
Methodology, library, and framework — where the three layers differ.
- Related topic
The UVM Factory
How macros register classes, type_id::create() resolution, overrides, and the critical ordering rule.
- Related topic
uvm_config_db Architecture
Architecture, get/set protocol, scoping rules, virtual interface pattern, configuration objects.