Skip to content

UVM

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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_;
        bit    ok = 1;
        if (!$cast(rhs_, rhs)) return 0;
 
        // Accumulate with &= , NOT with && .
        // `&&` short-circuits by language rule (IEEE 1800), so the first
        // failing compare_field prevents every later one from RUNNING - and
        // compare_field is what records a miscompare with the comparer. The
        // result is a report naming one differing field when five differ,
        // which is the opposite of what uvm_comparer's show_max exists for.
        ok &= super.do_compare(rhs, comparer);
        ok &= comparer.compare_field("addr",  addr,  rhs_.addr,  32);
        ok &= comparer.compare_field("data",  data,  rhs_.data,  32);
        ok &= comparer.compare_field("write", write, rhs_.write,  1);
        // resp intentionally excluded — DUT response, not stimulus
        return ok;
    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 resultsUse &= to accumulate. Writing && short-circuits by language rule, so later compare_field calls never run and the report names only the first differing field
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_;
        bit     ok = 1;
        if (!$cast(rhs_, rhs)) return 0;
 
        // Accumulate with &= so every field is compared and every miscompare
        // is recorded. `&&` would stop at the first failure - see §3.
        ok &= super.do_compare(rhs, comparer);
        ok &= comparer.compare_object("hdr", hdr, rhs_.hdr);   // nested object
        ok &= comparer.compare_field("addr", addr, rhs_.addr, 64);
 
        // The size check DOES belong in front of the element comparison -
        // comparing queues of different length is meaningless - so it is one
        // place a short-circuit is deliberate rather than accidental.
        if (data.size() != rhs_.data.size()) begin
            comparer.print_msg($sformatf("data size %0d != %0d",
                                         data.size(), rhs_.data.size()));
            ok = 0;
        end else begin
            ok &= (data === rhs_.data);   // === handles X/Z correctly
        end
        return ok;
    endfunction
endclass

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

SystemVerilog — conditional pack/unpack based on protocol mode
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

SystemVerilog — Bug 1 and Bug 2 with fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

§8b — The Copy That Wasn't a Copy

Bug 1 covers a partial copy. This is the one that produces no missing fields at all and is far harder to see: a copy that succeeds, passes every field comparison, and shares storage with the original.

Assigning a scalar field duplicates the value giving independent storage, while assigning an object handle copies the pointer so both transactions share one object until clone is usedsource txnaddr = 0x40 · hdr → HeaderObjScalar: this.addr =rhs_.addrvalue copied → independentHandle: this.hdr =rhs_.hdrpointer copied → SHAREDDeep: $cast(this.hdr,rhs_.hdr.clone())new object → independentTwo independent valueschanging one leaves the otheraloneOne object, two referencesa change through either is seenby bothTwo independent objectsthe copy owns its own header12
Figure 1 — what copy() does with a scalar field versus an object handle. A scalar field such as addr is a value, so assigning it in do_copy duplicates the data: the source and the destination then hold independent copies and changing one does not affect the other. An object handle such as hdr is a pointer, so assigning it copies the pointer rather than the object — both transactions now reference the same header, and a change made through either handle is visible through the other. That is a shallow copy, and it is what a plain assignment gives you. A deep copy requires explicitly cloning the referenced object and casting the result into the destination's handle, which is the only form that gives the two transactions independent state.
1

A scoreboard's stored copy changed after it was stored

SHALLOW-COPY-SHARED-HANDLE
Symptom

An AXI scoreboard stored each observed request and compared it against the response when it arrived. Under light traffic it worked. Under load it reported mismatches whose expected values were wrong in a specific way: the expected header always matched the most recent transaction on the bus rather than the one being checked.

The mismatch count rose with outstanding depth. With one transaction in flight there were none at all, which pointed the investigation at the ordering logic and kept it there for a day.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class axi_txn extends uvm_sequence_item;
  `uvm_object_utils(axi_txn)
  rand bit [63:0] addr;
  axi_hdr         hdr;      // <- an OBJECT HANDLE, not a value
 
  function void do_copy(uvm_object rhs);
    axi_txn rhs_;
    super.do_copy(rhs);
    if (!$cast(rhs_, rhs)) `uvm_fatal("COPY", "cast failed")
    this.addr = rhs_.addr;
    this.hdr  = rhs_.hdr;   // ✗ copies the POINTER, not the object
  endfunction
endclass
 
// Scoreboard:
function void write_req(axi_txn t);
  axi_txn stored = axi_txn::type_id::create("stored");
  stored.copy(t);                 // looks like a snapshot; is not
  outstanding[t.id] = stored;
endfunction
Diagnostic Evidence

Printing the handles rather than the values made it immediate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  monitor txn   : hdr handle = @0x7f9c40a1
  stored copy   : hdr handle = @0x7f9c40a1     <-- the SAME object
  addr (scalar) : 0x40 vs 0x40, independent

