Skip to content

UVM Class Library Hierarchy

uvm_void → uvm_object → uvm_component — what each level adds and when to extend which class.

UVM Fundamentals · Module 2

Why a Common Hierarchy Exists

Before UVM standardised the library, every team wrote their own base classes. One team's transaction had a print() method. Another team's had display(). A third team had neither. When these teams tried to share a VIP, every component was incompatible. Factory substitution was impossible. Debug messages looked different in every environment.

UVM solved this with a single, common class hierarchy. If a class extends uvm_object, you already know it has print(), copy(), compare(), and factory registration — guaranteed, regardless of who wrote it. If a class extends uvm_component, you know it participates in phasing, has a parent-child hierarchy, and responds to config_db.

The hierarchy is not bureaucracy. It is a contract. Every class in every compliant UVM environment honours the same contract, which is what makes IP-level reuse possible.

The Full Class Tree

Study this diagram before reading anything else on this page. Every class name you will encounter in UVM lives somewhere in this tree. uvm_voiduvm_objectData ObjectsSequencesStructural Componentsuvm_sequence_itemyour_seq_itemextends uvm_sequence_itemuvm_sequenceyour_sequenceextends uvm_sequenceuvm_componentuvm_testuvm_envuvm_agentuvm_scbrduvm_driveruvm_monitoruvm_sequencer Figure 1 — UVM class hierarchy. Dashed boxes are your custom extensions. Everything in green extends uvm_component; everything else extends uvm_object.

The dashed boxes in the diagram are your classes — the ones you write. Everything above the dashed line is provided by the UVM library. Your job is always to pick the right parent class and extend it.

uvm_void — The Universal Root

uvm_void is a pure virtual class — it defines no concrete functionality. Its only purpose is to be the single common ancestor of every class in the UVM library.

Why does this matter? Because SystemVerilog allows you to hold a handle of type uvm_void that can point to any UVM object or component. This is what makes the factory work — it can store all registered types in a generic container without knowing anything about what those types actually do.

SystemVerilog — uvm_void in the UVM source (simplified)
// From the UVM source: uvm_void is the common base — nothing more.
virtual class uvm_void;
// Intentionally empty.
// Its only job: be a type that everything else inherits from.
endclass
 
// This lets the factory hold any UVM class in one container:
uvm_void generic_handle;        // can point to anything in UVM
generic_handle = new();          // ERROR — uvm_void is virtual, cannot instantiate
generic_handle = my_driver_h;    // OK — my_driver extends uvm_component extends uvm_void

uvm_object — Data, Automation, and Factory

uvm_object is the base class for everything in UVM that is data — transactions, sequences, configuration objects, register models. It adds four critical capabilities that raw SystemVerilog classes do not have.

  • Field Automation — do_copy(), do_compare(), do_print(), do_pack(), do_unpack() — override these to teach the framework how to handle your object's fields. The field macros automate this (with trade-offs covered in Module 16).
  • Factory Registration — The `uvm_object_utils macro registers your class with the UVM factory. Without it, the factory cannot create or override your type. Always include it.
  • Named Identity — Every uvm_object has a string name passed to new(). get_name() returns the instance name. get_type_name() returns the class name. Both are used for debug messages and factory lookups.
  • Clone and Copy — clone() creates a deep copy of the object. copy() copies into an existing handle. Transaction pipelines rely on this — without it, sequences would overwrite in-flight transactions.

The uvm_object Constructor — One Argument

Every class that extends uvm_object must define a constructor that takes exactly one argument: a string name with a default value. This is a strict rule with no exceptions.

SystemVerilog — A complete uvm_object extension
class apb_seq_item extends uvm_sequence_item;   // uvm_sequence_item extends uvm_object
 
`uvm_object_utils(apb_seq_item)   // register with factory — never skip this
 
// Fields — these become the transaction's data payload
rand bit [31:0] addr;
rand bit [7:0]  data;
rand bit         write;   // 1 = write, 0 = read
bit      [1:0]  pslverr;  // response — filled by driver after DUT responds
 
// ── Constructor: ONE argument with a default value ──────────
function new(string name = "apb_seq_item");
super.new(name);   // pass name up — no parent argument
endfunction
 
