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 versusdo_*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'sprint, a packer'spack— all calluvm_objectmethods, 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 withclone(). 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; explicitdo_*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 theuvm_objectmethods.
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_objectis 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 auvm_objectrather than a plain struct is that toolkit: any code cancopy,compare,pack, orcloneit 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 (thedo_*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.
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.
// 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
endclassThe 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.
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.
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 cyclesThe 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
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.
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:
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 sharedThe 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.
The tell is "copies" that change together — shared state where you expected independence. Diagnose copy-depth bugs by inspecting nested handles:
- Check identity of nested objects after copy. After
b = a.clone(), confirmb.header !== a.header(different objects). If they're the same handle, the copy was shallow and they will corrupt each other. - 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 barethis.obj = rhs.objis a shallow copy. - 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).
Copy nested objects deeply, and decide copy depth deliberately:
- 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. - Verify clones are independent. After cloning, assert the nested objects are distinct handles; make "stored expected is truly independent" part of scoreboard bring-up.
- 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, notcopy/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 thesupercall in ado_*override skips base-class field handling, so inherited fields aren't copied or compared. Always callsuperin the hook. - Comparing handles with
==instead ofcompare().a == btests object identity, not value equality; scoreboards must usecompare()(which runsdo_compareover 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
- List the toolkit. Name the five operation groups
uvm_objectprovides and one place in a testbench each is used (e.g., compare → scoreboard). State which macro registers the type with the factory. - 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. - Spot the shallow copy. Given a transaction with a nested
configobject copied viathis.cfg = rhs.cfg, explain the bug, the symptom it produces in a scoreboard, and the one-line fix. - Clone = create + copy. Explain why a
clone()of a type not registered withuvm_object_utilsfails, and separately why a clone with an incompletedo_copyproduces a subtly wrong duplicate — connecting each failure to one half of create-plus-copy.
Summary
uvm_objectis 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 explicitdo_*hooks (do_copy,do_compare, … — full control, no overhead). Both must cover every field, anddo_*overrides must$castthe argument and callsuper. - The public methods (
copy/compare/pack) carry framework bookkeeping and delegate to thedo_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 thedo_*hooks (withsuperand$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.