Skip to content

do_copy, do_compare, do_print, do_pack

Manual field automation — when to use over macros, custom comparison, production patterns.

UVM Fundamentals · Module 17

§1 — Why You Eventually Stop Using Field Macros

Here's a scenario that plays out on almost every serious verification project. You're three months into a 64-bit AXI4 scoreboard. Field macros are in place. Everything compiles clean. Then someone asks: "Why is the scoreboard flagging mismatches on the BRESP field even when we told it to ignore error responses in burst mode?" And you realize — the macro doesn't know about burst mode. It just compares every field, every time, with no context.

That's the moment engineers discover the do_*() methods. Not from reading documentation, but from running face-first into a wall that field macros cannot climb. These five override methods — do_copy(), do_compare(), do_print(), do_pack(), and do_unpack() — are the manual implementations of the exact same operations that field macros automate. You implement them when you need control that macros cannot give you.

Before diving into each method, understand the contract they form: when you call txn.compare(other) on a uvm_object, the framework calls do_compare() on your class. Same for copy, print, pack, and unpack. The public API methods are thin wrappers. The do_*() methods are where the actual work happens. Override them, and you control everything.

§2 — The Five Methods and What They Own

MethodCalled When You…You Control…Use When…
do_copy(rhs)Call dst.copy(src)Which fields are copied and how — shallow vs deep for nested objectsNested objects need ownership transfer; some fields must not be copied (e.g., timestamps)
do_compare(rhs, cmp)Call a.compare(b)Which fields are compared, under what conditions, with what masksProtocol-mode-dependent comparison; masked-field ignore; custom tolerance ranges
do_print(printer)Call txn.print()Which fields appear, in what format, at what nesting levelHiding sensitive fields; custom field grouping; proprietary display formats
do_pack(packer)Call txn.pack() or pack_bytes()Which fields enter the bitstream and in what orderProtocol-specific wire encoding; endianness control; conditional field packing
do_unpack(packer)Call txn.unpack() or unpack_bytes()How a bitstream is decoded back into fieldsStateful unpacking; variable-length fields; protocol header parsing

§3 — Syntax Deep Dive — Signatures and Parameters

do_copy()

function void do_copy(uvm_object rhs); rhs — the source object (cast to your type). Return — void. Copy fields from rhs into this. Always call super.do_copy(rhs) first and cast rhs before accessing its fields.

do_compare()

function bit do_compare(uvm_object rhs, uvm_comparer comparer); rhs — object to compare against. comparer — UVM comparison engine (controls verbosity, miscompare printing). Return — 1 if equal, 0 if mismatch.

do_print()

function void do_print(uvm_printer printer); printer — the UVM printer object (table or tree format). Use printer.print_field_int(), print_string(), print_object() etc. Call super.do_print(printer) first.

do_pack() / do_unpack()

function void do_pack(uvm_packer packer);function void do_unpack(uvm_packer packer); packer — the UVM packer (serializer). Use packer.pack_field_int() / unpack_field_int(). Fields pack in the order you call them — this defines the wire format.

SystemVerilog — canonical skeleton: all five methods in one class
class my_txn extends uvm_sequence_item;
`uvm_object_utils(my_txn)
 
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit         write;
bit     [1:0]  resp;
 
function new(string name = "my_txn"); super.new(name); endfunction
 
// ── do_copy: deep copy from source object ─────────────────────────
function void do_copy(uvm_object rhs);
my_txn rhs_;
super.do_copy(rhs);             // ALWAYS first
if (!$cast(rhs_, rhs)) begin
`uvm_fatal("DO_COPY", "Cast failed — rhs is not my_txn")
end
this.addr  = rhs_.addr;
this.data  = rhs_.data;
this.write = rhs_.write;
this.resp  = rhs_.resp;
endfunction
 
// ── do_compare: field-by-field comparison ─────────────────────────
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
my_txn rhs_;
if (!$cast(rhs_, rhs)) return 0;
return super.do_compare(rhs, comparer) &&
comparer.compare_field("addr",  addr,  rhs_.addr,  32) &&
comparer.compare_field("data",  data,  rhs_.data,  32) &&
comparer.compare_field("write", write, rhs_.write,  1);
// resp intentionally excluded — DUT response, not stimulus
endfunction
 