// ── Field automation — teach UVM how to print/compare ───────
function void do_print(uvm_printer printer);
super.do_print(printer);
printer.print_field_int("addr",  addr,  32, UVM_HEX);
printer.print_field_int("data",  data,   8, UVM_HEX);
printer.print_field_int("write", write,  1, UVM_BIN);
endfunction
 
endclass
 
// ── Creating a uvm_object — always use the factory ────────────
apb_seq_item txn;
txn = apb_seq_item::type_id::create("txn");   // factory creation
// NOT: txn = new("txn");  — bypasses factory, kills override capability

uvm_component — Hierarchy, Phasing, and Configuration

uvm_component extends uvm_object and adds everything needed for structural elements — the pieces that stay alive for the entire simulation and form the testbench topology.

Three things make uvm_component fundamentally different from uvm_object:

  • Hierarchy and Full Path — Every component knows its parent. Every component has a unique dot-separated path name (e.g., uvm_test_top.env.agent.driver). This path is used by config_db, the factory, and the report server.
  • Phase Callbacks — Components receive phase callbacks automatically: build_phase(), connect_phase(), run_phase(), check_phase(), etc. The framework calls these in the correct order — you just override the ones you need.
  • Config DB and Reporting — Components get uvm_config_db scoped access and the full uvm_report_* API — uvm_info, uvm_error, `uvm_fatal — all automatically tagged with the component's full path in every message.

The uvm_component Constructor — Two Arguments, Always

This is the rule that trips every engineer at least once. A uvm_component constructor takes two arguments: the instance name and the parent component. Both are mandatory. There is no default for parent.

SystemVerilog — A complete uvm_component extension
class apb_driver extends uvm_driver #(apb_seq_item);  // uvm_driver extends uvm_component
 
`uvm_component_utils(apb_driver)   // NOT uvm_object_utils — this is structural
 
// ── Constructor: TWO arguments — name AND parent ────────────
function new(string name, uvm_component parent);
super.new(name, parent);   // parent establishes position in hierarchy tree
endfunction
 
// ── build_phase: allocate sub-components (called top-down) ──
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Retrieve virtual interface from config_db
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "Virtual interface not found in config_db")
endfunction
 
// ── run_phase: the main stimulus loop (time-consuming task) ─
task run_phase(uvm_phase phase);
apb_seq_item req;
forever begin
seq_item_port.get_next_item(req);
drive_bus(req);             // drive the DUT signals
seq_item_port.item_done();
end
endtask
 
virtual apb_if vif;
task drive_bus(apb_seq_item req); /* ... */ endtask
 
endclass
 
// ── Creating a uvm_component — factory with name AND parent ──
apb_driver drv;
drv = apb_driver::type_id::create("drv", this);   // 'this' = parent component

What uvm_component Cannot Do

Unlike uvm_object, components are permanent. You cannot clone a component, you cannot create one after build_phase, and you cannot pass one through a TLM port. If you find yourself trying to do any of these things, you have picked the wrong base class.

The Critical Split — Which One Do You Extend?

This decision determines whether your class works or silently corrupts your testbench. The table below gives you the complete decision framework.