One object, two references. The scalar addr was genuinely duplicated, which is why the transaction looked copied and why every field comparison passed at the moment of storage.

The load dependence followed directly. The monitor reuses its transaction object between beats, so it overwrites hdr in place for the next transaction. With one transaction in flight the comparison happens before any overwrite; with several outstanding, the stored "copy" tracks whatever the monitor most recently wrote.

Root Cause

this.hdr = rhs_.hdr copies a handle. SystemVerilog class variables are references, so assignment aliases the object rather than duplicating it — the two transactions share one header from that point on, and a write through either is visible through the other.

Two properties make this hard to catch. It is invisible at the moment of copying: every field of the copy equals the corresponding field of the source, because they are the same fields. And do_compare also passes, for the same reason — comparing an object against itself always succeeds.

The failure only appears when someone mutates the shared object afterwards, which puts an arbitrary distance between the bug and its symptom. In a testbench where the monitor allocates a fresh transaction per beat it may never appear at all, which is why the same VIP can be correct in one environment and broken in another with no code change.

The general rule: copy() is only as deep as do_copy makes it. UVM does not deep-copy handle fields for you, and the field macros do not either unless told to — uvm_field_object with UVM_REFERENCE copies the handle, and the default policy copies the handle too.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
function void do_copy(uvm_object rhs);
  axi_txn rhs_;
  super.do_copy(rhs);
  if (!$cast(rhs_, rhs)) `uvm_fatal("COPY", "cast failed")
  this.addr = rhs_.addr;
 
  // Deep copy: clone the referenced object and cast the result in.
  if (rhs_.hdr == null) this.hdr = null;
  else if (!$cast(this.hdr, rhs_.hdr.clone()))
    `uvm_fatal("COPY", "hdr clone/cast failed")
endfunction

The null guard matters: clone() on a null handle is a null dereference, and an optional sub-object is common.