// ── do_print: formatted output ────────────────────────────────────
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,  32, UVM_HEX);
printer.print_field_int("write", write,  1, UVM_BIN);
printer.print_field_int("resp",  resp,   2, UVM_BIN);
endfunction
 
// ── do_pack: serialize to bitstream ───────────────────────────────
function void do_pack(uvm_packer packer);
super.do_pack(packer);
packer.pack_field_int(addr,  32);
packer.pack_field_int(data,  32);
packer.pack_field_int(write,  1);
// resp NOT packed — it's a response field, not a stimulus field
endfunction
 
// ── do_unpack: deserialize from bitstream ─────────────────────────
function void do_unpack(uvm_packer packer);
super.do_unpack(packer);
addr  = packer.unpack_field_int(32);
data  = packer.unpack_field_int(32);
write = packer.unpack_field_int( 1);
endfunction
 
endclass

§4 — Step-by-Step Execution — Seeing Inside Each Method

do_compare() — What Happens When compare() Is Called

StepWhat HappensNotes
1a.compare(b) called in scoreboardPublic API — thin wrapper
2UVM framework allocates a uvm_comparer if none providedComparer tracks mismatches and verbosity
3a.do_compare(b, comparer) is called on your classYour override runs here
4super.do_compare(b, comparer) validates type compatibilityReturns 0 immediately if types are incompatible
5Each comparer.compare_field() call checks one fieldOn mismatch: logs a message with field name and values
6Return value: AND of all field resultsFirst mismatch short-circuits in most implementations
7Caller receives 1 (match) or 0 (mismatch)comparer.result also holds the count of mismatches

do_copy() — Data Flow from Source to Destination

FieldSource (rhs_)Destination (this)Copy Type
addr32'hA000_000432'hA000_0004Value copy — integral, always safe
data32'h1234_567832'h1234_5678Value copy — integral, always safe
sub_objHandle (pointer)Handle ONLY without $cast(this.sub_obj, rhs_.sub_obj.clone())⚠️ SHALLOW — both point to same object
sub_obj (correct)Handle (pointer)New deep copy via clone()✓ DEEP — independent copy
timestampNot copied (excluded by design)Keeps original valueIntentional omission — valid pattern

do_pack() — Bit Ordering and Wire Format

Pack OrderFieldWidthPacked Bit PositionHex in Stream
1staddr = 32'hA000_000032 bits[63:32]A0 00 00 00
2nddata = 32'h1234_567832 bits[31:0]12 34 56 78
3rdwrite = 1'b11 bit[64] (next byte boundary)80 (MSB)
Total packed size: 65 bits → 9 bytes. Order in do_pack() DEFINES the wire protocol. Change it and pack/unpack break.

§5 — Code Examples — From Simple to Production Grade

Example 1 — Custom do_compare() With Ignore Mask

This is the scenario that kills field macros. Your scoreboard compares APB transactions but the DUT is allowed to return any PSLVERR value during burst mode — you only care about the data and address matching.