QuestionExtend uvm_objectExtend uvm_component
Does it move between components?Yes — transactions, configuration objectsNo — components stay fixed in the hierarchy
Does it need phase callbacks?NoYes — build_phase, run_phase, etc.
Does it need a hierarchy parent?No — standalone, no parent argument in new()Yes — always has a parent component
Is it created dynamically at runtime?Yes — new transaction per stimulus itemNo — created once during build_phase
Does it get cloned?Yes — transactions are copied through pipelinesNo — components are never cloned
Constructor signaturenew(string name = "my_obj")new(string name, uvm_component parent)
Factory macro``uvm_object_utils(class_name)```uvm_component_utils(class_name)`
Common examplesseq_item, sequence, config object, reg modeldriver, monitor, agent, env, test, scoreboard
SystemVerilog — The two constructor patterns side by side
// ── uvm_object: ONE argument, default value required ──────────
class my_transaction extends uvm_sequence_item;
`uvm_object_utils(my_transaction)
 
function new(string name = "my_transaction");
super.new(name);
endfunction
endclass
 
// ── uvm_component: TWO arguments, no defaults ──────────────────
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver)
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
 
// ── Factory creation follows the same pattern ──────────────────
my_transaction txn = my_transaction::type_id::create("txn");        // one arg
my_driver      drv = my_driver::type_id::create("drv", this);   // two args

Key Derived Classes — What Each One Adds

The UVM library provides several concrete base classes between uvm_object / uvm_component and your custom classes. Each one adds domain-specific behaviour you would otherwise have to write yourself.

From uvm_object

ClassExtendsWhat It AddsYou Extend It For
uvm_sequence_itemuvm_objectTransaction ID, sequencer handle, response queue support. The building block that drivers and sequencers communicate with.Your protocol transaction class — APB, AXI, I2C, custom.
uvm_sequence #(REQ,RSP)uvm_sequence_itembody() task, start_item(), finish_item(), response queue. Sequences run on sequencers to generate stimulus.Your stimulus pattern — directed, constrained-random, or scenario-based.
uvm_reg_blockuvm_objectRegister map, address map, lock model. Core of the RAL layer.Your DUT's register model (usually auto-generated).

From uvm_component

ClassWhat It AddsYou Extend It For
uvm_driver #(REQ,RSP)TLM seq_item_port already declared and connected. Connects to the sequencer automatically via connect_phase().The component that pulls transactions and drives the DUT interface.
uvm_monitorAn empty shell — adds no ports. Exists as a type tag so the factory knows it is a passive, non-driving component.The component that observes DUT interface signals and broadcasts transactions via an analysis port.
uvm_sequencer #(REQ,RSP)Full arbitration logic — FIFO, priority, random. The seq_item_export that drivers connect to.Rarely extended directly. Use as-is for most protocols. Extend only for custom arbitration.
uvm_agentis_active config flag. Used to switch between active (driver + sequencer + monitor) and passive (monitor only) modes.The container that groups one set of driver, monitor, and sequencer for a single protocol interface.
uvm_scoreboardAn empty shell — same as uvm_monitor. Exists as a type tag for scoreboards.The component that receives transactions from the monitor and compares them against a reference model.
uvm_envAn empty shell. The conventional container for agents, scoreboard, coverage, and reference model.Your top-level environment class — the thing that uvm_test creates in build_phase().
uvm_testRegistered as the root component. run_test("my_test") instructs the factory to create this class at simulation start.Each test scenario — different tests create different sequences or configure the environment differently.

Quick Reference — Hierarchy at a Glance

Bookmark this. When you forget which macro or which constructor signature to use, come back here.

SystemVerilog — UVM hierarchy quick reference
// ═══════════════════════════════════════════════════════════════
//  uvm_object branch — data, transactions, sequences
// ═══════════════════════════════════════════════════════════════
 
// Transaction (extends uvm_sequence_item)
class my_txn extends uvm_sequence_item;
`uvm_object_utils(my_txn)
function new(string name = "my_txn"); super.new(name); endfunction
endclass
 
// Sequence (extends uvm_sequence)
class my_seq extends uvm_sequence #(my_txn);
`uvm_object_utils(my_seq)
function new(string name = "my_seq"); super.new(name); endfunction
task body(); /* generate items here */ endtask
endclass
 
// ═══════════════════════════════════════════════════════════════
//  uvm_component branch — drivers, monitors, agents, envs, tests
// ═══════════════════════════════════════════════════════════════
 
// Driver
class my_driver extends uvm_driver #(my_txn);
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent); super.new(name, parent); endfunction
task run_phase(uvm_phase phase); /* drive DUT */ endtask
endclass
 
// Monitor
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
function new(string name, uvm_component parent); super.new(name, parent); endfunction
task run_phase(uvm_phase phase); /* observe DUT */ endtask
endclass
 
// Environment
class my_env extends uvm_env;
`uvm_component_utils(my_env)
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// create sub-components here
endfunction
endclass
 
// Test
class my_test extends uvm_test;
`uvm_component_utils(my_test)
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
my_env env;
endclass

Code Examples — Seeing the Hierarchy in Action

Understanding the hierarchy as a diagram is one thing. Watching it execute in simulation is another. These four examples build from the simplest possible class extension up to the kinds of patterns you'll find in production VIPs.

Example 1 — Beginner: Extending uvm_object for a Transaction

SystemVerilog — Beginner: uvm_object Subclass
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── uvm_sequence_item → uvm_object → uvm_void
//    This is the correct base for ALL transactions/stimuli
class apb_txn extends uvm_sequence_item;
`uvm_object_utils(apb_txn)      // ← registers with factory
 
