A scoreboard reports item mismatch on one transaction in ten thousand, and only on long regressions. You add a print of both items and they look identical. After a day you find it: the item's compare ignores a field the designer cares about, the copy the scoreboard stored is shallow, and the printed values were read after the monitor had changed the shared object underneath. Three methods every item has (copy, compare, print), and each one hid part of the bug. How should they be written, and who decides what they do?
Every uvm_object has a family of utility methods: copy, clone, compare, print, sprint, convert2string, plus pack and record. The public method is fixed by UVM; the class-specific work happens in a protected hook with a do_ prefix (do_copy, do_compare, do_print). You fill those hooks in one of two ways. Field macros (uvm_field_int and friends, inside uvm_object_utils_begin/end) generate the work from a list of fields. Hand-written do_* methods spell it out.
Think of it like a form letter versus a handwritten note. The form letter (macros) is quick to produce, consistent and easy to forget about, and that last point is the problem: it does exactly what it was set up to do, which may not be what you meant. The handwritten note costs more effort but says precisely what you intend, and anyone reading the code sees it.
You call | UVM does | Your class supplies |
|---|---|---|
| Checks for null, sets up a copier policy, calls the hook |
|
|
| Nothing extra: it reuses |
| Sets up a comparer policy, calls the hook, reports miscompares |
|
| Sets up a printer policy (table, line, tree), formats the output |
|
| Nothing: the default returns an empty string | A one-line summary for |
Two details in that table carry interview weight. `clone()` is not a factory call. It calls the object's own create() method, which the registration macro defines as a plain new of the same class, so factory overrides are not consulted and the clone has the same dynamic type as the original. And the comparer and printer are policy objects: they decide formatting, how many miscompares to report, how deep to recurse into nested objects, and your do_* methods talk to them rather than printing or reporting directly.
The bench's items already hand-write do_copy and convert2string, because the truncating driver relies on clone(). Here is axis_item with the other two methods added, the way you would extend it for a scoreboard that compares whole items.
// snippet: axis_item with all four utilities hand-written
class axis_item extends uvm_sequence_item;
rand bit [31:0] data;
rand bit last;
rand int unsigned idle; // driver timing knob: never observed on the bus
`uvm_object_utils(axis_item)
function new(string name = "axis_item");
super.new(name);
endfunction
function void do_copy(uvm_object rhs);
axis_item r;
super.do_copy(rhs); // copy the parent class's fields first
$cast(r, rhs);
data = r.data; last = r.last; idle = r.idle;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
axis_item r;
if (!$cast(r, rhs)) return 0;
do_compare = super.do_compare(rhs, comparer);
do_compare &= comparer.compare_field_int("data", data, r.data, 32, UVM_HEX);
do_compare &= comparer.compare_field_int("last", last, r.last, 1);
// idle is deliberately not compared: a monitored item never has it
endfunction
function void do_print(uvm_printer printer);
super.do_print(printer);
printer.print_field_int("data", data, 32, UVM_HEX);
printer.print_field_int("last", last, 1, UVM_BIN);
printer.print_field_int("idle", idle, 32, UVM_DEC);
endfunction
function string convert2string();
return $sformatf("data=%08h%s", data, last ? " LAST" : "");
endfunction
endclass`super.do_copy` and `super.do_compare` first, so a derived item (say, an error-injecting item with an extra field) can extend these methods without repeating its parent's fields.
`comparer.compare_field_int` instead of ==: it returns the same answer but also records which field differed, which is what you want in a failing regression's log.
`idle` is left out of the compare on purpose. It is a stimulus knob; the monitor never sees it, so comparing it would make every driver-side item mismatch every monitored one.
// snippet
class axis_item_fm extends uvm_sequence_item;
rand bit [31:0] data;
rand bit last;
rand int unsigned idle;
`uvm_object_utils_begin(axis_item_fm)
`uvm_field_int(data, UVM_ALL_ON | UVM_HEX)
`uvm_field_int(last, UVM_ALL_ON)
`uvm_field_int(idle, UVM_ALL_ON | UVM_NOCOMPARE | UVM_DEC)
`uvm_object_utils_end
function new(string name = "axis_item_fm");
super.new(name);
endfunction
endclassSix lines instead of thirty, and the flags express the same intent: UVM_NOCOMPARE keeps idle out of the compare. The flags and the macro variants for arrays, objects and enums are covered in Field macros. The question is not which is shorter but which you would rather debug at 2 am.
Both versions were run through the same script with the course's Verilator flow: clone an item, change one field, compare, print. The relevant output:
CLONE: compare=1 same_handle=0 name=a
UVM_INFO .../uvm_comparer.svh(575) @ 0: reporter [MISCMP] Miscompare for a.data: lhs = 'h12345678 : rhs = 'h12345679
UVM_INFO .../uvm_comparer.svh(527) @ 0: reporter [MISCMP] 1 Miscompare(s) for object a@149 vs. a@153
AFTER EDIT: compare=0
IDLE DIFFERS: compare=1
-----------------------------------
Name Type Size Value
-----------------------------------
a axis_item - @149
data integral 32 'h12345678
last integral 1 'b1
idle integral 32 'd3
-----------------------------------
OVERRIDE: create gives axis_item_err, clone gives axis_itemThe clone is equal but not the same handle, and it keeps the original's name, a, because clone() passes get_name() to create.
The miscompare names the field, a.data, because do_compare used the comparer. With == you would get only compare=0.
A difference in `idle` compares equal, as intended. The field-macro version behaved the same with UVM_NOCOMPARE.
The last line is the factory point: with a type override registered, type_id::create built the derived class, but clone() still built an axis_item.
do_copy decides how deep a copy goes. Copy a handle field with = and both objects share the nested object. This plain-SystemVerilog model (no UVM library needed) shows the difference with a packet that holds a header object.
class hdr;
bit [7:0] id;
endclass
class pkt;
bit [31:0] data;
hdr h;
function new();
h = new();
endfunction
function void copy_shallow(pkt rhs);
data = rhs.data;
h = rhs.h; // shares the header
endfunction
function void copy_deep(pkt rhs);
data = rhs.data;
h = new();
h.id = rhs.h.id; // owns its own header
endfunction
endclass
module tb_copy_depth;
pkt orig, s, d;
int errors = 0;
initial begin
orig = new();
orig.data = 32'hA5;
orig.h.id = 8'd1;
s = new(); s.copy_shallow(orig);
d = new(); d.copy_deep(orig);
orig.h.id = 8'd7; // the producer reuses its header
if (s.h.id != 8'd7) errors++; // the shallow copy changed with it
if (d.h.id != 8'd1) errors++; // the deep copy did not
if (errors == 0) $display("PASS: shallow copy saw id 7, deep copy kept id 1");
else $display("FAIL: %0d copy-depth checks wrong", errors);
$finish;
end
endmoduleField macros give the same choice through flags: uvm_field_object copies deeply by default, and UVM_REFERENCE makes it a handle copy. Hand-written code makes the choice visible in one line, which is its biggest advantage.
// snippet: from the course DMA bench
// Fault injection without touching the sequence: cuts every packet after beat
// `cut` by setting TLAST there and dropping the rest of the packet.
class axis_truncating_driver extends axis_driver;
`uvm_component_utils(axis_truncating_driver)
int unsigned cut = 3; // index of the beat that gets TLAST
int unsigned beat_no;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function axis_item pre_drive(axis_item it);
axis_item b;
int unsigned i = beat_no;
beat_no = it.last ? 0 : beat_no + 1; // count beats per packet
if (i > cut) return null; // after the cut: drop the beat
if (i < cut) return it;
$cast(b, it.clone()); // never edit the sequence's own item
b.last = 1'b1;
`uvm_info("TRUNC", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
return b;
endfunction
endclassOn the cut beat, the driver needs an item with last = 1. It could simply set it.last = 1, but it belongs to the sequence: the sequence created it, may still hold the handle, and may read it after finish_item returns. Editing it would quietly change the stimulus record, and a sequence that counts beats by last would see a packet end where it never ended one. So the driver clones first and edits the copy, which is only correct because axis_item::do_copy copies every field. Leave do_copy empty and the clone comes back with data = 0: the DMA receives a zero word, and the scoreboard (which predicts from the monitored input) agrees with it, so nothing fails while the test quietly stops testing what it claims to.
Concern | Field macros | Hand-written do_* |
|---|---|---|
Writing it | One line per field | Four methods to keep in step |
Run time | Slower: generic code walks every field for every operation | Fast: only the work you wrote |
Debugging | Behaviour hidden in macro expansion and flags | Plain code you can step through |
Control | Flags per field ( | Anything: conditional compares, tolerances, custom formats |
Auto-configuration | Fields can be set from config_db automatically | Read config explicitly |
A common team policy, and a defensible interview answer: field macros for config objects and low-volume classes, where convenience and auto-configuration help; hand-written methods for sequence items and anything copied or compared millions of times, where speed and explicit behaviour matter.
axis_err_item adds a field corrupt and overrides do_copy without calling super.do_copy. What does clone() return?An axis_err_item (clone keeps the dynamic type) with corrupt copied but data, last and idle left at their defaults, because the parent's do_copy never ran. Always call super.do_copy(rhs) first.
exp.compare(act) and a test fails with compare=0 but no MISCMP line in the log. What is the likely cause?The item's do_compare uses plain == and returns the result without going through the comparer, so the comparer never learns which field differed. Rewrite it with comparer.compare_field_int (or the other compare_* calls), or have the scoreboard print both items with sprint() when the compare fails.
No. clone() builds an object of the same class as the one being cloned; it never consults the factory. If you need the overridden type, create through type_id::create and then copy(). Cloning in a monitor is fine for a different reason: it gives subscribers an object the monitor will not change.
Next: you can now build the data side of a bench. The stimulus, check and coverage triangle shows how that data serves a testplan.