SystemVerilog — conditional do_compare with burst-mode mask
class apb_txn extends uvm_sequence_item;
`uvm_object_utils(apb_txn)
 
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit [3:0]  strobe;
bit     [1:0]  pslverr;   // DUT response — not always meaningful
bit              burst_mode;  // when set, pslverr is don't-care
bit     [31:0]  addr_mask = 32'hFFFF_FFFF; // configurable address mask
 
function new(string name="apb_txn"); super.new(name); endfunction
 
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
apb_txn rhs_;
bit result = 1;
 
if (!$cast(rhs_, rhs)) return 0;
 
result &= super.do_compare(rhs, comparer);
 
// Compare only masked address bits
result &= comparer.compare_field("addr",
addr  & addr_mask,
rhs_.addr & rhs_.addr_mask, 32);
 
// Data must always match
result &= comparer.compare_field("data", data, rhs_.data, 32);
 
// Strobe must match
result &= comparer.compare_field("strobe", strobe, rhs_.strobe, 4);
 
// pslverr: only compare when NOT in burst mode
// In burst mode, DUT may return any error code — don't flag it
if (!burst_mode && !rhs_.burst_mode) begin
result &= comparer.compare_field("pslverr", pslverr, rhs_.pslverr, 2);
end
 
return result;
endfunction
endclass

Example 2 — Deep Copy of Nested Objects in do_copy()

The shallow-copy trap catches everyone at least once. Here's the pattern that actually works when your transaction contains sub-objects that the scoreboard needs to store independently.

SystemVerilog — deep copy with nested uvm_object sub-objects
class axi_header extends uvm_object;
`uvm_object_utils(axi_header)
rand bit [7:0] id;
rand bit [3:0] len;
function new(string n="axi_header"); super.new(n); endfunction
 
function void do_copy(uvm_object rhs);
axi_header r; $cast(r, rhs);
super.do_copy(rhs);
id = r.id; len = r.len;
endfunction
endclass
 
class axi_txn extends uvm_sequence_item;
`uvm_object_utils(axi_txn)
 
axi_header       hdr;         // nested sub-object
rand bit[63:0]   addr;
rand bit[7:0]    data[$];     // payload
time             created_at;  // never copy — always fresh
 
function new(string n="axi_txn");
super.new(n);
hdr = axi_header::type_id::create("hdr");
created_at = $time;
endfunction
 
function void do_copy(uvm_object rhs);
axi_txn rhs_;
super.do_copy(rhs);
if (!$cast(rhs_, rhs)) return;
 
// DEEP copy of nested header — hdr gets its own clone
// Without this: this.hdr and rhs_.hdr POINT TO THE SAME OBJECT
$cast(hdr, rhs_.hdr.clone());
 
addr = rhs_.addr;
 
// Deep copy of the dynamic array
data = rhs_.data;   // SV dynamic arrays copy by value ✓
 
// created_at intentionally NOT copied
// Each copy represents a new observation at a new time
created_at = $time;
endfunction
 
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
axi_txn rhs_;
if (!$cast(rhs_, rhs)) return 0;
return super.do_compare(rhs, comparer) &&
// Recursively compare nested object
comparer.compare_object("hdr", hdr, rhs_.hdr) &&
comparer.compare_field("addr", addr, rhs_.addr, 64) &&
// Queue comparison — iterate and compare each element
(data.size() == rhs_.data.size()) &&
(data === rhs_.data);   // === handles X/Z correctly
endfunction
endclass

Example 3 — Protocol-Specific do_pack() and do_unpack()

SystemVerilog — conditional pack/unpack based on protocol mode
class pcie_tlp extends uvm_sequence_item;
`uvm_object_utils(pcie_tlp)
 
rand bit [2:0]  fmt;       // TLP format: 3DW/4DW, with/without data
rand bit [4:0]  tlp_type;
rand bit [9:0]  length;    // in DWORDs
rand bit [31:0] addr32;    // used when fmt[0]==0 (3DW)
rand bit [63:0] addr64;    // used when fmt[0]==1 (4DW)
rand bit [31:0] payload[$];
 
function new(string n="pcie_tlp"); super.new(n); endfunction
 
function void do_pack(uvm_packer packer);
super.do_pack(packer);
packer.pack_field_int(fmt,      3);
packer.pack_field_int(tlp_type, 5);
packer.pack_field_int(length,  10);
 
// Protocol determines address width: 3DW vs 4DW header
if (fmt[0] == 0)
packer.pack_field_int(addr32, 32);   // 3DW header
else
packer.pack_field_int(addr64, 64);   // 4DW header
 
// Payload only if format indicates data TLP
if (fmt[1]) begin
foreach (payload[i])
packer.pack_field_int(payload[i], 32);
end
endfunction
 