rand bit[31:0] addr;
rand bit[31:0] data;
rand bit        write;
 
function new(string name = "apb_txn");
super.new(name);   // MUST pass name up — no parent for objects
endfunction
 
function string convert2string();
return $sformatf("[%s] addr=%0h data=%0h",
write ? "WR" : "RD", addr, data);
endfunction
endclass
 
// ── Prove that clone() and copy() came free from uvm_object ──────
module test_obj;
initial begin
apb_txn t1, t2;
t1 = apb_txn::type_id::create("t1");
void'(t1.randomize());
$display("t1: %s", t1.convert2string());
 
$cast(t2, t1.clone());   // clone() is from uvm_object — free!
t2.addr = 32'hDEAD;
$display("t2 (modified clone): %s", t2.convert2string());
$display("t1 unchanged:        %s", t1.convert2string());
end
endmodule
// Output:
// t1: [WR] addr=a3f91bc2 data=7e045d31
// t2 (modified clone): [WR] addr=dead data=7e045d31
// t1 unchanged:        [WR] addr=a3f91bc2 data=7e045d31

Example 2 — Intermediate: Extending uvm_component for a Driver

SystemVerilog — Intermediate: uvm_component Subclass
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── uvm_driver → uvm_component → uvm_object → uvm_void
//    Two arguments in new(): name + parent (component tree requirement)
class apb_driver extends uvm_driver#(apb_txn);
`uvm_component_utils(apb_driver)   // ← component macro, NOT object
 
virtual apb_if vif;
 
function new(string name, uvm_component parent);
super.new(name, parent);   // TWO args — component has a parent
endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "Virtual interface not found in config_db")
endfunction
 
task run_phase(uvm_phase phase);
apb_txn req;
forever begin
seq_item_port.get_next_item(req);
`uvm_info("DRV", req.convert2string(), UVM_MEDIUM)
@(posedge vif.clk);
vif.paddr  <= req.addr;
vif.pwdata <= req.data;
vif.pwrite <= req.write;
vif.psel   <= 1;
vif.penable <= 1;
@(posedge vif.clk iff vif.pready);
vif.psel   <= 0;
vif.penable <= 0;
seq_item_port.item_done();
end
endtask
endclass
 
// Key points:
// 1. new() takes TWO args (name, parent) — component rule
// 2. build_phase() gets config via uvm_config_db — component feature
// 3. run_phase() drives signals — phase-based execution, component only
// 4. None of this is available if you extended uvm_object instead

Example 3 — Verification: Proving the Hierarchy with print() and get_type_name()

SystemVerilog — Verification: Hierarchy Introspection
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── Using uvm_object methods to introspect the hierarchy ──────────
class hierarchy_test extends uvm_test;
`uvm_component_utils(hierarchy_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
 
task run_phase(uvm_phase phase);
apb_txn txn;
phase.raise_objection(this);
 
txn = apb_txn::type_id::create("my_txn");
void'(txn.randomize());
 
// These methods exist because uvm_object provides them
$display("[OBJ] type_name : %s", txn.get_type_name());
$display("[OBJ] inst_name : %s", txn.get_name());
$display("[OBJ] full_name : %s", txn.get_full_name());
 
// print() uses field automation from `uvm_object_utils_begin
txn.print();
 
// compare() — also from uvm_object
begin
apb_txn t2;
$cast(t2, txn.clone());
$display("[OBJ] compare (same): %0b", txn.compare(t2));
t2.addr = 32'hFFFF;
$display("[OBJ] compare (diff): %0b", txn.compare(t2));
end
 
// Component-specific: full path includes hierarchy
$display("[COMP] test path: %s", get_full_name());
phase.drop_objection(this);
endtask
endclass
// Output includes:
// [OBJ] type_name : apb_txn
// [OBJ] inst_name : my_txn
// [OBJ] full_name : my_txn     ← objects don't have hierarchy path
// [COMP] test path: uvm_test_top ← components do!

Example 4 — Tricky Corner Case: The Wrong Constructor Signature

