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
| Method | Called When You… | You Control… | Use When… |
|---|---|---|---|
do_copy(rhs) | Call dst.copy(src) | Which fields are copied and how — shallow vs deep for nested objects | Nested 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 masks | Protocol-mode-dependent comparison; masked-field ignore; custom tolerance ranges |
do_print(printer) | Call txn.print() | Which fields appear, in what format, at what nesting level | Hiding 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 order | Protocol-specific wire encoding; endianness control; conditional field packing |
do_unpack(packer) | Call txn.unpack() or unpack_bytes() | How a bitstream is decoded back into fields | Stateful 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.
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
| Step | What Happens | Notes |
|---|---|---|
| 1 | a.compare(b) called in scoreboard | Public API — thin wrapper |
| 2 | UVM framework allocates a uvm_comparer if none provided | Comparer tracks mismatches and verbosity |
| 3 | a.do_compare(b, comparer) is called on your class | Your override runs here |
| 4 | super.do_compare(b, comparer) validates type compatibility | Returns 0 immediately if types are incompatible |
| 5 | Each comparer.compare_field() call checks one field | On mismatch: logs a message with field name and values |
| 6 | Return value: AND of all field results | First mismatch short-circuits in most implementations |
| 7 | Caller receives 1 (match) or 0 (mismatch) | comparer.result also holds the count of mismatches |
do_copy() — Data Flow from Source to Destination
| Field | Source (rhs_) | Destination (this) | Copy Type |
|---|---|---|---|
addr | 32'hA000_0004 | 32'hA000_0004 | Value copy — integral, always safe |
data | 32'h1234_5678 | 32'h1234_5678 | Value copy — integral, always safe |
sub_obj | Handle (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 |
timestamp | Not copied (excluded by design) | Keeps original value | Intentional omission — valid pattern |
do_pack() — Bit Ordering and Wire Format
| Pack Order | Field | Width | Packed Bit Position | Hex in Stream |
|---|---|---|---|---|
| 1st | addr = 32'hA000_0000 | 32 bits | [63:32] | A0 00 00 00 |
| 2nd | data = 32'h1234_5678 | 32 bits | [31:0] | 12 34 56 78 |
| 3rd | write = 1'b1 | 1 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.
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
endclassExample 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.
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
endclassExample 3 — Protocol-Specific do_pack() and do_unpack()
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()
// 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
| Component | Method Used | Why | Real Example |
|---|---|---|---|
| Scoreboard | do_compare() | Check expected vs actual with protocol-aware masking | AXI scoreboard ignores RRESP on read errors when RD_RSP_OK is set |
| Monitor | do_pack() | Capture wire-level encoding for protocol analysis | Pack observed bus signals into TLM transaction for coverage |
| Driver | do_unpack() | Decode received response back to transaction fields | USB driver unpacks device response into status fields |
| Coverage | do_print() | Generate debug-friendly labels for coverage bins | Custom print shows burst type + length + address range together |
| Reference Model | do_copy() | Deep copy golden transactions before modification | Golden queue stores independent copies — no aliasing bugs |
| DMA Verification | do_pack() + do_unpack() | Serialize descriptor data to memory, deserialize back | DMA descriptor encoding for memory-mapped verification |
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.
// ── 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
// 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 (notuvm_object_utils_begin)? The baseuvm_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_arrcreates 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, andX === Xis 1 (true), butX === 32'hA000is 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
| Practice | Reasoning |
|---|---|
| Always call super.do_*() first | No exceptions. Parent class state, parent field handling, and UVM internal housekeeping all depend on it. |
| Always $cast and check the result | If 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 identical | Write 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 test | Randomize → pack → unpack → compare. If result is 1, you're symmetric. Automate this for every VIP release. |
| Document exclusions explicitly | When 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 scoreboards | Configure 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.