The test that fails on the old code and passes on the new one is short, and it is the one nobody writes because copying feels self-evidently correct:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
axi_txn a = axi_txn::type_id::create("a");
axi_txn b = axi_txn::type_id::create("b");
a.hdr = axi_hdr::type_id::create("h");
a.hdr.qos = 4'h3;
b.copy(a);
a.hdr.qos = 4'hC;                       // mutate the SOURCE afterwards
if (b.hdr.qos != 4'h3)
  `uvm_error("COPY", "shallow copy: b.hdr tracks a.hdr")

Mutate the source after copying and assert the copy did not follow. That is the only check that distinguishes a deep copy from a shallow one; comparing the two immediately after the copy passes in both cases.

Two habits follow. Audit every handle field in a transaction and decide deliberately whether it should be deep-copied or deliberately shared — sharing is sometimes correct, for a configuration object every transaction points at, and it should be a comment rather than an accident. And prefer clone() over copy() when creating a snapshot, since clone() constructs a new object of the correct type through the factory and then calls copy(), which removes one opportunity to reuse a handle by mistake.

See sequence items for where these transactions originate and analysis ports for the broadcast path along which a shared handle propagates to every subscriber at once.

§9 — Ready-to-Run Demo

Ready to Run — Questa / VCS / Xcelium

SystemVerilog — do_methods_demo.sv (copy and run)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Two reasons, and the second is the one people forget.

It copies any fields registered through field macros in parent classes. If your transaction extends a base that used uvm_field_* macros, those fields are handled by the generated base implementation and by nothing else — skip the super call and they are silently never copied.

And it lets the base class perform its own bookkeeping before your fields are touched, which keeps the chain intact when someone later inserts a class between yours and the base.

The failure mode is a partial copy that looks complete: the fields you wrote by hand are right, the inherited ones hold whatever the destination happened to contain, and nothing errors. It typically surfaces after a factory override introduces a derived type, because that is when the inherited fields start carrying values that matter.

The same rule applies to do_compare, do_print and do_pack. Call super first, then handle your own fields.

Because && short-circuits by language rule, so the first failing compare_field prevents every later one from running — and running is what records a miscompare with the comparer.

The consequence is a debug experience exactly opposite to what uvm_comparer is designed for. The comparer accumulates miscompares up to show_max so the log shows every differing field in one report; a short-circuited chain names one field when five differ, and the next four appear only after you fix the first and re-run.

Accumulating with the bitwise &= evaluates every term:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bit ok = 1;
ok &= super.do_compare(rhs, comparer);
ok &= comparer.compare_field("addr", addr, rhs_.addr, 32);
ok &= comparer.compare_field("data", data, rhs_.data, 32);
return ok;

There is one place a short-circuit is deliberate rather than accidental: a size check before comparing queue elements, because comparing queues of different length is meaningless. Make that an explicit if so the intent is visible, rather than relying on operator behaviour.

A handle field was assigned rather than cloned, so the "copy" shares an object with the source.

Class variables in SystemVerilog are references, so this.hdr = rhs_.hdr duplicates the pointer, not the object. Both transactions then refer to one header, and a write through either is visible through the other.

What makes it hard is that it is invisible at the moment of copying. Every field of the copy equals the source's — because they are the same fields — so do_compare passes too. The failure appears only when something mutates the shared object afterwards, which puts arbitrary distance between the bug and its symptom.

The check that finds it is the one nobody writes, because copying feels self-evidently correct: mutate the source after copying and assert the copy did not follow.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
b.copy(a);
a.hdr.qos = 4'hC;
if (b.hdr.qos == 4'hC) `uvm_error("COPY", "shallow: b.hdr tracks a.hdr")

The fix is $cast(this.hdr, rhs_.hdr.clone()) with a null guard, since clone() on a null handle is a null dereference.

By value: scalars, packed structs, enums, strings, and — perhaps surprisingly — dynamic arrays and queues. this.data = rhs_.data; on a byte data[] genuinely duplicates the elements, so no clone is needed.

By reference: class handles, including nested transaction objects, configuration objects, and any uvm_object field. These need an explicit clone() and $cast.

The dividing line is whether the type is a class. Everything else is a value type in SystemVerilog, and assignment duplicates it.

Two caveats. An array of class handles copies the array by value and the handles inside it by reference, so this.items = rhs_.items; gives you a new array pointing at the same objects — you need to iterate and clone each element. And the field macros follow the same rule: uvm_field_object with the default policy copies the handle, and UVM_REFERENCE makes that explicit rather than changing it.

The practical habit is to audit every handle field in a transaction and decide deliberately whether it should be deep-copied or deliberately shared. Sharing is sometimes correct — a config object every transaction points at — and it should be a comment rather than an accident.

It reports a mismatch. compare_field performs a 4-state comparison in which an unknown does not equal anything, including another unknown — so comparing 32'hxxxxxxxx against 32'hxxxxxxxx fails.

That is the correct default for a scoreboard, because an X in observed data means the DUT drove something undefined and reporting it is the point — see scoreboard debugging for triaging which side an unexpected value came from. The trap is on the expected side: a reference model that leaves a field uninitialised produces expected values containing X, and every comparison then fails with a message that looks like a DUT bug.

The distinction to make when triaging is which operand carries the X. An X in the observed value is a DUT or sampling problem; an X in the expected value is a reference-model problem. Printing both operands rather than only the mismatch verdict is what separates them, which is another reason for the accumulating &= form — you want every field's report, not just the first.

Note this is also why comparing with == inside do_compare is wrong: X == X evaluates to X, which is treated as false in a condition, so a hand-rolled comparison silently reports a mismatch without telling the comparer anything. Use compare_field or ===.

Field macros for transactions whose fields are simple and whose semantics are uniform: copy everything, compare everything, print everything. That covers most sequence items, and the generated implementations are correct and free.

Write the methods manually when any field needs different treatment from the others. The common cases: a response field that must be copied but excluded from comparison, because it is the DUT's answer rather than stimulus; a timestamp that should be neither compared nor packed; a nested object needing a deep copy; a comparison that depends on mode, such as ignoring the upper address bits when a burst-type field says they are unused.

Mixing is legitimate and common: use uvm_object_utils_begin / uvm_field_* / uvm_object_utils_end for the bulk, and override just do_compare to add a mask. The generated method is virtual, so your override wins, and super.do_compare still runs the generated comparison for the fields you did not special-case.

The performance argument for manual methods is real but usually secondary: field macros work through a generic virtual dispatch over a field table, which costs more than direct assignments on a transaction compared millions of times. Write manually when the semantics require it, and treat the speed as a bonus.

Where This Is Specified

  • IEEE 1800.2-2020 (UVM) — uvm_object. The copy() / do_copy(), compare() / do_compare(), print() / do_print(), pack() / do_pack() and unpack() / do_unpack() pairs, and the convention that the public method drives policy while the do_* hook implements the class's own fields.
  • IEEE 1800.2-2020 — uvm_comparer. compare_field, compare_object, compare_string, the show_max limit on reported miscompares, and the accumulation of results that a short-circuited return defeats.
  • IEEE 1800.2-2020 — clone(). Construction of a new object of the correct type through the factory followed by copy(), and its role in deep-copying handle fields.
  • IEEE 1800.2-2020 — field automation macros. uvm_field_object and the copy policies, including that the default copies the handle rather than the referenced object.
  • IEEE 1800-2023 §11.4.7 — Logical operators. The short-circuit evaluation of && that makes it the wrong operator for accumulating comparison results.
  • IEEE 1800-2023 §8.3 — Class objects. Class variables as references, and the aliasing that makes handle assignment a shallow copy.

§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.

Continue learning