SystemVerilog — Tricky: Constructor Signature Matters
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── WRONG: Component constructor with only one argument ───────────
class bad_driver extends uvm_driver#(apb_txn);
`uvm_component_utils(bad_driver)
 
function new(string name = "bad_driver");  // ← missing 'parent' arg!
super.new(name, null);  // null parent = hierarchy is broken
endfunction
endclass
// This compiles but:
// 1. Component is orphaned — not in the hierarchy tree
// 2. uvm_config_db lookups by path WILL FAIL
// 3. print_topology() won't show this component
// 4. Factory override by instance name WILL NOT WORK
 
// ── CORRECT: Both arguments required for uvm_component ─────────
class good_driver extends uvm_driver#(apb_txn);
`uvm_component_utils(good_driver)
 
function new(string name, uvm_component parent);  // ← both required
super.new(name, parent);  // parent connects to hierarchy
endfunction
endclass
// ── OBJECT constructor: only name, with default ─────────────────
// Objects like transactions: default value IS correct
class good_txn extends uvm_sequence_item;
`uvm_object_utils(good_txn)
function new(string name = "good_txn");  // ← default OK for objects
super.new(name);
endfunction
endclass
// Rule: If it extends uvm_component (or its children) → two args, no default
//       If it extends uvm_object   (or its children) → one arg, with default

Common Bugs and Debugging — What Trips Up Every Engineer

The hierarchy rules look simple on paper. In practice, violating them produces confusing runtime failures — not compile errors — because SystemVerilog's type system can't catch logic errors. These are the bugs that show up in code review on every UVM project.

⚠️ Bug 1 — Using `uvm_component_utils on a Transaction

Symptom: Transaction is created, but when you call clone() or pass it through TLM, the scoreboard receives a null handle or the data is corrupted.

Root cause: The component macro prevents proper factory-based creation of objects. The transaction ends up with a broken reference chain because component-type objects expect a parent argument that data objects don't have.

SystemVerilog — Bug 1: Wrong Macro on Transaction
// ❌ WRONG — component macro on a data object
class bad_txn extends uvm_sequence_item;
`uvm_component_utils(bad_txn)  // ← WRONG macro
rand bit[31:0] data;
function new(string n, uvm_component p);  // ← forced to add parent
super.new(n, p);
endfunction
endclass
// This fails because:
// 1. Sequences call: req = bad_txn::type_id::create("req") — no parent
// 2. Factory expects object registration, gets component registration
// 3. $cast fails silently in write() callbacks
 
// ✓ CORRECT
class good_txn extends uvm_sequence_item;
`uvm_object_utils(good_txn)   // ← correct macro
rand bit[31:0] data;
function new(string name = "good_txn");  // ← no parent
super.new(name);
endfunction
endclass
SystemVerilog — Bug 2: Component Method on Object
// ❌ WRONG — trying to use component methods inside a transaction
class bad_txn extends uvm_sequence_item;
`uvm_object_utils(bad_txn)
 
function void do_something();
uvm_component p = get_parent();   // ← COMPILE ERROR
// uvm_object does NOT have get_parent()
// Only uvm_component has parent-child hierarchy
endfunction
endclass
 
// ✓ CORRECT — access config from the component that OWNS the sequence
class my_sequence extends uvm_sequence#(apb_txn);
`uvm_object_utils(my_sequence)
int num_txns;
 
task body();
// Get config through the sequencer's parent component
if(!uvm_config_db#(int)::get(
get_sequencer(), "", "num_txns", num_txns))
num_txns = 10;   // safe default
endtask
endclass

Ready-to-Run Simulator Demo

This complete testbench demonstrates all three hierarchy levels — uvm_void, uvm_object, and uvm_component — plus shows the key API methods inherited at each level. Save as hierarchy_demo.sv and run it.