function void do_unpack(uvm_packer packer);
super.do_unpack(packer);
fmt      = packer.unpack_field_int( 3);
tlp_type = packer.unpack_field_int( 5);
length   = packer.unpack_field_int(10);
 
if (fmt[0] == 0)
addr32 = packer.unpack_field_int(32);
else
addr64 = packer.unpack_field_int(64);
 
if (fmt[1]) begin
payload = {};
repeat(length)
payload.push_back(packer.unpack_field_int(32));
end
endfunction
endclass

§6 — Simulation Thinking — What the Tool Actually Does

The Call Chain for compare()

SystemVerilog — how compare() resolves through the UVM framework
// What actually happens when you write: result = a.compare(b)
 
// Step 1: uvm_object::compare() is called (from uvm_object base class)
// It creates a default comparer if you didn't supply one:
//   comparer = uvm_default_comparer (singleton)
// Then it calls: do_compare(b, comparer)
 
// Step 2: Your do_compare() runs:
//   super.do_compare(b, comparer)   ← type check (returns 0 if wrong type)
//   comparer.compare_field("addr", addr, rhs_.addr, 32)
//     → if mismatch: prints "Miscompare for object..." to transcript
//     → returns 0 and increments comparer.result
 
// Step 3: The short-circuit behavior —
//   In UVM 1.2: ALL fields are compared, mismatches accumulated
//   In some implementations: stops at first mismatch
//   Behavior depends on comparer.show_max_mismatches setting
 
// ── Controlling comparer verbosity ────────────────────────────────────
uvm_comparer cmp = new();
cmp.verbosity = UVM_HIGH;          // print all field mismatches
cmp.show_max_mismatches = 5;        // stop reporting after 5 mismatches
cmp.sev = UVM_ERROR;               // mismatches trigger uvm_error (CI fail)
 
if (!a.compare(b, cmp)) begin
`uvm_error("SCB", $sformatf(
"Transaction mismatch! %0d field(s) differ",
cmp.result))
end
 
// ── compare_field vs compare_field_int — know the difference ─────────
// compare_field(name, lhs, rhs, width_bits)  — works for any integral type
// compare_field_int(name, lhs, rhs, width)   — optimized for native int width
// compare_string(name, lhs_str, rhs_str)     — string comparison
// compare_object(name, lhs_obj, rhs_obj)     — recursive object comparison

§7 — Real Verification Usage — Where These Methods Actually Live

ComponentMethod UsedWhyReal Example
Scoreboarddo_compare()Check expected vs actual with protocol-aware maskingAXI scoreboard ignores RRESP on read errors when RD_RSP_OK is set
Monitordo_pack()Capture wire-level encoding for protocol analysisPack observed bus signals into TLM transaction for coverage
Driverdo_unpack()Decode received response back to transaction fieldsUSB driver unpacks device response into status fields
Coveragedo_print()Generate debug-friendly labels for coverage binsCustom print shows burst type + length + address range together
Reference Modeldo_copy()Deep copy golden transactions before modificationGolden queue stores independent copies — no aliasing bugs
DMA Verificationdo_pack() + do_unpack()Serialize descriptor data to memory, deserialize backDMA descriptor encoding for memory-mapped verification
SystemVerilog — scoreboard using do_compare with custom comparer settings
class axi_scoreboard extends uvm_scoreboard;
`uvm_component_utils(axi_scoreboard)
 
axi_txn           expected_q[$];
uvm_comparer      m_comparer;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Configure comparer once — reuse across all comparisons
m_comparer = new();
m_comparer.verbosity         = UVM_LOW;    // only print first mismatch
m_comparer.show_max_mismatches = 1;
m_comparer.sev               = UVM_ERROR;  // mismatches → CI failure
endfunction
 
