UVM Basics

Chapter 4 of 10

uvm_object and Transactions

The real relationship between uvm_object and uvm_component (not just their differences), the uvm_sequence_item base every transaction actually extends, and writing do_copy/do_compare/convert2string by hand instead of field macros.

Chapter 1 already drew a quick contrast: uvm_component has phases and needs a parent, uvm_object doesn't and generally only needs a name. True as far as it goes, but it makes them sound like two unrelated categories — they aren't. This chapter covers the actual relationship, and what uvm_object looks like when you write one yourself: a transaction.

The real relationship: uvm_component is built on uvm_object

uvm_component isn't a sibling of uvm_object — it's built on top of it:

uvm_object
  └── uvm_report_object
        └── uvm_component

Every uvm_component you wrote in chapters 2-3 already inherits everything uvm_object provides — for free. Spelled out, that's: create() (via the factory, chapters 3/6), clone()/copy() (this chapter), compare(), print(), and a name. One more worth knowing exists even though this track doesn't use it: record(), which feeds a transaction into a recording database that waveform-viewer tools can display alongside signals — a debug/tooling feature that belongs to dv-methodology's scope, not this track's. uvm_report_object (the layer in between) is what actually gives uvm_component its `uvm_info/`uvm_error/etc. reporting ability from chapter 2. uvm_component then adds the structural pieces on top: a parent handle, and a fixed slot in the phase-driven simulation lifecycle.

So the real question isn't "which one has more features" — uvm_component always has more, because it's built on uvm_object. The real question is what a permanent slot in the component tree and a phase-driven lifecycle actually cost, and when that cost isn't worth paying.

Two concrete differences the relationship implies

This isn't just an abstract inheritance chain — it shows up in two places you'll actually run into:

  • print() walks the tree for a component, but not for an object. print() is inherited from uvm_object, so every uvm_component has it too — but call it on a component and it does more than show that one object's fields. Because a uvm_component tracks its children (that's what the parent bookkeeping from chapter 2 is for), the built-in printer recurses into the whole subtree underneath whatever you called print() on. env.print() shows the driver's fields, the monitor's fields, and the scoreboard's fields all at once — not just env's own, which usually has none of its own to show. Call print() on a mux_transaction, and you only ever see that one transaction's sel/a/b fields, because a uvm_object has no children to walk in the first place.
  • The registration macro decides create()'s signature. `uvm_object_utils-registered types get SomeType::type_id::create(name) — no parent argument, because a uvm_object doesn't have one. `uvm_component_utils-registered types get SomeType::type_id::create(name, parent), matching the two-argument constructor chapter 3 already relied on. Using the wrong macro for a class (say, `uvm_object_utils on something meant to be a uvm_component) doesn't fail with an explanation — it shows up as a confusing compile error about a missing or unexpected argument somewhere else in the code, which is worth recognizing for what it actually is.

When plain uvm_object is the right base — and why it's a hard rule, not just a cost

In UVM's own common vocabulary, this split has a name: uvm_component is static — built once, with a fixed identity and tree position for the rest of the run — while uvm_object is dynamic (or transient) — created and destroyed as often as needed, with no fixed position anywhere. Those are the terms you'll see used for this exact distinction elsewhere (Verification Academy, most UVM references); the rest of this section is why that's true, not just a definition to memorize.

A uvm_component's place in the tree isn't just bookkeeping — the phase engine actually depends on that tree being fixed once elaboration finishes. UVM's phasing assumes the entire hierarchy is complete by the end of build_phase/connect_phase: printing the testbench's topology, and the bottom-up ordering connect_phase relies on (chapter 2), both assume a tree that doesn't change shape afterward. Constructing a new uvm_component in the middle of run_phase — one per transaction, say — isn't just wasteful, it works against an assumption the phase engine is actually built on. This is a real UVM rule, not a style preference: components get constructed during build_phase, and nowhere else.

That's the sharper version of "a permanent tree slot is wasteful": it's not only wasteful, a uvm_component built outside build_phase is doing something the methodology doesn't support in the first place. A plain uvm_object has no such restriction — a stimulus transaction generated fresh for every item driven, a sequence, a small configuration object passed around briefly, can all be created and discarded at any point during run_phase, as often as needed, precisely because none of them ever join the fixed tree to begin with.

Transactions specifically: uvm_sequence_item, not plain uvm_object

A stimulus class isn't written as extends uvm_object directly — it goes one level more specific:

uvm_object
  └── uvm_transaction
        └── uvm_sequence_item

uvm_sequence_item is uvm_object plus the bookkeeping a sequencer needs to track an item as it moves through the sequencer/driver handshake (chapter 7 uses this directly). Naming this chain here means chapter 7 doesn't need to introduce a new base class mid-chapter — by then, uvm_sequence_item is already familiar.

Writing a transaction: mux_transaction

class mux_transaction extends uvm_sequence_item;
  `uvm_object_utils(mux_transaction)
 
  rand bit sel;
  rand bit a;
  rand bit b;
 
  function new(string name = "mux_transaction");
    super.new(name);
  endfunction
 
  function void do_copy(uvm_object rhs);
    mux_transaction rhs_;
    if (!$cast(rhs_, rhs))
      `uvm_fatal("DO_COPY", "rhs is not a mux_transaction")
    super.do_copy(rhs);
    sel = rhs_.sel;
    a   = rhs_.a;
    b   = rhs_.b;
  endfunction
 
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    mux_transaction rhs_;
    if (!$cast(rhs_, rhs)) return 0;
    return super.do_compare(rhs, comparer) &&
           (sel == rhs_.sel) && (a == rhs_.a) && (b == rhs_.b);
  endfunction
 
  function string convert2string();
    return $sformatf("sel=%0b a=%0b b=%0b", sel, a, b);
  endfunction
