Skip to content

UVM

uvm_object

The base class for all UVM data — copy, compare, print, pack, and clone via field automation or do_* hooks, the template-method pattern, and the deep-vs-shallow-copy trap.

UVM Base Classes · Module 4 · Page 4.2

The Engineering Problem

Module 3 gave you the environment as a whole; Module 4 goes down to the foundation it's built on — the base classes. The most fundamental is uvm_object, the root of all data in UVM: every transaction, configuration object, and sequence is a uvm_object. Where a component is a permanent node in the tree, a uvm_object is transient data that flows through it — and it needs a standard set of operations so the rest of the framework can copy it, compare it, print it, and serialise it without knowing its specific fields.

That standard interface is exactly what uvm_object provides, and getting it right is the difference between a testbench that checks correctly and one that lies. A scoreboard compares transactions, an expected queue clones them, a logger prints them, a bus model packs them to bytes — all through uvm_object's methods. If those methods don't account for every field, comparisons miss mismatches and clones come out incomplete. And there is a deeper trap: an object containing another object can be shallow-copied, so two "copies" secretly share one sub-object and corrupt each other. Mastering uvm_object is mastering the operations every piece of UVM data must support, and the subtle ways they go wrong.

What is uvm_object, what standard operations does it provide (copy, compare, print, pack, clone), how do you implement them — field automation versus do_* hooks — and where do deep-versus-shallow copy and missing fields cause silent bugs?

Motivation — why a standard data interface matters

uvm_object's standard operations are what let the framework manipulate data it knows nothing specific about:

  • The framework operates on data generically. A scoreboard's compare, an analysis FIFO's storage, a printer's print, a packer's pack — all call uvm_object methods, so they work on any transaction type without knowing its fields. The standard interface is what makes generic, reusable infrastructure possible.
  • Checking and cloning depend on it directly. A scoreboard compares expected vs observed with compare(); an expected queue stores clones (not handles) made with clone(). Both are only as correct as the object's method implementations — a field they skip is a field that can't be checked or preserved.
  • Two implementation paths, one right choice per context. Field automation (uvm_field_*) is fast to write and covers the common case; explicit do_* hooks give full control and avoid macro overhead for performance-critical or unusual objects. Knowing both — and when each is appropriate — is core fluency.
  • The subtle bugs are expensive. A missing field in compare() is a scoreboard that passes on real mismatches (Module 3.2); a shallow copy of a nested object is aliasing that corrupts data across "independent" transactions. Both are silent and both live in the uvm_object methods.

The motivation, in one line: every piece of UVM data is a uvm_object, and the framework's ability to copy, compare, print, and serialise it correctly — the basis of all checking and storage — rests on these methods being implemented completely and with the right copy depth.

Mental Model

Hold uvm_object as a standard-issue data record with a toolkit:

A uvm_object is a data record that comes with a standard toolkit: make a copy of me, tell me if I equal another, print me, and serialise me to bytes. It is just data — it has no place in the component tree, no phases, no lifecycle beyond being created, used, and discarded. What makes it a uvm_object rather than a plain struct is that toolkit: any code can copy, compare, print, pack, or clone it through a uniform interface, without knowing what fields it holds. You provide the toolkit one of two ways — let UVM generate it from a list of your fields (field automation), or write the operations yourself (the do_* hooks). Either way, the toolkit must account for every field, and when a field is itself an object, you must decide whether "copy me" means share the sub-object (shallow) or copy it too (deep) — getting that depth wrong is how two records secretly become one.

So when you model any data — a transaction, a config — you are building a uvm_object: declare its fields, and provide its toolkit completely, deciding copy depth deliberately for any nested objects. The mental check is always "do my copy/compare/print/pack cover every field, at the right depth?"

Visual Explanation — the standard toolkit

Every uvm_object exposes the same set of operations, grouped by purpose: identity, copy/clone, compare, print, and pack. This uniform interface is what the framework relies on.