SystemVerilog — hierarchy_demo.sv (Complete, Ready-to-Run)
// ================================================================
// hierarchy_demo.sv — UVM Class Hierarchy Demonstration
// Demonstrates: uvm_void → uvm_object → uvm_component methods
//
// Compile (Questa):
//   vlog -sv -L uvm_1_2 hierarchy_demo.sv
//   vsim -c -L uvm_1_2 hierarchy_demo_top -do "run -all; quit"
//
// Compile (VCS):
//   vcs -sverilog -ntb_opts uvm-1.2 hierarchy_demo.sv
//   ./simv
//
// Compile (Xcelium):
//   xrun -sv -uvm hierarchy_demo.sv
// ================================================================
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── Level 1: uvm_object subclass — data, no hierarchy ─────────────
class demo_txn extends uvm_sequence_item;
`uvm_object_utils_begin(demo_txn)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_object_utils_end
 
rand bit[31:0] data;
rand bit[31:0] addr;
 
function new(string name = "demo_txn");
super.new(name);
endfunction
endclass
 
// ── Level 2: uvm_component subclass — structural, has phases ─────
class demo_comp extends uvm_component;
`uvm_component_utils(demo_comp)
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info("DEMO", $sformatf(
"[comp] name='%s' full_path='%s' depth=%0d",
get_name(), get_full_name(), get_depth()),
UVM_NONE)
endfunction
endclass
 
// ── Test: demonstrates object vs component capabilities ──────────
class hierarchy_test extends uvm_test;
`uvm_component_utils(hierarchy_test)
 
demo_comp child;
 
function new(string n, uvm_component p); super.new(n,p); endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
child = demo_comp::type_id::create("child", this);
endfunction
 
task run_phase(uvm_phase phase);
demo_txn t1, t2;
phase.raise_objection(this);
 
`uvm_info("TEST", "\n=== uvm_object API demonstration ===", UVM_NONE)
 
// Create via factory (correct way)
t1 = demo_txn::type_id::create("txn_1");
void'(t1.randomize());
 
$display("  get_type_name: %s",  t1.get_type_name());
$display("  get_name:      %s",  t1.get_name());
 
// clone() — deep copy, both point to independent data
$cast(t2, t1.clone());
t2.addr = 32'hCAFEBABE;
$display("  t1.addr after clone+modify t2: 0x%0h", t1.addr);
$display("  t2.addr: 0x%0h", t2.addr);
 
// compare() — field-level equality check
$display("  compare (should be 0): %0b", t1.compare(t2));
t2.addr = t1.addr;  // make equal
t2.data = t1.data;
$display("  compare (should be 1): %0b", t1.compare(t2));
 
// print() — automated field display
`uvm_info("TEST", "\nTransaction print():", UVM_NONE)
t1.print();
 
`uvm_info("TEST", "\n=== uvm_component API demonstration ===", UVM_NONE)
$display("  test full_name:  %s", get_full_name());
$display("  child full_name: %s", child.get_full_name());
$display("  child depth:     %0d", child.get_depth());
$display("  num_children:    %0d", get_num_children());
 
phase.drop_objection(this);
endtask
endclass
 
module hierarchy_demo_top;
initial run_test("hierarchy_test");
endmodule
 
// ================================================================
// Expected Output (values vary due to randomize()):
//
// [DEMO] [comp] name='child' full_path='uvm_test_top.child' depth=2
//
// [TEST] === uvm_object API demonstration ===
//   get_type_name: demo_txn
//   get_name:      txn_1
//   t1.addr after clone+modify t2: 0xa3f91bc2  ← unchanged
//   t2.addr: 0xcafebabe                         ← different object
//   compare (should be 0): 0
//   compare (should be 1): 1
//
// Transaction print():
// -----------------------------------------------
// Name         Type      Size  Value
// -----------------------------------------------
// txn_1        demo_txn  -     @2
//   data       integral  32    'h7e045d31
//   addr       integral  32    'ha3f91bc2
//
// [TEST] === uvm_component API demonstration ===
//   test full_name:  uvm_test_top
//   child full_name: uvm_test_top.child
//   child depth:     2
//   num_children:    1
// ================================================================

Interview Questions — Beginner to Expert

Beginner Level

  • Q1 What is uvm_void and why does it exist? Answer: uvm_void is an empty virtual base class at the root of the entire UVM class hierarchy. Its sole purpose is to provide a common ancestor for all UVM classes so that you can write utility code (like print and record routines) that accepts any UVM object using a uvm_void handle. You never extend it directly — it exists purely as an architectural anchor point.
  • Q2 What is the correct constructor signature for a uvm_object subclass? Answer: One argument with a default value: function new(string name = "my_class");. No parent argument. Objects are not part of the component hierarchy, so they don't have a parent. The default value is important — it allows the factory to create objects without specifying a name.