function void write_from_monitor(axi_txn actual);
axi_txn expected;
if (expected_q.size() == 0) begin
`uvm_error("SCB", "Received transaction but expected queue is empty")
return;
end
expected = expected_q.pop_front();
 
// compare() calls our do_compare() under the hood
if (!expected.compare(actual, m_comparer))
`uvm_error("SCB", $sformatf(
"Mismatch!\nExpected: %s\nActual: %s",
expected.sprint(), actual.sprint()))
else
`uvm_info("SCB", "Transaction matched ✓", UVM_HIGH)
endfunction
endclass

§8 — Common Bugs and Debugging Scenarios

Bug 1 — Forgetting super.do_copy() — Silent Partial Copy

Symptom: Copy appears to work but after factory overrides, copies from parent class fields are missing. Scoreboard reports spurious mismatches on fields you thought were being copied.

Root Cause: super.do_copy() copies any fields registered via field macros in parent classes. Skip it and those fields are never copied into this.

Fix: Always super.do_copy(rhs) before touching this.

SystemVerilog — Bug 1 and Bug 2 with fixes
// ── Bug 1: Missing super.do_copy() ───────────────────────────────────
// ❌ WRONG
function void do_copy(uvm_object rhs);
my_txn r; $cast(r, rhs);
addr = r.addr;   // parent fields from field macros: NEVER COPIED
endfunction
 
// ✓ CORRECT
function void do_copy(uvm_object rhs);
my_txn r;
super.do_copy(rhs);   // ← ALWAYS FIRST
if (!$cast(r, rhs)) return;
addr = r.addr;
endfunction
 
// ── Bug 2: Shallow copy of nested object ──────────────────────────────
// ❌ WRONG — both source and destination point to SAME header object
function void do_copy(uvm_object rhs);
axi_txn r; super.do_copy(rhs); $cast(r, rhs);
hdr  = r.hdr;    // SHALLOW: this.hdr IS r.hdr — same memory location
addr = r.addr;
endfunction
// Later when r.hdr.id is changed, this.hdr.id changes too — silent corruption
 
// ✓ CORRECT — independent deep copy
function void do_copy(uvm_object rhs);
axi_txn r; super.do_copy(rhs); $cast(r, rhs);
$cast(hdr, r.hdr.clone());  // DEEP: independent copy via clone()
addr = r.addr;
endfunction
 
// ── Bug 3: Using == instead of === for X-aware compare ────────────────
// ❌ WRONG — X propagates, 4-state 'X' data causes false mismatch
function bit do_compare(uvm_object rhs, uvm_comparer cmp);
my_txn r; $cast(r, rhs);
return super.do_compare(rhs, cmp) &&
(data == r.data);   // if data contains X: X==X is X, not 1
endfunction
 
// ✓ CORRECT — use compare_field which handles X properly
function bit do_compare(uvm_object rhs, uvm_comparer cmp);
my_txn r; $cast(r, rhs);
return super.do_compare(rhs, cmp) &&
cmp.compare_field("data", data, r.data, $bits(data));
endfunction
 
// ── Bug 4: Pack/unpack order mismatch ────────────────────────────────
// ❌ WRONG — pack order and unpack order must be IDENTICAL
function void do_pack(uvm_packer pk);
super.do_pack(pk);
pk.pack_field_int(addr, 32);  // packed first
pk.pack_field_int(data, 32);
endfunction
 
function void do_unpack(uvm_packer pk);
super.do_unpack(pk);
data = pk.unpack_field_int(32);  // ❌ unpacked first — addr goes into data!
addr = pk.unpack_field_int(32);
endfunction
// Result: addr and data are swapped — hard to catch without a known-pattern test

§9 — Ready-to-Run Demo

Ready to Run — Questa / VCS / Xcelium

SystemVerilog — do_methods_demo.sv (copy and run)
// do_methods_demo.sv — all five methods demonstrated
// Questa : vlog -sv do_methods_demo.sv && vsim -c do_methods_top -do "run -all; quit"
// VCS    : vcs -sverilog -ntb_opts uvm do_methods_demo.sv && ./simv
// Xcelium: xrun -sv -uvm do_methods_demo.sv -input "run; exit"
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── Transaction with all five do_* methods ────────────────────────────
class demo_txn extends uvm_sequence_item;
`uvm_object_utils(demo_txn)
 
rand bit[31:0] addr;
rand bit[31:0] data;
rand bit       write;
bit[1:0]      resp;      // excluded from compare and pack
string          tag;      // debug only — excluded from compare
 