uvm_object operations: identity, copy/clone, compare, print/sprint, pack/unpackThe uvm_object operation toolkitThe uvm_object operation toolkitIdentityget_name / set_name, get_type_name — naming and type, plus factory registration (uvm_object_utils)get_name / set_name, get_type_name — naming and type, plus factory registration (uvm_object_utils)Copy / clonecopy(rhs) into this object; clone() returns a fresh deep copy — the basis of storing expected valuescopy(rhs) into this object; clone() returns a fresh deep copy — the basis of storing expected valuesComparecompare(rhs) — field-by-field value equality; the basis of scoreboard checkingcompare(rhs) — field-by-field value equality; the basis of scoreboard checkingPrint / sprintformatted, recursive output of all fields — debugging and loggingformatted, recursive output of all fields — debugging and loggingPack / unpackserialise to/from a bitstream — bus models, reference models, file I/Oserialise to/from a bitstream — bus models, reference models, file I/O
Figure 1 — the standard operations every uvm_object provides. Identity (get_name, get_type_name). Copy/clone (copy into an existing object, or clone to a fresh one). Compare (value equality, field by field). Print/sprint (formatted output). Pack/unpack (serialise to and from a bitstream). All are available on any uvm_object, which is what lets generic framework code manipulate data of any type. Each is implemented via field automation or a do_* hook.

These five groups are the contract of being a uvm_object. Identity names the object and registers its type with the factory. Copy/clone duplicates it — copy into an existing object, clone returning a new one — which is how a scoreboard stores an expected transaction without aliasing the original. Compare answers value-equality field by field, the foundation of all scoreboard checking. Print renders it for logs and debug. Pack/unpack serialises it to and from bits, used by bus functional models, reference models, and file I/O. Crucially, all of these are generic — framework code calls compare() or pack() on a uvm_object handle without knowing the concrete type — which is exactly why the operations must be implemented completely for each data class you write.

RTL / Simulation Perspective — two ways to provide the toolkit

You implement the toolkit either by listing your fields for automation (UVM generates the methods) or by writing the do_* hooks yourself. Both are shown below; both must cover every field.

uvm_object — field automation vs explicit do_* hooks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// OPTION A — FIELD AUTOMATION: list the fields; UVM generates copy/compare/print/pack.
class my_data extends uvm_object;
  rand bit [31:0] addr, data;
  `uvm_object_utils_begin(my_data)
    `uvm_field_int(addr, UVM_ALL_ON)      // included in copy/compare/print/pack
    `uvm_field_int(data, UVM_ALL_ON)
  `uvm_object_utils_end
  function new(string name = "my_data"); super.new(name); endfunction
endclass
 