endclass

A few things worth connecting to material you already have:

  • uvm_object's constructor only takes name — no parent, unlike uvm_component. That's the one-line proof of the difference chapter 1 already mentioned.
  • do_copy/do_compare use $cast to downcast the generic uvm_object rhs argument to mux_transaction before touching its fields — the exact same $cast pattern SV ch11 (oop-inheritance-polymorphism) taught, applied to UVM's own base classes.
  • do_copy is the standardized version of SV ch10's hand-written copy(). SV ch10 taught writing a copy() method that new()s a fresh object and copies each field across — and said outright that "UVM's uvm_object::copy()/clone() are the standardized version of exactly this idea." This is that payoff: you write do_copy() (just the field-copying part), and uvm_object's own copy() — inherited, not written by you — handles the rest. To get a copy the way SV ch10's copy() did (allocate a new object and copy into it in one step), call clone() and $cast the result:
mux_transaction tr2;
$cast(tr2, tr1.clone());
  • convert2string() replaces hand-rolled $sformatf calls scattered through driver code — write the formatting once, then call tr.convert2string() anywhere you need to print a transaction, including inside `uvm_info.

Field macros vs. hand-written methods: which one this track uses

You'll see transaction classes elsewhere written with field macros instead:

`uvm_object_utils_begin(mux_transaction)
  `uvm_field_int(sel, UVM_ALL_ON)
  `uvm_field_int(a, UVM_ALL_ON)
  `uvm_field_int(b, UVM_ALL_ON)
`uvm_object_utils_end

This generates do_copy, do_compare, convert2string, and more, all from one field list, instead of writing each method by hand. It's common in existing/legacy UVM code, and worth recognizing when you run into it — but it's also less explicit (the generated convert2string() output format isn't something you chose), and its use has fallen out of favor in newer codebases in favor of hand-written methods. This track uses hand-written do_copy/do_compare/convert2string, for the same reason SV ch10 taught a hand-written copy() instead of relying on a shortcut: it's what makes each method's exact behavior visible in the code, not generated from a macro list. (Same shape of decision as SV ch2's `timescale vs. timeunit/timeprecision — name the older, still-common style, then use the more explicit one going forward.)

Putting it to use: the driver drives transactions, not raw signals

Chapter 3's driver assigned vif.sel/vif.a/vif.b directly from hardcoded literals. Swap that for mux_transaction objects:

task run_phase(uvm_phase phase);
  mux_transaction tr;
 
  phase.raise_objection(this);
 
  tr = new();
  tr.sel = 0; tr.a = 1; tr.b = 0;
  drive(tr);
 
  tr = new();
  tr.sel = 1; tr.a = 1; tr.b = 0;
  drive(tr);
 
  phase.drop_objection(this);
endtask
 
task drive(mux_transaction tr);
  vif.sel = tr.sel;
  vif.a   = tr.a;
  vif.b   = tr.b;
  #1;
  `uvm_info("DRV", $sformatf("%s -> y=%0b", tr.convert2string(), vif.y), UVM_MEDIUM)
endtask

Still the same two hardcoded combinations chapter 3 drove — nothing about the stimulus changed yet, only how it's represented. That distinction matters: once stimulus is a structured object instead of loose local variables, it's ready to be the thing a sequence produces and hands to the driver (chapter 7) instead of something the driver invents itself.

Summary

  • uvm_component isn't a sibling of uvm_object — it's built on top of it (uvm_objectuvm_report_objectuvm_component), inheriting naming, print()/copy()/compare(), and factory registration for free, then adding a parent handle and a place in the phase-driven lifecycle.
  • print() recurses into an entire subtree for a uvm_component (because it tracks children) but only ever shows one object's own fields for a plain uvm_object; the registration macro (`uvm_object_utils vs. `uvm_component_utils) decides whether type_id::create() takes just a name or a name and a parent.
  • uvm_component is static (built once, fixed identity and tree position) and uvm_object is dynamic/transient (created and destroyed as often as needed) — standard UVM vocabulary for a rule with real teeth: components must be constructed during build_phase, and only there, because the phase engine's guarantees (like connect_phase's bottom-up ordering) depend on the tree being fixed once elaboration finishes.
  • A transaction's real base is uvm_sequence_item (uvm_objectuvm_transactionuvm_sequence_item), which adds the bookkeeping the sequencer/driver handshake (chapter 7) needs.
  • do_copy/do_compare use $cast (SV ch11) to downcast the generic uvm_object argument before touching fields; do_copy is the standardized version of SV ch10's hand-written copy(), and clone() + $cast is the closest match to what that copy() method did in one step.
  • This track writes do_copy/do_compare/convert2string by hand rather than using field macros (`uvm_field_int and friends) — common in existing code, but less explicit than writing each method out.

Which of these best describes the actual relationship between uvm_object and uvm_component?

Calling env.print() (env a uvm_component) shows fields from the driver, monitor, and scoreboard underneath it too, not just env's own. Why doesn't tr.print() (tr a mux_transaction) do anything similar?

Why does UVM insist that uvm_components only be constructed during build_phase, rather than, say, once per transaction inside run_phase?

Why is a transaction class written as extends uvm_sequence_item rather than extends uvm_object directly?

Inside do_copy(uvm_object rhs), why is $cast needed before accessing rhs's fields?

Which method call gives you a brand-new, independent copy of a transaction in one step (the closest analog to SV chapter 10's hand-written copy() method)? (lowercase, no parentheses)