uvm_object vs uvm_component
Lifecycles, hierarchy mechanics, construction rules, factory vs new(), and what breaks when violated.
UVM Fundamentals · Module 4
Two Different Lifecycles — The Fundamental Divide
The entire uvm_object vs. uvm_component distinction reduces to one question: how long does this thing need to exist?
uvm_object — Data. Created when needed, discarded when done.
- A transaction is created by a sequence, driven by the driver, checked by the scoreboard. After checking, it is no longer referenced — the garbage collector reclaims it.
- A configuration object is created in a test, passed via config_db, retrieved by a driver. Once the driver has its data, the config object may be discarded.
- Thousands of objects are created and discarded in a single simulation. Their lifetimes are short and variable.
new()ortype_id::create()— anywhere in the code. uvm_component — Structure. Built once, persists forever.- A driver is built in
build_phase, runs duringrun_phase, and exists until the simulator shuts down. It is never discarded mid-simulation. - An environment is built, connected, and then runs. It holds handles to every agent, scoreboard, and coverage collector — all of which must exist for the entire simulation.
- Components form a static topology. The number of components is fixed after
build_phasecompletes. type_id::create(name, parent)— only duringbuild_phase.
uvm_object Lifecycle — Create, Use, Discard
A uvm_object follows the standard SystemVerilog garbage-collection model: it lives as long as at least one handle points to it. When the last handle goes out of scope or is explicitly set to null, the simulator can reclaim the memory.
build_phaseconnectrun_phasecheck / reportfinaluvm_componentdriver · monitor · agent · scoreboard · env · test — created in build_phase, never discardeduvm_object (transactions)txn_001txn_002txn_003txn_004txn_005txn_N· · ·createdin buildpersists untilsimulation endssequence createseach transactionGC'd after lasthandle is released
Figure 1 — uvm_component lives for the entire simulation (green bar). uvm_object instances are created and garbage-collected repeatedly during run_phase (amber bars).
What uvm_object Provides
Beyond being a base class, uvm_object gives every derived class six concrete capabilities. You get all of them for free — they just need to be wired up with the field macros or the do_*() methods (covered in Modules 16 and 17).
| Capability | Method | What It Does |
|---|---|---|
| Identity | get_name(), get_type_name() | Returns the instance name and class name. Used in debug messages and factory lookups. |
| Printing | print(), sprint() | Formats all fields into a human-readable table. Driven by do_print(). |
| Copying | copy() | Copies all fields from one object into an existing object handle. |
| Cloning | clone() | Creates a new object and copies all fields. Returns a handle to the new copy. |
| Comparison | compare() | Field-by-field equality check. Returns 1 if equal. Scoreboards rely on this. |
| Packing / Unpacking | pack_bytes(), unpack_bytes() | Serialises fields to a byte array. Used for coverage, constrained-random byte streams, and some coverage databases. |
uvm_component Lifecycle — Build, Connect, Run, Persist
A uvm_component's lifetime is tied to the simulation itself. From the moment it is created in build_phase to the moment the simulator exits, the component is alive and reachable through the hierarchy.
The framework guarantees a precise execution order for component phase callbacks. You never schedule these yourself — you override the ones you need, and the framework calls them at the right time.
| Phase | Type | Order | Your Responsibility |
|---|---|---|---|
build_phase | Function | Top-down (parent before child) | Create sub-components with type_id::create(). Get config from uvm_config_db. |
connect_phase | Function | Bottom-up (child before parent) | Connect TLM ports: driver.seq_item_port.connect(seqr.seq_item_export). |
start_of_simulation_phase | Function | Bottom-up | Print topology, validate configuration. |
run_phase | Task (time-consuming) | All components in parallel | Drive and monitor — the main simulation body. |
extract_phase | Function | Bottom-up | Pull final values from DUT, compute derived metrics. |
check_phase | Function | Bottom-up | Assert correctness. Use ``uvm_error` for failures. |
report_phase | Function | Bottom-up | Print summary statistics. |
final_phase | Function | Top-down | Close files, flush buffers, last-resort cleanup. |
The Component Hierarchy Tree
Every uvm_component has a parent — except uvm_test_top, which the framework creates as the root. The parent-child relationship is set at construction time and never changes. It gives every component a unique, dot-separated full path name.
COMPONENT TREEFULL PATH (get_full_name())uvm_test_topmy_test (created by run_test())uvm_test_topRoot — no parent. Created by the framework.my_envmy_env::type_id::create("env", this)uvm_test_top.envparent = uvm_test_top · name = "env"apb_agentuvm_test_top.env.apb_agentparent = my_env · name = "apb_agent"my_scbuvm_test_top.env.scbparent = my_env · name = "scb"drvmonseqruvm_test_top.env.apb_agent.drvparent = apb_agent · name = "drv"uvm_test_top.env.apb_agent.monparent = apb_agent · name = "mon"
Figure 2 — Component hierarchy tree. Every component's full path is the dot-separated chain of name arguments passed to create(). This path is used by config_db, factory instance overrides, and all UVM messages.
Hierarchy Navigation Methods
The hierarchy is navigable at runtime. These methods work on any uvm_component:
// Inside any uvm_component:
// ── Identity ───────────────────────────────────────────────────
string my_name = get_name(); // "drv" (just this component's name)
string full = get_full_name(); // "uvm_test_top.env.apb_agent.drv"
string cls = get_type_name(); // "apb_driver"
// ── Navigation ─────────────────────────────────────────────────
uvm_component par = get_parent(); // returns the apb_agent handle
uvm_component child;
// iterate all direct children
string child_name;
if (get_first_child(child_name)) begin
do begin
child = get_child(child_name);
`uvm_info("TOPO", child.get_full_name(), UVM_LOW)
end while (get_next_child(child_name));
end
// ── Lookup by path ─────────────────────────────────────────────
uvm_component found;
found = uvm_root::get().find("uvm_test_top.env.apb_agent.drv");
// ── Depth in the tree ──────────────────────────────────────────
int depth = get_depth(); // 0 = uvm_test_top, 1 = env, 2 = agent, 3 = driverConstruction Rules — What, When, and Where
These rules are not suggestions. Violating them causes failures that are difficult to diagnose because the error often appears far from the violation site.
| Rule | uvm_object | uvm_component |
|---|---|---|
| When can it be created? | Anywhere — function, task, build_phase, run_phase, connect_phase, in-line during simulation. | Only during build_phase. Creating after build_phase is complete means the component misses all phase callbacks, has no place in the hierarchy, and config_db cannot find it. |
| Constructor arguments | new(string name = "my_obj") — one argument with default. Parent is never specified. | new(string name, uvm_component parent) — two arguments, no defaults. The parent argument places it in the tree permanently. |
| How to create it | type_id::create("name") through the factory, or new("name") if factory override is not needed. | Always type_id::create("name", this) through the factory. Direct new() works but bypasses all override capability. |
| Can it be cloned? | Yes — clone() creates a deep copy. | No — components cannot be cloned. Their identity and position in the hierarchy tree is fixed. |
| Can it flow through TLM ports? | Yes — transactions are the fundamental unit of TLM communication. | No — TLM ports carry uvm_sequence_item derived objects, not components. |
| Is it held by the hierarchy? | No — garbage-collected when no handles remain. | Yes — every component is held in its parent's child list and is never GC'd. |
factory create() vs. Direct new() — Why It Matters
Both type_id::create() and new() produce a working object. The difference only becomes visible when you try to override a type — which is exactly what makes UVM testbenches reusable.
// ── Scenario: you want to swap apb_driver with error_inject_driver ──
// ❌ WRONG — direct new() bypasses the factory ──────────────────────
function void build_phase(uvm_phase phase);
drv = new("drv", this); // always creates apb_driver, override impossible
endfunction
// In your test, you call:
// apb_driver::type_id::set_type_override(error_inject_driver::get_type());
// ...but it has NO effect because build_phase used new() directly.
// ✓ CORRECT — factory create() respects overrides ─────────────────
function void build_phase(uvm_phase phase);
drv = apb_driver::type_id::create("drv", this);
// Factory checks: "Is there an override for apb_driver?"
// If yes → creates error_inject_driver instead.
// If no → creates apb_driver as written.
endfunction
// In your error injection test:
class error_inject_test extends base_test;
`uvm_component_utils(error_inject_test)
function void build_phase(uvm_phase phase);
// Override before super.build_phase() which triggers env.build_phase()
apb_driver::type_id::set_type_override(error_inject_driver::get_type());
super.build_phase(phase);
endfunction
endclass
// Result: env creates error_inject_driver without touching any env/agent code.What Breaks When You Violate the Rules
These are the most common rule violations and their symptoms. Knowing them saves hours of debugging.
Violation 1 — Creating a Component in run_phase
// ❌ WRONG — component created in run_phase
task run_phase(uvm_phase phase);
my_sub_monitor extra_mon;
extra_mon = my_sub_monitor::type_id::create("extra_mon", this);
// extra_mon EXISTS in the hierarchy but:
// ✗ Its build_phase() was never called
// ✗ Its connect_phase() was never called
// ✗ Its run_phase() was never started
// ✗ config_db::get() will fail (get_full_name() path is wrong)
// Result: a broken component that silently does nothing
endtaskViolation 2 — Extending uvm_component for a Transaction
// ❌ WRONG — extending uvm_component for data that flows through TLM ports
class apb_txn extends uvm_component; // ERROR: wrong base class
`uvm_component_utils(apb_txn)
rand bit [31:0] addr;
rand bit [7:0] data;
function new(string name, uvm_component parent); // must pass parent!
super.new(name, parent);
endfunction
endclass
// What breaks:
// ✗ Cannot use as TLM sequence_item — uvm_driver expects uvm_sequence_item
// ✗ Cannot clone() — components do not support clone
// ✗ Every "transaction" permanently occupies a slot in the hierarchy tree
// ✗ Sequences cannot start on it
// ✗ Randomize runs but factory override doesn't work via `uvm_object_utilsViolation 3 — Wrong parent Argument
// In my_agent.sv (path = "uvm_test_top.env.apb_agent")
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// ❌ WRONG — null parent disconnects driver from hierarchy
drv = apb_driver::type_id::create("drv", null);
// drv.get_full_name() → "drv" (not "uvm_test_top.env.apb_agent.drv")
// uvm_config_db::get(this, "drv", "vif", vif) → FAILS
// Because the scoping path doesn't match
// ✓ CORRECT — 'this' = the agent, places driver as its child
drv = apb_driver::type_id::create("drv", this);
// drv.get_full_name() → "uvm_test_top.env.apb_agent.drv" ✓
endfunctionThe Three Signs You Have the Wrong Base Class
- Your "transaction" class must pass a
parentargument tonew()— it probably extendsuvm_componentinstead ofuvm_sequence_item. - Your "component" can be cloned — it probably extends
uvm_objectand should extenduvm_component. - A factory override has no effect — the code uses
new()instead oftype_id::create(), or the wrong macro was used (``uvm_object_utils` on a component or vice versa).
Quick Reference — The Complete Decision Checklist
// ════════════════════════════════════════════════════════════════
// DECISION CHECKLIST
//
// ✓ Is it data that moves between components? → uvm_sequence_item
// ✓ Is it a sequence that generates stimulus? → uvm_sequence
// ✓ Does it need build_phase / run_phase callbacks? → uvm_component
// ✓ Is there exactly one instance in the topology? → uvm_component
// ✓ Is it created and discarded repeatedly? → uvm_object
// ✓ Does it get cloned? → uvm_object
// ✓ Does it flow through a TLM port? → uvm_sequence_item
// ════════════════════════════════════════════════════════════════
// ── uvm_object: one arg, uvm_object_utils, factory with no parent ──
class my_txn extends uvm_sequence_item;
`uvm_object_utils(my_txn)
function new(string name = "my_txn");
super.new(name);
endfunction
endclass
// Create: my_txn::type_id::create("txn") — no parent
// ── uvm_component: two args, uvm_component_utils, factory WITH parent ─
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
function void build_phase(uvm_phase phase);
super.build_phase(phase); // create sub-components here
endfunction
task run_phase(uvm_phase phase);
// drive DUT here
endtask
endclass
// Create: my_driver::type_id::create("drv", this) — parent = this (the agent)
// ── Key get_full_name() paths (knowing this is essential for config_db) ─
// uvm_test_top ← the test itself
// uvm_test_top.env ← environment
// uvm_test_top.env.apb_agent ← agent
// uvm_test_top.env.apb_agent.drv ← driver
// uvm_test_top.env.apb_agent.mon ← monitor
// uvm_test_top.env.apb_agent.seqr ← sequencer
// uvm_test_top.env.scb ← scoreboardCode Examples — Lifecycle and Construction in Action
The lifecycle differences between objects and components are best understood by watching them behave differently in simulation. These four examples build from the simplest possible case to the production patterns that expose the subtle rules engineers get wrong.
Example 1 — Beginner: Object Created, Used, Discarded
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── uvm_object: created on demand, garbage-collected when unreferenced
class simple_txn extends uvm_sequence_item;
`uvm_object_utils(simple_txn)
rand bit[7:0] data;
function new(string n="simple_txn"); super.new(n); endfunction
endclass
// ── uvm_component: created once in build_phase, lives forever
class lifecycle_test extends uvm_test;
`uvm_component_utils(lifecycle_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
// Object: created on demand inside a task
begin
simple_txn txn = simple_txn::type_id::create("txn");
void'(txn.randomize());
$display("[OBJ] Created @ %0t: data=%0h", $time, txn.data);
#10ns;
$display("[OBJ] Using @ %0t: data=%0h", $time, txn.data);
end // txn goes out of scope → eligible for GC
$display("[OBJ] txn out of scope — garbage collected");
// Component: already exists — we're inside it right now
$display("[COMP] I (%s) have existed since build_phase", get_full_name());
$display("[COMP] I will exist until simulation ends");
$display("[COMP] My depth in hierarchy: %0d", get_depth());
#5ns;
phase.drop_objection(this);
endtask
endclass
module lifecycle_top;
initial run_test("lifecycle_test");
endmodule
// Expected Output:
// [OBJ] Created @ 0: data=a3
// [OBJ] Using @ 10: data=a3
// [OBJ] txn out of scope — garbage collected
// [COMP] I (uvm_test_top) have existed since build_phase
// [COMP] I will exist until simulation ends
// [COMP] My depth in hierarchy: 1Example 2 — Intermediate: Constructor Signature Determines Hierarchy
`include "uvm_macros.svh"
import uvm_pkg::*;
class child_comp extends uvm_component;
`uvm_component_utils(child_comp)
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
$display(" [%s] build_phase — depth=%0d parent=%s",
get_name(), get_depth(), get_parent().get_name());
endfunction
endclass
class parent_test extends uvm_test;
`uvm_component_utils(parent_test)
child_comp child_a, child_b;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Both children are placed under THIS component in the hierarchy
child_a = child_comp::type_id::create("child_a", this);
child_b = child_comp::type_id::create("child_b", this);
$display("[%s] I have %0d children", get_name(), get_num_children());
endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("TEST", "Printing component topology:", UVM_NONE)
uvm_root::get().print_topology();
phase.drop_objection(this);
endtask
endclass
module hierarchy_top;
initial run_test("parent_test");
endmodule
// Expected Output:
// [uvm_test_top] I have 2 children
// [child_a] build_phase — depth=2 parent=uvm_test_top
// [child_b] build_phase — depth=2 parent=uvm_test_top
// uvm_test_top (parent_test)
// child_a (child_comp)
// child_b (child_comp)Example 3 — Verification: Why Scoreboard Uses Component, Transaction Uses Object
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── OBJECT: APB transaction — flows through testbench, created many times
class apb_txn extends uvm_sequence_item; // ← uvm_object family
`uvm_object_utils(apb_txn)
rand bit[31:0] addr, data;
rand bit write;
function new(string n="apb_txn"); super.new(n); endfunction
function string convert2string();
return $sformatf("%s a=%0h d=%0h",write?"WR":"RD",addr,data);
endfunction
endclass
// ── COMPONENT: Scoreboard — exists once, receives transactions, checks them
class apb_scoreboard extends uvm_scoreboard; // ← uvm_component family
`uvm_component_utils(apb_scoreboard)
uvm_analysis_imp#(apb_txn, apb_scoreboard) mon_imp;
apb_txn expected_q[$]; // queue of expected objects
int pass_cnt, fail_cnt;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon_imp = new("mon_imp", this);
endfunction
// write() receives a uvm_object (transaction) at runtime
function void write(apb_txn actual);
apb_txn exp;
if(expected_q.size() == 0) begin
`uvm_error("SCB","No expected transaction available")
return;
end
exp = expected_q.pop_front();
if(actual.addr==exp.addr && actual.data==exp.data && actual.write==exp.write) begin
`uvm_info("SCB",$sformatf("PASS: %s",actual.convert2string()),UVM_LOW)
pass_cnt++;
end else begin
`uvm_error("SCB",$sformatf("FAIL: got %s exp %s",
actual.convert2string(), exp.convert2string()))
fail_cnt++;
end
endfunction
function void check_phase(uvm_phase phase);
super.check_phase(phase);
$display("\n[SCB] Results: PASS=%0d FAIL=%0d", pass_cnt, fail_cnt);
endfunction
endclass
// Key observation:
// - apb_txn (object): 100s created per test, garbage-collected freely
// - apb_scoreboard (component): ONE instance, lives entire simulation
// - The scoreboard receives objects and stores them in a queue
// - Queue grows/shrinks dynamically — objects are born and die
// - Scoreboard never dies — it has a parent and permanent hierarchy positionExample 4 — Tricky: The Phantom Component (Created Outside build_phase)
`include "uvm_macros.svh"
import uvm_pkg::*;
class phantom_demo extends uvm_test;
`uvm_component_utils(phantom_demo)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
// ❌ WRONG: Creating a component in run_phase
begin
uvm_component phantom;
phantom = uvm_component::type_id::create("phantom", this);
// phantom EXISTS in the hierarchy
$display("phantom path: %s", phantom.get_full_name());
// BUT its build_phase, connect_phase already ran → NEVER called
// It missed build_phase, connect_phase, start_of_simulation_phase
// It WILL NOT receive run_phase callback either
// uvm_config_db lookups by path WILL FAIL for it
end
// ✓ CORRECT: Objects CAN be created anywhere, anytime
begin
uvm_sequence_item item = uvm_sequence_item::type_id::create("item");
$display("item name: %s (no path — it is an object)", item.get_name());
// Objects: no phases, no hierarchy, totally fine to create any time
end
#5ns;
phase.drop_objection(this);
endtask
endclass
module phantom_top;
initial run_test("phantom_demo");
endmodule
// This is why the rule exists:
// COMPONENTS → only in build_phase (receive all phase callbacks)
// OBJECTS → anywhere, anytime (no phases, no restrictions)Common Bugs — The Ones That Take Days to Find
Every one of these bugs compiles without error. The simulator starts, build_phase runs, and things look fine — until they silently don't. These are the top object vs. component confusion bugs in production UVM code.
⚠️ Bug 1 — Component Created in run_phase (The Phantom Component)
Symptom: Component appears in print_topology() output and has a valid full path, but uvm_config_db::get() always returns 0 for it. No error messages. Quiet failure.
Root cause: When a component is created in run_phase, it misses the build_phase and connect_phase. Config DB lookups that require the component to have been registered during build_phase will always fail. The component is in the hierarchy tree but was never initialized.
// ❌ WRONG — component created in task (run_phase context)
task run_phase(uvm_phase phase);
my_monitor late_mon;
late_mon = my_monitor::type_id::create("late_mon", this);
// late_mon.build_phase() was NEVER called
// late_mon.vif = null (config_db lookup never happened)
// late_mon.run_phase() will NEVER be called by the framework
// Manually calling: late_mon.run_phase(phase) creates a new uvm_phase context — WRONG
endtask
// ✓ CORRECT — component created in build_phase
my_monitor mon; // class-level handle
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = my_monitor::type_id::create("mon", this);
// mon.build_phase() called automatically by UVM framework
// mon.connect_phase() called automatically
// mon.run_phase() called automatically — it's a proper component
endfunction⚠️ Bug 2 — Storing Object Handle in Component, Forgetting clone()
Symptom: Scoreboard comparison always passes, even when you inject errors. Or all stored transactions suddenly have the same data — the last value that was randomized.
Root cause: Objects are handles (references), not values. Storing the handle without cloning means you're storing a reference to the same object. When the sequence randomizes and reuses the transaction, every stored reference now shows the new randomized values.
// ❌ WRONG — storing raw handle (alias, not a copy)
apb_txn expected_q[$];
function void write_expected(apb_txn txn);
expected_q.push_back(txn); // ← just stores the reference</ // If the caller randomizes txn again, ALL entries in the queue change
endfunction
// ✓ CORRECT — clone the object to get an independent copy
function void write_expected(apb_txn txn);
apb_txn copy;
$cast(copy, txn.clone()); // ← independent deep copy
expected_q.push_back(copy); // ← now safe to randomize txn again
endfunction
// Debug: If your scoreboard always passes regardless of DUT output
// → check whether you're cloning before storing expected transactions
// → print expected_q contents; if all entries are identical → aliasing bug🔍 Debug Insight — Object vs Component Runtime Checks
- Phases not firing: Run
uvm_root::get().print_topology()instart_of_simulation_phase. If a component is missing → it was created after build_phase cascade, or wrong macro used. - Scoreboard queue corruption: Add
$display("%p", expected_q[i])to print object addresses. If all entries have the same address → aliasing bug, you needclone(). - Config_db returning 0: If your component was created in run_phase, config_db path resolution was set up before it existed. Create it in build_phase.
- get_type_name() wrong: If
get_type_name()returns "uvm_component" or "uvm_sequence_item" → factory macro missing from your class definition.
Ready-to-Run Complete Demo
This single-file demo shows all the critical differences — lifecycle, hierarchy, construction rules, and cloning — in one compilable testbench. Save as obj_vs_comp_demo.sv and run it.
// ================================================================
// obj_vs_comp_demo.sv — uvm_object vs uvm_component Demonstration
//
// Compile (Questa):
// vlog -sv -L uvm_1_2 obj_vs_comp_demo.sv
// vsim -c -L uvm_1_2 obj_vs_comp_demo_top -do "run -all; quit"
//
// Compile (VCS):
// vcs -sverilog -ntb_opts uvm-1.2 obj_vs_comp_demo.sv && ./simv
//
// Compile (Xcelium):
// xrun -sv -uvm obj_vs_comp_demo.sv
// ================================================================
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── OBJECT: data that flows — no hierarchy, no phases ─────────────
class demo_pkt extends uvm_sequence_item;
`uvm_object_utils_begin(demo_pkt)
`uvm_field_int(id, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_object_utils_end
rand bit[7:0] id;
rand bit[31:0] data;
function new(string n="demo_pkt"); super.new(n); endfunction
function string convert2string();
return $sformatf("pkt[id=%0d data=0x%0h]", id, data);
endfunction
endclass
// ── COMPONENT: structural, has phases, fixed in hierarchy ──────────
class demo_collector extends uvm_component;
`uvm_component_utils(demo_collector)
demo_pkt collected_q[$];
int received_count;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
$display(" [collector] build_phase @ %0t — I am %s",
$time, get_full_name());
endfunction
function void receive(demo_pkt pkt);
demo_pkt stored;
$cast(stored, pkt.clone()); // ← CLONE before storing
collected_q.push_back(stored);
received_count++;
$display(" [collector] Received #%0d: %s",
received_count, stored.convert2string());
endfunction
function void check_phase(uvm_phase phase);
super.check_phase(phase);
$display(" [collector] check_phase — %0d packets collected", received_count);
endfunction
endclass
// ── TEST: orchestrates objects and components ──────────────────────
class demo_test extends uvm_test;
`uvm_component_utils(demo_test)
demo_collector collector; // component — class member (persists)
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
collector = demo_collector::type_id::create("collector", this);
endfunction
task run_phase(uvm_phase phase);
demo_pkt pkt; // object — local variable (created on demand)
phase.raise_objection(this);
`uvm_info("TEST", "\n=== Creating and sending 4 packets ===", UVM_NONE)
// Each iteration: create a NEW object, use it, pass to collector
repeat(4) begin
pkt = demo_pkt::type_id::create($sformatf("pkt_%0d",
collector.received_count + 1));
void'(pkt.randomize());
collector.receive(pkt);
#5ns;
end
`uvm_info("TEST", "\n=== Object count vs Component count ===", UVM_NONE)
$display(" Objects created: 4 (all eligible for GC now)");
$display(" Components: %0d (in hierarchy, permanent)",
get_num_children() + 1);
`uvm_info("TEST", "\n=== Topology ===", UVM_NONE)
uvm_root::get().print_topology();
phase.drop_objection(this);
endtask
endclass
module obj_vs_comp_demo_top;
initial run_test("demo_test");
endmodule
// ================================================================
// Expected Output:
// [collector] build_phase @ 0 — I am uvm_test_top.collector
//
// === Creating and sending 4 packets ===
// [collector] Received #1: pkt[id=42 data=0xa3f2b1c0]
// [collector] Received #2: pkt[id=17 data=0x7e920f44]
// [collector] Received #3: pkt[id=99 data=0x1bc5d830]
// [collector] Received #4: pkt[id=05 data=0xf04a71e2]
//
// === Object count vs Component count ===
// Objects created: 4 (all eligible for GC now)
// Components: 2 (in hierarchy, permanent)
//
// === Topology ===
// uvm_test_top (demo_test)
// collector (demo_collector)
//
// [collector] check_phase — 4 packets collected
// ================================================================Interview Questions — From Fresher to Architect
Beginner Level
- Q1 When should you extend uvm_object versus uvm_component? Answer: Extend
uvm_object(or its subclasses likeuvm_sequence_item) when the class represents data that moves through the testbench — transactions, sequences, configuration objects. Extenduvm_component(or its subclasses) when the class represents a structural element that lives in the testbench topology — driver, monitor, scoreboard, agent, environment, test. The key question: does it need build_phase to configure itself, connect_phase to wire up ports, and run_phase to execute continuously? If yes → component. If it's data being created, passed around, and discarded → object. - Q2 What is the lifecycle difference between a uvm_object and a uvm_component? Answer:
uvm_objecthas a dynamic lifecycle — it's created when needed (via factory ornew()), used, and garbage-collected when no more references point to it. Hundreds may exist simultaneously and be discarded.uvm_componenthas a static lifecycle — created once inbuild_phase, it persists for the entire simulation in a fixed position in the component hierarchy. It receives all phase callbacks and is never garbage-collected.
Intermediate Level
- Q3 What happens if you create a uvm_component in run_phase instead of build_phase? Answer: The component IS created and gets a valid hierarchy position. However, it has already missed the
build_phase,connect_phase,start_of_simulation_phasecallbacks — those already ran for all other components. It also will not receive arun_phasecallback from the framework. Config DB lookups by path fail because the component wasn't present when config was set up. The component is a "phantom" — it exists in the tree but never functions correctly. Always create components inbuild_phase. - Q4 Why does storing a transaction in a scoreboard queue without clone() cause the "scoreboard always passes" bug? Answer: In SystemVerilog, class variables are handles (references), not copies. When you push a transaction handle into a queue without cloning, you store a reference to the same object that the driver or sequence is still using. When the driver randomizes the same transaction object for the next beat, every entry in your queue now reflects the new randomized values — because they all point to the same memory location. The scoreboard then compares the "expected" value against the "actual" — both of which are now the same (latest) randomized values, so it always matches. Fix: always
$cast(copy, txn.clone())before storing.
Senior / Architect Level
- Q5 A uvm_sequence is a uvm_object, not a uvm_component. It runs with timing, drives the DUT, and executes for long periods. How does a data object "execute"? Answer: Sequences are objects that execute through a delegation mechanism — they start on a sequencer, which is a component. The sequencer manages a thread for the sequence's
body()task. The sequence usesstart_item()andfinish_item()to coordinate with the sequencer's thread. The sequence itself has no run_phase callback from UVM; it borrows the sequencer's phase execution context. This is why you callsequence.start(sequencer)— the sequence has no lifecycle of its own; it parasitically executes within the component's lifecycle. This design lets sequences be created and discarded freely while the infrastructure (sequencer, driver) persists.
Best Practices — Rules That Engineers Learn After Getting Burned
| Rule | Correct Pattern | Why It Matters |
|---|---|---|
| Components only in build_phase | type_id::create("name", this) inside build_phase() | Ensures all phase callbacks fire correctly; config_db lookups by path work |
| Objects anywhere, but clone before storing | $cast(copy, txn.clone()); queue.push_back(copy); | Prevents aliasing bugs where "expected" values silently change |
| One-arg constructor for objects | function new(string name = "..."); | Factory uses one-arg construction; missing default causes null returns |
| Two-arg constructor for components | function new(string name, uvm_component parent); | No default for parent — prevents accidental orphan components |
| Never use new() directly for UVM classes | Always type_id::create() through the factory | Direct new() bypasses factory — type overrides won't work |
| Keep objects pure data | No component handles, no config_db calls inside transactions | Transactions must be reusable independently of testbench structure |
| Topology verification in start_of_simulation | uvm_root::get().print_topology() | Confirms all components are connected correctly before simulation starts |
💡 Pro Tip — The 30-Second Diagnostic for Object vs Component Problems
When something isn't working and you don't know if it's an object/component confusion:
- Run
uvm_root::get().print_topology()instart_of_simulation_phase— if a component is missing from the tree, it was created in the wrong phase. - Add
$display("I am: %s type: %s", get_full_name(), get_type_name())in build_phase of every component — if it doesn't print, the component was created too late. - If scoreboard always passes → print object addresses:
$display("addr: %0p", txn). If all expected queue entries have the same address → clone() missing.
Summary — The Rules That Govern Half of All UVM Code
The object vs. component distinction sounds like a taxonomy exercise until you hit the bugs it creates. A scoreboard that always passes. A component that exists but never responds to phase callbacks. An expected queue that silently corrupts because handle aliasing replaced clone(). Getting this distinction right early prevents days of debugging later.
| Dimension | uvm_object | uvm_component |
|---|---|---|
| Lifecycle | Dynamic — created on demand, GC'd when unused | Static — created once in build_phase, lives forever |
| Hierarchy | No position — it's data, not structure | Fixed position — parent set at construction |
| Phases | None — no build/run/check callbacks | Full phase schedule — all callbacks fire automatically |
| Constructor | One arg with default: new(name="") | Two args no default: new(name, parent) |
| Config DB | Cannot use as context | Full access — is the context for lookups |
| Clone | clone() available — always use before storing | No clone — one instance by design |
| Examples | Transactions, sequences, config objects | Driver, monitor, scoreboard, agent, env, test |
- 1 Objects are data; components are structure. This is the dividing line in your mental model. When you're deciding which class to extend, start here.
- 2 Components belong in build_phase. Creating a component anywhere else creates a phantom — it's in the hierarchy but never initialized. Non-negotiable rule.
- 3 Clone before storing any object. Every time you push an object into a queue or store it in a class member, clone it first. Handle aliasing is silent and insidious.