// OPTION B — EXPLICIT do_* HOOKS: full control, no macro overhead. copy() calls do_copy(), etc.
class my_data2 extends uvm_object;
  rand bit [31:0] addr, data;
  `uvm_object_utils(my_data2)             // factory + get_type_name, but NO field automation
  function new(string name = "my_data2"); super.new(name); endfunction
 
  function void do_copy(uvm_object rhs);                 // invoked by copy()/clone()
    my_data2 r;  super.do_copy(rhs);                     // copy base-class fields too
    if (!$cast(r, rhs)) return;
    addr = r.addr;  data = r.data;                        // every field, by hand
  endfunction
 
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);  // invoked by compare()
    my_data2 r;
    if (!$cast(r, rhs)) return 0;
    return super.do_compare(rhs, comparer) && (addr == r.addr) && (data == r.data);
  endfunction
endclass

The two options trade convenience for control, but share one rule. Field automation (Option A) is the common path: list each field with a uvm_field_* macro and UVM generates copy, compare, print, and pack for you — concise, but with some run-time overhead and limited flexibility. Explicit do_* hooks (Option B) give full control and avoid the macro overhead: you write do_copy, do_compare, etc., and the public methods (copy(), compare()) call them. Note two essentials in the hooks — the $cast to the concrete type (the argument is a base uvm_object handle), and the super.do_copy/super.do_compare call so base-class fields are handled. The shared rule, whichever path: every field must appear, or the generated/written method silently skips it.

Verification Perspective — the template-method pattern (copy calls do_copy)

A subtle but important design point: you never override the public copy(), compare(), or pack() — you override the do_* hooks they call. The public method does the framework bookkeeping and delegates the field work to your hook. Understanding this delegation is what makes the methods predictable.

copy() calls do_copy(), compare() calls do_compare(): public methods delegate field work to do_ hooksPublic method → do_ hook delegationPublic method → do_ hook delegation1Code calls the public methodscoreboard calls obs.compare(exp); a queue calls txn.clone() (whichcalls copy()).2uvm_object does bookkeepingthe public method (compare/copy/pack) handles policy, recursionsetup, naming — framework concerns.3It calls the do_ hookcompare→do_compare, copy→do_copy, pack→do_pack: where the per-fieldwork happens.4Field work runsauto-generated (field macros) or hand-written (do_ override): everyfield is compared/copied/packed.
Figure 2 — the template-method pattern in uvm_object. The public method (copy, compare, print, pack) is provided by uvm_object and handles framework bookkeeping; it then calls the corresponding do_ hook (do_copy, do_compare, …) where the per-field work happens. With field automation, the macros generate the do_ hooks for you; without it, you write them. You override the do_ hook, never the public method — so the framework's bookkeeping always runs, and your field logic plugs in underneath.

This delegation is why the rule is "override do_copy, not copy." The public copy() (and compare(), pack(), …) is part of uvm_object and runs the framework's bookkeeping — comparison policy, recursion control, naming, packer state — that you should not reimplement. It then calls your do_copy() (or the macro-generated one) for the actual fields. So your job is always the hook, and the framework's bookkeeping always runs around it. This also explains why field automation and do_* hooks are interchangeable: both ultimately supply the do_ hook the public method calls — the macros generate it, or you write it. Whichever you use, the public method's contract is fixed, and your field logic plugs in at exactly one place.

Runtime / Execution Flow — clone is create-plus-copy

clone() — the operation a scoreboard uses to store an expected transaction safely — is built from two primitives: it creates a fresh object of the same type (via the factory) and then copys this object's fields into it. Understanding that composition explains both how clone works and how it fails.

clone is create plus copy: factory creates a fresh typed object, then do_copy copies fields into it, producing an independent duplicate1. create(factory)fresh typed object2. copyfieldsvia do_copyindependentduplicateclone()the public callcreate()factory: fresh, registeredtypecopy()/do_copy()copy this object's fieldsnew objectbeing filledIndependent duplicate(if copy is complete + deep)12
Figure 3 — clone() = create() + copy(). clone first creates a fresh object of the same type via the factory (so the type is registered with uvm_object_utils), then copies this object's fields into the new one (via do_copy). The result is an independent duplicate — used to store expected transactions without aliasing the original. If do_copy misses a field, the clone is incomplete; if do_copy shallow-copies a nested object, the clone shares that sub-object with the original.

This composition is why clone depends on two things being right. The create half needs the type registered with the factory (uvm_object_utils), or there's nothing to instantiate — clone of an unregistered type fails. The copy half needs do_copy (or field automation) to handle every field, and to handle nested objects deeply — or the "independent duplicate" isn't independent. A scoreboard storing obs.clone() as its expected value relies on this being a true, complete, deep copy: if do_copy skips a field, the stored expectation is wrong; if it shallow-copies a nested header object, the stored expectation shares that header with the live transaction and changes when the original does. Clone is only as good as create-plus-copy, and copy is only as good as its field coverage and depth — which is exactly the DebugLab below.

Waveform Perspective — pack serialises an object to a bitstream

Most uvm_object operations are pure data manipulation, but pack has a tangible output: a bitstream. Packing lays the object's fields out as an ordered sequence of bytes — used to drive a bus, feed a reference model, or write a file — and unpack reverses it.

pack() serialises an object's fields into an ordered bitstream

8 cycles
pack() serialises an object's fields into an ordered bitstreampack(): the addr byte (0x10) — the object serialised field by fieldpack(): the addr byte …the data byte (0xAB), then write (01) and len (04)the data byte (0xAB), …the whole object as a bitstream — for a bus, a reference model, or a filethe whole object as a …pospack_enbyte--10AB0104------t0t1t2t3t4t5t6t7
Figure 4 — packing a transaction {addr=0x10, data=0xAB, write=1, len=4}. pack() lays the fields out in order as a byte stream (the position axis is the byte index, not time): the addr byte, the data byte, then write and len. The order and width come from the field declarations (or do_pack); unpack() reads the same stream back into an object's fields. This serialisation is how a transaction crosses to a bus model, a reference model, or a file — the object's data flattened to bits and reconstructed.

The byte stream is the object flattened. pack() walks the fields in their declared order and emits their bits — addr, then data, then write and len — producing a deterministic bitstream, and unpack() reads that stream back into a fresh object's fields. This is how a uvm_object crosses a boundary that only carries bits: a bus functional model packs a transaction to drive it, a reference model unpacks a stream into a transaction to process it, a log writes packed bytes to a file. Like the other operations, pack is field-driven — so a field omitted from automation (or do_pack) is simply absent from the stream, and unpack on the other side will mis-align every field after it. Completeness of the field list governs serialisation just as it governs copy and compare.

DebugLab — the clone that shared a sub-object

Two 'independent' transactions that corrupted each other — a shallow copy of a nested object

Symptom

A scoreboard stored a clone of each observed transaction as its expected value, then later compared incoming transactions against the stored clones. It worked for flat transactions, but for transactions carrying a nested header object, the checks went haywire: storing a clone, then continuing to drive new traffic, changed the stored clone's header. The "independent" expected copies were mutating after they were stored, as if they shared state with the live transactions.

Root cause

A shallow copy of the nested object. The transaction's do_copy (or its field-automation flag) copied the handle to the header sub-object rather than cloning the header itself — so the clone and the original pointed at the same header object, and mutating one mutated both:

why the clone shared its sub-object
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
txn fields:   addr, data, header (a uvm_object sub-object — a HANDLE)
do_copy did:  this.header = rhs.header;     // copied the HANDLE → shared object (shallow)
clone result: clone.header === original.header   // SAME object, not a copy
consequence:  driver reuses & mutates original.header → clone.header changes too
              → stored 'expected' header is corrupted by later traffic
fix (deep):   this.header = my_header'(rhs.header.clone());   // clone the sub-object
              // or field-automation: register header so it is deep-copied, not shared

The flat fields (addr, data) were value types, so they copied by value and were fine. The nested object field was a handle, and copying a handle shares the referent — a shallow copy. A true clone must copy nested objects deeply (clone them), or every "copy" aliases the same sub-object.

Diagnosis

The tell is "copies" that change together — shared state where you expected independence. Diagnose copy-depth bugs by inspecting nested handles:

  1. Check identity of nested objects after copy. After b = a.clone(), confirm b.header !== a.header (different objects). If they're the same handle, the copy was shallow and they will corrupt each other.
  2. Audit do_copy / field flags for object fields. Value fields copy by value safely; object (handle) fields must be cloned in do_copy, or registered so field automation deep-copies them. A bare this.obj = rhs.obj is a shallow copy.
  3. Look for "stored value changed later." A stored expected transaction whose fields mutate after storage is the signature of aliasing — almost always a shallow-copied sub-object (or a stored handle instead of a clone).
Prevention

Copy nested objects deeply, and decide copy depth deliberately:

  1. Deep-copy object fields in do_copy. For any field that is itself a uvm_object, clone it (this.obj = type'(rhs.obj.clone())) rather than copying the handle — so the copy owns its own sub-objects. With field automation, register sub-objects so they are deep-copied.
  2. Verify clones are independent. After cloning, assert the nested objects are distinct handles; make "stored expected is truly independent" part of scoreboard bring-up.
  3. Remember value vs reference. Scalar/array fields copy by value (safe); object-handle fields copy the reference unless you clone — the deep-vs-shallow decision applies precisely to nested objects.

The one-sentence lesson: copying an object-handle field shares the referent — a shallow copy — so a clone with a nested object must deep-copy (clone) that sub-object, or two "independent" copies will secretly share it and corrupt each other.

Common Mistakes

  • A field missing from the toolkit. A field not listed in field automation (or not handled in do_copy/do_compare/do_pack) is silently skipped — clones come out incomplete, comparisons miss mismatches, packed streams mis-align. Every field must appear.
  • Shallow-copying a nested object. Copying an object-handle field copies the reference, so the copy shares the sub-object — aliasing that corrupts "independent" copies. Deep-copy (clone) nested objects in do_copy.
  • Overriding the public method instead of the hook. Override do_copy/do_compare/do_pack, not copy/compare/pack — the public methods do framework bookkeeping you must not bypass. The hook is where your field logic belongs.
  • Forgetting super.do_copy / super.do_compare. Omitting the super call in a do_* override skips base-class field handling, so inherited fields aren't copied or compared. Always call super in the hook.
  • Comparing handles with == instead of compare(). a == b tests object identity, not value equality; scoreboards must use compare() (which runs do_compare over the fields).
  • Storing a handle instead of a clone. Keeping a transaction by handle (not clone()) captures a reference the producer may mutate — the aliasing bug from Module 2.6, at the data level. Store clones.

Senior Design Review Notes

Interview Insights

uvm_object is the base class for all data in UVM — every transaction, configuration object, and sequence ultimately extends it. Unlike a uvm_component, it has no place in the component hierarchy and no phases; it is transient data that is created, used, and discarded. What it provides is a standard set of operations so the framework can manipulate any data generically: copy and clone (duplicate the object), compare (field-by-field value equality), print/sprint (formatted output), pack/unpack (serialise to and from a bitstream), plus identity methods (get_name, get_type_name) and factory registration via the uvm_object_utils macro. These operations are what let generic infrastructure — a scoreboard's compare, an analysis FIFO's storage, a printer, a packer — work on any transaction type without knowing its specific fields. You implement the operations either through field automation (the uvm_field_* macros, which auto-generate them) or by writing the do_* hooks (do_copy, do_compare, etc.) yourself. The essential discipline is that the operations must account for every field, and nested object fields must be copied at the right depth.

Exercises

  1. List the toolkit. Name the five operation groups uvm_object provides and one place in a testbench each is used (e.g., compare → scoreboard). State which macro registers the type with the factory.
  2. Automation vs hooks. Write (in prose or code) the same two-field data class twice — once with field automation, once with explicit do_copy/do_compare — and state one situation where you'd prefer each.
  3. Spot the shallow copy. Given a transaction with a nested config object copied via this.cfg = rhs.cfg, explain the bug, the symptom it produces in a scoreboard, and the one-line fix.
  4. Clone = create + copy. Explain why a clone() of a type not registered with uvm_object_utils fails, and separately why a clone with an incomplete do_copy produces a subtly wrong duplicate — connecting each failure to one half of create-plus-copy.

Summary

  • uvm_object is the base class for all UVM data — transactions, configs, sequences — providing a standard toolkit (copy, clone, compare, print/sprint, pack/unpack, identity) so the framework can manipulate any data type generically. It has no phases and no place in the tree; it is pure data.
  • You implement the toolkit two ways: field automation (uvm_field_* macros — convenient, some overhead) or explicit do_* hooks (do_copy, do_compare, … — full control, no overhead). Both must cover every field, and do_* overrides must $cast the argument and call super.
  • The public methods (copy/compare/pack) carry framework bookkeeping and delegate to the do_ hooks (template-method pattern) — so you override the hook, never the public method. clone() = create() (factory) + copy().
  • The two silent traps: a missing field (copy/compare/pack skips it — incomplete clones, missed mismatches) and a shallow copy of a nested object (copying a handle shares the sub-object, so "independent" copies corrupt each other). Deep-copy nested objects.
  • The durable rule of thumb: every UVM data class is a uvm_object — give it a complete toolkit covering every field, override the do_* hooks (with super and $cast), and deep-copy nested objects, because a skipped field or a shared sub-object is a silent, expensive bug.

Next — uvm_component: uvm_object is the data half of the class library. The next chapter covers its counterpart — uvm_component, the base class for the persistent structure: what it adds over uvm_object (a place in the tree, phases, configuration), and why everything structural extends it.