function new(string n="demo_txn"); super.new(n); endfunction
 
function void do_copy(uvm_object rhs);
demo_txn r;
super.do_copy(rhs);
if(!$cast(r,rhs)) return;
addr=r.addr; data=r.data; write=r.write; resp=r.resp; tag=r.tag;
endfunction
 
function bit do_compare(uvm_object rhs, uvm_comparer c);
demo_txn r;
if(!$cast(r,rhs)) return 0;
return super.do_compare(rhs,c) &&
c.compare_field("addr",  addr,  r.addr,  32) &&
c.compare_field("data",  data,  r.data,  32) &&
c.compare_field("write", write, r.write,  1);
// resp and tag excluded — they are metadata not stimulus
endfunction
 
function void do_print(uvm_printer p);
super.do_print(p);
p.print_field_int("addr",  addr,  32, UVM_HEX);
p.print_field_int("data",  data,  32, UVM_HEX);
p.print_field_int("write", write,  1, UVM_BIN);
p.print_field_int("resp",  resp,   2, UVM_BIN);
p.print_string("tag", tag);
endfunction
 
function void do_pack(uvm_packer pk);
super.do_pack(pk);
pk.pack_field_int(addr,  32);
pk.pack_field_int(data,  32);
pk.pack_field_int(write,  1);
endfunction
 
function void do_unpack(uvm_packer pk);
super.do_unpack(pk);
addr  = pk.unpack_field_int(32);
data  = pk.unpack_field_int(32);
write = pk.unpack_field_int( 1);
endfunction
endclass
 
// ── Test exercising all five methods ─────────────────────────────────
class do_methods_test extends uvm_test;
`uvm_component_utils(do_methods_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
 
task run_phase(uvm_phase phase);
demo_txn orig, copy_a, copy_b;
bit[7:0] packed[$];
phase.raise_objection(this);
 
// Create and randomize original
orig = demo_txn::type_id::create("orig");
void'(orig.randomize());
orig.resp = 2'b01;
orig.tag  = "WRITE_BURST_TEST";
 
`uvm_info("TEST","=== do_print() output ===",UVM_NONE) orig.print();
 
// Test do_copy via clone()
$cast(copy_a, orig.clone());
copy_a.set_name("copy_a");
`uvm_info("TEST",$sformatf("do_compare after clone: %0d (expect 1)",
orig.compare(copy_a)),UVM_NONE)
 
// Modify copy — compare should fail on data, NOT on resp/tag
copy_a.data = 32'hFFFF_0000;
copy_a.resp = 2'b11;   // excluded from compare — should not affect result
copy_a.tag  = "DIFFERENT_TAG";  // also excluded
`uvm_info("TEST",$sformatf("do_compare after data change: %0d (expect 0)",
orig.compare(copy_a)),UVM_NONE)
 
// Test do_pack + do_unpack symmetry
void'(orig.pack_bytes(packed));
`uvm_info("TEST",$sformatf("Packed size: %0d bytes (expect 9)",
packed.size()),UVM_NONE)
 
copy_b = demo_txn::type_id::create("copy_b");
void'(copy_b.unpack_bytes(packed));
`uvm_info("TEST",$sformatf("Pack/unpack symmetry: %0d (expect 1)",
orig.compare(copy_b)),UVM_NONE)
 