Intermediate Level

  • Q3 What does uvm_object_utils provide that uvm_component_utils does not, and vice versa? Answer: Both register the class with the UVM factory. uvm_object_utils` registers it as a data object and enables field automation via uvm_object_utils_begin/end with ``uvm_field_*. ``uvm_component_utils` additionally registers it as a component, enabling phase callbacks (build, connect, run, etc.), config_db access, and component hierarchy membership. Using the wrong one for your class type produces subtle runtime failures.
  • Q4 Why does uvm_object have a copy() method but uvm_component does not? Answer: uvm_object's copy() and clone() exist because data objects are supposed to be freely duplicated — a scoreboard needs to store a snapshot of a transaction, a coverage collector needs its own copy. Components are structural singletons in the testbench topology. Copying a driver or a scoreboard makes no semantic sense — there's only one, with a fixed place in the hierarchy. UVM intentionally makes this impossible by not providing copy/clone on components.

Senior / Tricky Level

  • Q5 A colleague's uvm_scoreboard extends uvm_scoreboard, but the write() callback is never called even though the analysis port is connected correctly. Possible cause? Answer: The most likely cause is that the scoreboard class used uvm_object_utils` instead of uvm_component_utils. When this happens, the class is not registered as a component, so the factory creates it without phase support, and worse — the analysis implementation (uvm_analysis_imp) may not be properly connected in the component tree. The write()` callback exists in the class definition, but the infrastructure to call it (the TLM analysis port connection mechanism) depends on component hierarchy registration.
  • Q6 Is it possible to store a uvm_component handle inside a uvm_sequence_item? What are the risks? Answer: Syntactically yes — both are objects, so you can store any handle. But this creates a severe anti-pattern: it couples stimulus data to testbench structure, breaking reuse and portability. The sequence item should be data-only. Accessing the component through the item also risks phase ordering issues — the component may be in a different phase state when the item is accessed later. The correct pattern is to pass configuration data into transactions through constraints or sequence configuration, not by embedding component handles.

Best Practices — Rules That Actually Get Enforced in Code Review

These guidelines come directly from code review checklists at semiconductor companies. They exist because someone violated them on a real project, spent days debugging a phantom issue, and then wrote the rule.

RuleCorrect PatternWhy It Matters
One-arg constructor for objectsfunction new(string name = "my_txn");Factory requires this signature for type_id::create(name) to work
Two-arg constructor for componentsfunction new(string name, uvm_component parent);No default for parent — prevents accidental orphan components
Correct macro: objects vs componentstxn→ uvm_object_utils`; driver→ uvm_component_utils`Wrong macro = broken phases, broken factory, broken TLM
super.build_phase() first linesuper.build_phase(phase); before everything elseParent class may configure child creation order
Never store component handles in objectsPass data through config or sequence parametersBreaks reusability, creates phase coupling, hard to debug
Use factory, not new()T::type_id::create("name")Direct new() bypasses factory — type overrides never work
Clone before storing transactions$cast(stored, txn.clone())Scoreboard queues need independent copies, not aliases

Summary — The Foundation That Everything Else Builds On

The UVM class hierarchy isn't bureaucracy. It's the load-bearing wall of every testbench you'll ever write. Get this right and the rest of UVM clicks into place. Get it wrong and you'll spend hours debugging phantom issues that compile cleanly but break at runtime.

ClassRoleKey MethodsConstructor
uvm_voidUniversal root — exists only as an anchorNone (empty virtual class)Not instantiated
uvm_objectAll data objects: transactions, config, sequencescopy(), clone(), compare(), print(), pack()new(string name = "")
uvm_componentAll structural elements: drivers, monitors, scoreboardsbuild_phase(), run_phase(), get_full_name(), get_parent()new(string name, uvm_component parent)

Three rules to internalize permanently:

  • 1 Objects are data. Components are structure. Transactions, sequences, and configuration are objects. Drivers, monitors, agents, and scoreboards are components. Never mix the patterns.
  • 2 The constructor signature is a contract. One-arg default for objects, two-arg no-default for components. Violate this and the factory breaks silently.
  • 3 Everything you need is already there. clone(), compare(), print(), phases, config_db — all of this comes free from the hierarchy. Your job is to extend the right class and call the right constructor.