`uvm_info("TEST","All do_* method tests complete",UVM_NONE)
phase.drop_objection(this);
endtask
endclass
 
module do_methods_top;
initial run_test("do_methods_test");
endmodule
 
// Expected output:
// TEST: === do_print() output ===
//   Name    Type     Size  Value
//   addr    integral  32   'h????????
//   data    integral  32   'h????????
//   write   integral   1   'b?
//   resp    integral   2   'b01
//   tag     string    16   WRITE_BURST_TEST
// TEST: do_compare after clone: 1 (expect 1)
// TEST: do_compare after data change: 0 (expect 0)
// TEST: Packed size: 9 bytes (expect 9)
// TEST: Pack/unpack symmetry: 1 (expect 1)
// TEST: All do_* method tests complete

§10 — Interview Questions

  • What happens if you call txn.compare(other) without overriding do_compare(), and your class uses uvm_object_utils (not uvm_object_utils_begin)? The base uvm_object::do_compare() is called. It performs a type check only — verifying that both objects are the same class. It does NOT compare any fields because no field macros were declared and no override is present. The result is always 1 (match) regardless of field values. This is a silent bug that causes every scoreboard comparison to pass — even when fields differ.
  • Why must do_copy() call super.do_copy() before casting and copying fields? The super call handles two things: (1) it copies any fields registered via field macros in parent classes, and (2) it sets internal UVM state on the destination object. If you copy fields before the super call, and the super call then overwrites them with parent-class copies, you get the parent's values in the destination — not your child class values. The order is always: super first, then child-specific fields.
  • You have a transaction with a dynamic array of bytes and a nested sub-object. In do_copy(), how do you correctly handle both? Dynamic arrays in SystemVerilog copy by value with simple assignment: this.byte_arr = rhs_.byte_arr creates an independent copy. For the nested sub-object, simple handle assignment is a shallow copy — both objects share the same sub-object memory. You need $cast(this.sub_obj, rhs_.sub_obj.clone()) to create an independent deep copy. Failing to deep-copy nested objects is the most common bug in do_copy() implementations.
  • Tricky: What does compare_field("addr", addr, rhs_.addr, 32) return if addr = 32'hX? It returns 0 (mismatch) in most UVM implementations, because 4-state comparison with X values is treated as inequality. The comparer.compare_field() method uses === internally, and X === X is 1 (true), but X === 32'hA000 is 0. So if both sides contain X at the same bit positions, it matches. If the X patterns differ, it mismatches. In verification practice, X values in a comparison usually indicate a simulation initialization issue that needs to be investigated, not masked.
  • What is the difference between using `uvm_object_utils_begin and implementing do_copy manually? When would you mix both? Field macros generate generic do_* implementations via virtual dispatch — convenient but inflexible. Manual do_copy() is explicit code — flexible but verbose. You mix them when a class has simple fields that field macros handle well (parent class with macros) and complex fields that need custom logic (child class with manual override). The child's do_copy() calls super.do_copy() to invoke the parent's macro-generated copy, then handles the complex fields manually. This is a common pattern in layered VIP architectures.

§11 — Best Practices and Engineering Summary

PracticeReasoning
Always call super.do_*() firstNo exceptions. Parent class state, parent field handling, and UVM internal housekeeping all depend on it.
Always $cast and check the resultIf the cast fails, calling methods on the null handle crashes. A uvm_fatal before the crash gives you useful context.
Use compare_field() instead of ==It handles X/Z correctly, reports mismatches with field names to the transcript, and respects comparer verbosity settings.
Deep copy nested objects with clone()Any nested uvm_object or uvm_sequence_item must be cloned, not handle-assigned. Document why each field IS or IS NOT copied.
Keep pack and unpack order identicalWrite do_pack() and do_unpack() side by side in the same editor window. Order mismatch bugs are invisible until you test with a known bitstream.
Write a pack/unpack symmetry testRandomize → pack → unpack → compare. If result is 1, you're symmetric. Automate this for every VIP release.
Document exclusions explicitlyWhen a field is excluded from compare or pack, add a one-line comment explaining why. Future engineers (and future-you) will thank you.
Use a custom comparer in scoreboardsConfigure verbosity, max mismatches, and severity once at the scoreboard level. Reuse it for every comparison. Don't let UVM pick defaults for you.

These five methods form the backbone of object automation in UVM. They're not glamorous — you won't find them in conference talks or marketing slides. But when your scoreboard starts flagging the wrong mismatches, when a deep copy turns into a debugging nightmare, or when you need to serialize a transaction to a protocol-specific wire format — this is exactly where you end up. Knowing them well separates testbenches that hold up under real project pressure from ones that need constant fire-fighting.