Field Macros — `uvm_field_*
What field macros automate, complete catalogue, flag options, critical limitations, performance cost.
UVM Fundamentals · Module 16
What Field Macros Do — The Six Auto-Generated Methods
A uvm_sequence_item needs to support printing, copying, comparing, packing, unpacking, and recording. Without field macros you write all six yourself. With field macros you declare each field once and the framework generates all six.
uvm_sequence_item class bodyuvm_object_utils_begin(my_txn) uvm_field_int(addr, UVM_ALL_ON) uvm_field_int(data, UVM_ALL_ON) uvm_field_bit(write, UVM_ALL_ON) uvm_field_enum(dir_e, dir, UVM_ALL_ON)uvm_object_utils_endgeneratesAUTO-GENERATED METHODSdo_print() / sprint()do_copy()do_compare()do_pack() / do_unpack()do_record()USAGE (no manual code needed)// All just work:txn.print(); // tables = txn.sprint(); // stringcopy_txn.copy(txn); // deepok = a.compare(b); // 1/0txn.pack_bytes(ba); // bytestxn.unpack_bytes(ba);txn.record(recorder);
Figure 1 — Field macros auto-generate six object automation methods. One `uvm_field_* line per field replaces 30–50 lines of manual do_copy/do_compare/do_print/do_pack/do_unpack/do_record code.
The Complete Field Macro Catalogue
| Macro | Used For | Example |
|---|---|---|
| ``uvm_field_int(NAME, FLAG)` | All integral types: bit, logic, int, byte, integer, longint, packed structs | ``uvm_field_int(addr, UVM_ALL_ON)` |
| ``uvm_field_string(NAME, FLAG)` | string fields | ``uvm_field_string(tag, UVM_ALL_ON)` |
| ``uvm_field_object(NAME, FLAG)` | uvm_object sub-objects (handles) | ``uvm_field_object(sub_pkt, UVM_ALL_ON)` |
| ``uvm_field_real(NAME, FLAG)` | real fields (note: no packing) | ``uvm_field_real(voltage, UVM_ALL_ON)` |
| ``uvm_field_enum(TYPE, NAME, FLAG)` | Enum fields — requires the enum type as first arg | ``uvm_field_enum(opcode_t, op, UVM_ALL_ON)` |
| ``uvm_field_array_int(NAME, FLAG)` | Fixed or dynamic arrays of integers | ``uvm_field_array_int(byte_arr, UVM_ALL_ON)` |
| ``uvm_field_array_object(NAME, FLAG)` | Arrays of uvm_object handles | ``uvm_field_array_object(pkts, UVM_ALL_ON)` |
| ``uvm_field_array_string(NAME, FLAG)` | Arrays of strings | ``uvm_field_array_string(tags, UVM_ALL_ON)` |
| ``uvm_field_queue_int(NAME, FLAG)` | Queues of integers | ``uvm_field_queue_int(data_q, UVM_ALL_ON)` |
| ``uvm_field_queue_object(NAME, FLAG)` | Queues of uvm_object handles | ``uvm_field_queue_object(pkt_q, UVM_ALL_ON)` |
| ``uvm_field_aa_int_string(NAME, FLAG)` | Associative array: int[string] | ``uvm_field_aa_int_string(reg_map, UVM_ALL_ON)` |
| ``uvm_field_aa_object_string(NAME, FLAG)` | Associative array: uvm_object[string] | ``uvm_field_aa_object_string(db, UVM_ALL_ON)` |
| ``uvm_field_event(NAME, FLAG)` | SystemVerilog events (print/record only) | ``uvm_field_event(done_ev, UVM_ALL_ON)` |
The begin/end Pattern — Every Field Macro Needs a Wrapper
Field macros must be enclosed between uvm_object_utils_begin` and uvm_object_utils_end (for objects) or ``uvm_component_utils_begin and uvm_component_utils_end` (for components). The simple uvm_object_utils(class)` macro is equivalent to begin/end with no fields in between.
typedef enum bit { WRITE=0, READ=1 } dir_e;
class apb_txn extends uvm_sequence_item;
// ── Field declarations ────────────────────────────────────────────
rand bit [31:0] addr;
rand bit [31:0] data;
rand dir_e direction;
rand bit [3:0] strobe; // byte enables
bit [1:0] resp; // not randomised
string tag; // debug label
bit [7:0] data_q[$]; // payload queue
// ── Factory registration with field automation ────────────────────
`uvm_object_utils_begin(apb_txn)
`uvm_field_int (addr, UVM_ALL_ON)
`uvm_field_int (data, UVM_ALL_ON)
`uvm_field_enum (dir_e, direction, UVM_ALL_ON)
`uvm_field_int (strobe, UVM_ALL_ON)
`uvm_field_int (resp, UVM_NOCOMPARE | UVM_PRINT)
`uvm_field_string(tag, UVM_NOCOMPARE | UVM_PRINT)
`uvm_field_queue_int(data_q, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "apb_txn");
super.new(name);
endfunction
constraint c_valid_addr { addr[1:0] == 2'b00; } // word-aligned
endclass
// ── What you get for free — all without writing any of these manually:
apb_txn a = apb_txn::type_id::create("a");
apb_txn b = apb_txn::type_id::create("b");
void'(a.randomize());
a.print(); // formatted table via do_print
b.copy(a); // deep copy via do_copy
`uvm_info("T", $sformatf("%0d", a.compare(b)), UVM_LOW) // 1 = match
bit [7:0] packed[$];
void'(a.pack_bytes(packed)); // serialize to bytes
b.unpack_bytes(packed); // deserialize backFlags and Options — Controlling Each Field's Behaviour
The second argument to every ``uvm_field_*macro is a flag that controls which methods include this field. Flags are bit masks combined with|`.
| Flag | Value | Effect |
|---|---|---|
UVM_ALL_ON | 0x3FF | Include field in all six operations. The standard choice for most fields. |
UVM_COPY | 0x001 | Include in do_copy() |
UVM_NOCOPY | negate | Exclude from do_copy() |
UVM_COMPARE | 0x002 | Include in do_compare() |
UVM_NOCOMPARE | negate | Exclude from do_compare() — use for debug fields like tag, timestamp, resp |
UVM_PRINT | 0x010 | Include in do_print() |
UVM_NOPRINT | negate | Exclude from print — use for large arrays or sensitive data |
UVM_PACK | 0x100 | Include in do_pack() / do_unpack() |
UVM_NOPACK | negate | Exclude from pack — use for metadata that doesn't travel with the packet |
UVM_DEFAULT | 0x3CF | All_on minus UVM_PACK — useful when packing is not needed |
UVM_READONLY | print only | Print only — no copy, compare, or pack. Good for internal counters and timestamps. |
UVM_HEX | display mode | Print value as hexadecimal. Combine: UVM_ALL_ON | UVM_HEX |
UVM_DEC | display mode | Print value as decimal. |
UVM_BIN | display mode | Print value as binary. |
`uvm_object_utils_begin(my_txn)
// ── Payload fields: copy, compare, pack, print ────────────────────
`uvm_field_int(addr, UVM_ALL_ON | UVM_HEX) // print as 0x...
`uvm_field_int(data, UVM_ALL_ON | UVM_HEX)
`uvm_field_int(strobe, UVM_ALL_ON | UVM_BIN) // print as 0b...
// ── Control field: all ops but show as decimal ────────────────────
`uvm_field_int(burst_len, UVM_ALL_ON | UVM_DEC)
// ── Response field: do not compare, do not pack — just print ─────
`uvm_field_int(resp, UVM_NOCOMPARE | UVM_NOPACK | UVM_PRINT | UVM_COPY)
// ── Debug tag: print only — never affects compare or copy ─────────
`uvm_field_string(debug_tag, UVM_READONLY)
// ── Timestamp: print only, not compared, not packed ───────────────
`uvm_field_int(timestamp, UVM_READONLY)
// ── Sub-object: include in copy+compare, exclude from pack ────────
`uvm_field_object(header_pkt, UVM_ALL_ON | UVM_NOPACK)
// ── Queue: all operations ─────────────────────────────────────────
`uvm_field_queue_int(payload_bytes, UVM_ALL_ON)
// ── Enum: remember the type comes first ───────────────────────────
`uvm_field_enum(opcode_t, opcode, UVM_ALL_ON)
`uvm_object_utils_endWhat Gets Generated — The Simplified Expansion
Understanding what the macros expand to helps you predict their behaviour and know when they will not do what you expect.
// `uvm_field_int(addr, UVM_ALL_ON) expands to contributions inside each
// of the automation framework methods. Simplified equivalent:
// Contribution to do_copy:
function void do_copy(uvm_object rhs);
apb_txn rhs_;
$cast(rhs_, rhs);
super.do_copy(rhs);
this.addr = rhs_.addr; // ← addr field contribution
// ... data, direction, strobe ...
endfunction
// Contribution to do_compare:
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
apb_txn rhs_;
$cast(rhs_, rhs);
return super.do_compare(rhs, comparer) &&
comparer.compare_field("addr", this.addr, rhs_.addr, $bits(addr));
endfunction
// Contribution to do_print:
function void do_print(uvm_printer printer);
super.do_print(printer);
printer.print_field("addr", this.addr, $bits(addr), UVM_HEX);
endfunction
// Contribution to do_pack/do_unpack:
function void do_pack(uvm_packer packer);
super.do_pack(packer);
packer.pack_field(this.addr, $bits(addr));
endfunction
function void do_unpack(uvm_packer packer);
super.do_unpack(packer);
this.addr = packer.unpack_field_int($bits(addr));
endfunction
// The actual macro expansion uses a dispatch table with virtual methods —
// it goes through uvm_field_automation() which adds runtime dispatch overhead.
// This is why field macros are slower than direct do_* implementations.Limitations — When to Stop Using Field Macros
Side-by-Side: With Macros vs Manual do_* Methods
❌ With Field Macros (convenient but limited) class apb_txn extends uvm_sequence_item; uvm_object_utils_begin(apb_txn) uvm_field_int(addr, UVM_ALL_ON) uvm_field_int(data, UVM_ALL_ON) uvm_object_utils_end rand bit[31:0] addr, data; function new(string n="apb_txn"); super.new(n); endfunction // Issues: // 1. Virtual dispatch overhead on // EVERY compare/copy call // 2. Cannot compare sub-ranges // e.g. addr[15:0] only // 3. Cannot add custom logic // e.g. skip compare if ignore_mask // 4. Enum display format fixed // 5. Packed structs need workaround endclass✓ Manual do_* (explicit but flexible) class apb_txn extends uvm_sequence_item; `uvm_object_utils(apb_txn) rand bit[31:0] addr, data; bit[31:0] addr_mask = 32'hFFFF_FFFF; function void do_copy(uvm_object rhs); apb_txn r; $cast(r, rhs); super.do_copy(rhs); addr = r.addr; data = r.data; endfunction function bit do_compare(uvm_object rhs, uvm_comparer cmp); apb_txn r; $cast(r, rhs); // Custom: compare only masked bits return ((addr & addr_mask) == (r.addr & r.addr_mask)) && (data == r.data); endfunction endclass
Five Situations Where Field Macros Fail You
- Performance-critical environments: Field macros use virtual dispatch through
uvm_field_automation(). In a scoreboard processing 1 million transactions, the overhead is measurable. Manualdo_compare()is 5–10× faster. - Custom comparison logic: You need to compare only certain bits, or skip comparison when a mask field is set. Field macros compare the full field value always — no conditional logic possible.
- Nested objects with ownership semantics: ``uvm_field_object
performs a shallow copy by default. Deep-copy semantics for nested objects require manualdo_copy()`. - Packed structs and unions: Packed structs with non-standard bit layouts do not map cleanly to field macros. You end up with workarounds that are harder to read than manual code.
- Waveform and coverage database recording:
do_record()generated by macros records fields with generic names. If your waveform viewer needs specific naming, manualdo_record()is required.
Ready-to-Run Simulator Example
Complete demonstration: an APB transaction with field macros, showing print(), compare(), clone(), pack_bytes(), and the effect of selective flags. Copy to field_macros_demo.sv and run.
Ready to Run — Questa / VCS / Xcelium
// field_macros_demo.sv — complete ready-to-run UVM field macro demonstration
//
// Compile and run:
// Questa : vlog -sv field_macros_demo.sv &&
// vsim -c field_macros_top -do "run -all; quit"
// VCS : vcs -sverilog -ntb_opts uvm field_macros_demo.sv && ./simv
// Xcelium: xrun -sv -uvm field_macros_demo.sv -input "run; exit"
`include "uvm_macros.svh"
import uvm_pkg::*;
// ═══════════════════════════════════════════════════════════════════════
// ENUM TYPE (must be defined outside the class)
// ═══════════════════════════════════════════════════════════════════════
typedef enum bit { APB_WRITE=0, APB_READ=1 } apb_dir_e;
// ═══════════════════════════════════════════════════════════════════════
// TRANSACTION WITH FIELD MACROS
// ═══════════════════════════════════════════════════════════════════════
class apb_txn extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand apb_dir_e direction;
rand bit [3:0] strobe;
bit [1:0] pslverr; // response — excluded from compare
string debug_tag; // label — excluded from compare
bit [7:0] payload[$]; // byte queue
time issue_time; // readonly — print only
`uvm_object_utils_begin(apb_txn)
`uvm_field_int (addr, UVM_ALL_ON | UVM_HEX)
`uvm_field_int (data, UVM_ALL_ON | UVM_HEX)
`uvm_field_enum (apb_dir_e, direction, UVM_ALL_ON)
`uvm_field_int (strobe, UVM_ALL_ON | UVM_BIN)
`uvm_field_int (pslverr, UVM_NOCOMPARE | UVM_PRINT | UVM_COPY)
`uvm_field_string (debug_tag, UVM_NOCOMPARE | UVM_PRINT)
`uvm_field_queue_int(payload, UVM_ALL_ON)
`uvm_field_int (issue_time,UVM_READONLY)
`uvm_object_utils_end
function new(string name = "apb_txn");
super.new(name);
endfunction
constraint c_align { addr[1:0] == 2'b00; }
constraint c_strobe { strobe != 4'h0; }
endclass
// ═══════════════════════════════════════════════════════════════════════
// TEST — exercises all field macro capabilities
// ═══════════════════════════════════════════════════════════════════════
class field_macro_test extends uvm_test;
`uvm_component_utils(field_macro_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
apb_txn a, b, c;
bit [7:0] packed_bytes[$];
phase.raise_objection(this);
// ── Create and randomize ──────────────────────────────────────
a = apb_txn::type_id::create("txn_a");
void'(a.randomize() with { addr == 32'hA000_0000; direction == APB_WRITE; });
a.data = 32'h1234_5678;
a.pslverr = 2'b00;
a.debug_tag = "test_write_1";
a.issue_time = $time;
a.payload = { 8'h12, 8'h34, 8'h56, 8'h78 };
// ── print(): shows formatted table ────────────────────────────
`uvm_info("TEST", "=== print() output ===", UVM_NONE)
a.print();
// ── clone(): deep copy ────────────────────────────────────────
$cast(b, a.clone());
b.set_name("txn_b_clone");
`uvm_info("TEST", "=== compare() after clone — must be 1 ===", UVM_NONE)
`uvm_info("TEST", $sformatf("a.compare(b) = %0d", a.compare(b)), UVM_NONE)
// ── Modify and compare — should show mismatch ─────────────────
b.data = 32'hFFFF_0000;
`uvm_info("TEST", "=== compare() after data change — must be 0 ===", UVM_NONE)
`uvm_info("TEST", $sformatf("a.compare(b) = %0d", a.compare(b)), UVM_NONE)
// ── pslverr is NOCOMPARE — change it, compare still passes ────
$cast(c, a.clone());
c.pslverr = 2'b11; // error response
`uvm_info("TEST", "=== compare() with different pslverr — must be 1 (NOCOMPARE) ===", UVM_NONE)
`uvm_info("TEST", $sformatf("a.compare(c) = %0d", a.compare(c)), UVM_NONE)
// ── pack_bytes(): serialize to byte array ─────────────────────
void'(a.pack_bytes(packed_bytes));
`uvm_info("TEST",
$sformatf("pack_bytes() produced %0d bytes", packed_bytes.size()),
UVM_NONE)
// ── unpack_bytes(): deserialize back ──────────────────────────
b.data = 0; b.addr = 0;
void'(b.unpack_bytes(packed_bytes));
`uvm_info("TEST",
$sformatf("After unpack: addr=0x%0h data=0x%0h", b.addr, b.data),
UVM_NONE)
// ── sprint(): get the print as a string ───────────────────────
`uvm_info("TEST", "=== sprint() into string ===", UVM_NONE)
`uvm_info("TEST", a.sprint(), UVM_NONE)
`uvm_info("TEST", "=== All field macro tests passed ===", UVM_NONE)
phase.drop_objection(this);
endtask
endclass
// ═══════════════════════════════════════════════════════════════════════
// TOP MODULE
// ═══════════════════════════════════════════════════════════════════════
module field_macros_top;
initial run_test("field_macro_test");
endmodule
// ═══════════════════════════════════════════════════════════════════════
// EXPECTED OUTPUT (abridged):
//
// TEST: === print() output ===
// -----------------------------------------------
// Name Type Size Value
// -----------------------------------------------
// txn_a apb_txn - @1
// addr integral 32 'ha0000000
// data integral 32 'h12345678
// direction apb_dir_e 1 APB_WRITE
// strobe integral 4 'b1111
// pslverr integral 2 'h0
// debug_tag string 12 test_write_1
// payload da(integral) 4 ...
// issue_time integral 64 'h0 (readonly)
// -----------------------------------------------
//
// TEST: compare() after clone — must be 1
// TEST: a.compare(b) = 1
//
// TEST: compare() after data change — must be 0
// TEST: a.compare(b) = 0
//
// TEST: compare() with different pslverr — must be 1 (NOCOMPARE)
// TEST: a.compare(c) = 1
//
// TEST: pack_bytes() produced N bytes
// TEST: After unpack: addr=0xa0000000 data=0x12345678
// TEST: === All field macro tests passed ===
// ═══════════════════════════════════════════════════════════════════════## ── Questa ────────────────────────────────────────────────────────────
vlog -sv -timescale 1ns/1ps field_macros_demo.sv
vsim -c field_macros_top +UVM_VERBOSITY=UVM_NONE \
-do "run -all; quit -f"
## ── VCS ───────────────────────────────────────────────────────────────
vcs -sverilog -ntb_opts uvm -timescale=1ns/1ps field_macros_demo.sv -o simv
./simv +UVM_TESTNAME=field_macro_test +UVM_VERBOSITY=UVM_NONE
## ── Xcelium ───────────────────────────────────────────────────────────
xrun -sv -uvm -timescale 1ns/1ps field_macros_demo.sv \
-input "run; exit" \
+UVM_TESTNAME=field_macro_test +UVM_VERBOSITY=UVM_NONE
## ── To see UVM report summary:
## Remove +UVM_VERBOSITY=UVM_NONE from any command aboveQuick Reference
| Task | Syntax |
|---|---|
| Wrap field macros (object) | ``uvm_object_utils_begin(class_name) ... uvm_object_utils_end |
| Wrap field macros (component) | ``uvm_component_utils_begin(class_name) ... uvm_component_utils_end |
| Integer field (all ops) | ``uvm_field_int(field_name, UVM_ALL_ON)` |
| Integer field (hex display) | ``uvm_field_int(field_name, UVM_ALL_ON | UVM_HEX)` |
| Enum field | ``uvm_field_enum(enum_type, field_name, UVM_ALL_ON)` |
| String field | ``uvm_field_string(field_name, UVM_ALL_ON)` |
| Object field (sub-object) | ``uvm_field_object(field_name, UVM_ALL_ON)` |
| Queue of integers | ``uvm_field_queue_int(field_name, UVM_ALL_ON)` |
| Exclude from compare (timestamps, responses) | UVM_NOCOMPARE | UVM_PRINT | UVM_COPY |
| Print only (read-only metadata) | UVM_READONLY |
| Print and call all auto methods | txn.print() / s = txn.sprint() |
| Clone (deep copy to new object) | $cast(clone_h, txn.clone()) |
| Pack to bytes | void'(txn.pack_bytes(byte_queue)) |
| Unpack from bytes | void'(txn.unpack_bytes(byte_queue)) |
§9 — Code Examples
Example 1 — Beginner: Minimal APB Transaction with Field Macros
The most common starting point. Three fields, three macros, and you get print(), copy(), compare(), and pack/unpack — plus convert2string() and record() — all for free. This is what the generated output actually looks like.
class apb_seq_item extends uvm_sequence_item;
`uvm_object_utils_begin(apb_seq_item)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_field_int(write, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
function new(string name="apb_seq_item"); super.new(name); endfunction
endclass
// ── What each generated method produces ──────────────────────────────
//
// txn.print():
// --------------------------------------------------------
// Name Type Size Value
// --------------------------------------------------------
// apb_seq_item object -
// addr integral 32 'h4000
// data integral 32 'hA5A5
// write integral 1 'h1
//
// txn.convert2string():
// "addr='h4000 data='ha5a5 write='h1"
//
// txn1.compare(txn2):
// Returns 1 if all three fields match, 0 otherwise.
// Prints mismatch detail if +UVM_REPORT_VERBOSITY=UVM_HIGH
//
// txn_copy = txn.clone(): deep copy of all three fields (new object)
//
// txn.pack_ints(arr):
// Packs addr[31:0] + data[31:0] + write[0] = 65 bits into integer array
// ── Scoreboard compare using auto-generated compare() ────────────────
function void write(apb_seq_item actual);
apb_seq_item expected;
// ... get expected ...
if (!actual.compare(expected)) begin
`uvm_error("SCB", $sformatf("Mismatch:\n actual: %s\n expected: %s",
actual.convert2string(), expected.convert2string()))
end
endfunctionExample 2 — Intermediate: Flags Controlling Comparison and Print
The UVM_DEFAULT flag is actually a combination. Breaking it apart gives you precise control over which operations include which fields.
// UVM_DEFAULT = UVM_ALL_ON = 0x01FF
// Deconstructed: COPY | COMPARE | PRINT | RECORD | PACK | UNPACK | NORADIX
//
// Selectively disable specific operations per field:
class packet extends uvm_sequence_item;
`uvm_object_utils_begin(packet)
// addr: full default — compare, print, copy, pack all enabled
`uvm_field_int(addr, UVM_DEFAULT)
// data: compared and printed, but EXCLUDED from pack/unpack
`uvm_field_int(data, UVM_DEFAULT | UVM_NOPACK)
// timestamp: printed (for debugging) but NOT compared
// Timestamps differ between expected and actual — compare must skip it
`uvm_field_int(timestamp, UVM_DEFAULT | UVM_NOCOMPARE)
// debug_tag: internal debug only — never printed in clean regression logs
`uvm_field_int(debug_tag, UVM_COPY | UVM_COMPARE)
// UVM_COPY | UVM_COMPARE: copy and compare but don't print
// status: excluded from comparison entirely (DUT-side, cannot predict)
`uvm_field_int(status, UVM_DEFAULT | UVM_NOCOMPARE)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
logic [63:0] timestamp;
logic [7:0] debug_tag;
logic [3:0] status;
function new(string n="packet"); super.new(n); endfunction
endclass
// ── Common flag values ────────────────────────────────────────────────
//
// UVM_DEFAULT = 0x01FF All operations enabled (copy+compare+print+pack+...)
// UVM_NOCOMPARE = 0x0080 Exclude from compare()
// UVM_NOPRINT = 0x0010 Exclude from print()
// UVM_NOPRINTX = 0x0010 (same as NOPRINT in most implementations)
// UVM_NOPACK = 0x0100 Exclude from pack/unpack
// UVM_NOCOPY = 0x0001 Exclude from copy() — rarely used
// UVM_COPY = 0x0001 Explicitly enable copy
// UVM_COMPARE = 0x0004 Explicitly enable compare
// UVM_PRINT = 0x0008 Explicitly enable printExample 3 — Verification: Dynamic Array and Queue Fields
Field macros handle dynamic arrays and queues natively — but the behaviour differs from what engineers expect when the array size changes between operations.
class burst_txn extends uvm_sequence_item;
`uvm_object_utils_begin(burst_txn)
`uvm_field_int(start_addr, UVM_DEFAULT)
// Dynamic array of 32-bit data words
`uvm_field_array_int(data_words, UVM_DEFAULT)
// Queue of byte-sized error flags
`uvm_field_queue_int(error_flags, UVM_DEFAULT)
// Enum field
`uvm_field_enum(burst_type_e, burst_type, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] start_addr;
rand logic [31:0] data_words[]; // dynamic array
rand logic [7:0] error_flags[$]; // queue
rand burst_type_e burst_type;
function new(string n="burst_txn"); super.new(n); endfunction
endclass
// ── What compare() does with arrays ──────────────────────────────────
// 1. Compares array SIZE first — different sizes → immediate mismatch
// 2. Compares elements index by index
// 3. Prints element-level mismatch detail at UVM_HIGH verbosity
//
// ── What copy() does with dynamic arrays ─────────────────────────────
// Creates a new dynamic array of the same size and copies each element.
// This IS a deep copy for int/logic arrays — each element is a value type.
// For object arrays (uvm_field_array_object), copy() does SHALLOW copy
// — each element is still a shared handle. Use do_copy() for true deep copy.
//
// ── Tricky: compare() on different-length arrays ─────────────────────
burst_txn a = burst_txn::type_id::create("a");
burst_txn b = burst_txn::type_id::create("b");
a.data_words = new[4]('{1,2,3,4});
b.data_words = new[3]('{1,2,3});
// a.compare(b) → 0 (FAIL) — size mismatch detected first
// Error: "data_words: size mismatch: lhs=4 rhs=3"Example 4 — Tricky: Object Handle Field — The Shallow Copy Trap
The most dangerous field macro behaviour. When a transaction contains a handle to another object, ``uvm_field_object` does a shallow copy by default. Both the original and the copy point to the same sub-object. Modifying one corrupts the other.
class header extends uvm_object;
`uvm_object_utils_begin(header)
`uvm_field_int(hdr_type, UVM_DEFAULT)
`uvm_object_utils_end
rand int hdr_type;
function new(string n="header"); super.new(n); endfunction
endclass
class complex_txn extends uvm_sequence_item;
`uvm_object_utils_begin(complex_txn)
// ❌ Shallow copy — both original and copy share the SAME header object!
`uvm_field_object(hdr, UVM_DEFAULT)
`uvm_field_int(payload, UVM_DEFAULT)
`uvm_object_utils_end
header hdr;
rand int payload;
function new(string n="complex_txn"); super.new(n); endfunction
endclass
// ── Demonstration of the shallow copy problem ────────────────────────
complex_txn orig = complex_txn::type_id::create("orig");
complex_txn copy;
orig.hdr = header::type_id::create("hdr");
orig.hdr.hdr_type = 1;
$cast(copy, orig.clone()); // clone calls copy(), which calls do_copy()
copy.hdr.hdr_type = 99; // modifying copy's header...
// $display(orig.hdr.hdr_type) → prints 99, not 1!
// BECAUSE: `uvm_field_object does shallow copy — hdr pointer is shared!
// ✓ FIX: override do_copy() for deep copy of object fields ─────────────
function void do_copy(uvm_object rhs);
complex_txn rhs_cast;
super.do_copy(rhs); // copies scalar fields via field macros
$cast(rhs_cast, rhs);
// Manually deep-copy the object handle
hdr = header::type_id::create("hdr");
hdr.copy(rhs_cast.hdr); // new object with same field values
endfunction§10 — Bugs & Debugging
Bug 1 — UVM_NOCOMPARE Silently Making Scoreboard Always Pass
⚠️ Critical Bug — Scoreboard Reports PASS with Wrong DUT Data
The data field has UVM_NOCOMPARE set — perhaps from a copy-paste where the engineer wanted to exclude it from printing. The scoreboard calls compare(). It always returns 1 (match). The DUT could be sending all zeros and the test would still pass. This bug has slipped through real project regression for weeks before being caught in directed testing.
// ❌ WRONG — UVM_NOCOMPARE applied to critical data field ─────────────
class apb_txn extends uvm_sequence_item;
`uvm_object_utils_begin(apb_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_NOCOMPARE) // ← data NOT compared!
`uvm_field_int(write, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
endclass
// In scoreboard:
// txn_actual.compare(txn_expected) → returns 1 even if data differs!
// → `uvm_error never fires → CI reports PASS → DUT corruption undetected
// ✓ CORRECT — use UVM_DEFAULT for fields that must be compared ────────
`uvm_field_int(data, UVM_DEFAULT) // ← compare enabled
// If you genuinely need to exclude from comparison, document it clearly:
// `uvm_field_int(timestamp, UVM_DEFAULT | UVM_NOCOMPARE) // timestamp: DUT-generated, not predicted
// And add an assertion in check_phase to verify the field separately if needed.Bug 2 — Missing begin/end Wrapper — Methods Not Generated
// ❌ WRONG — field macro outside the begin/end block ─────────────────
class bad_txn extends uvm_sequence_item;
`uvm_object_utils(bad_txn) // ← compact form, no begin/end
`uvm_field_int(addr, UVM_DEFAULT) // ← OUTSIDE the wrapper — ignored!
rand logic [31:0] addr;
endclass
// bad_txn t;
// t.print() → shows "bad_txn" with NO fields listed
// t.compare(other) → always returns 1 (no fields registered for compare)
// t.clone() → creates empty object (no fields copied)
// ✓ CORRECT — field macros inside begin/end ───────────────────────────
class good_txn extends uvm_sequence_item;
`uvm_object_utils_begin(good_txn) // ← _begin form required for field macros
`uvm_field_int(addr, UVM_DEFAULT) // ← INSIDE: generates code for all 6 methods
`uvm_object_utils_end // ← closes the block
rand logic [31:0] addr;
function new(string n="good_txn"); super.new(n); endfunction
endclass
// good_txn t;
// t.addr = 32'h4000;
// t.print() → shows addr = 'h4000 ← field visible
// t.compare(other) → actually compares addr fieldBug 3 — Performance Kill: Field Macros on High-Frequency Transactions
// ── Auto-generated (field macro): ~6 function calls per field ────────
class slow_txn extends uvm_sequence_item;
`uvm_object_utils_begin(slow_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr, data;
// convert2string() → uses generic recorder → slow for high-frequency use
endclass
// ── Hand-written: one $sformatf call ─────────────────────────────────
class fast_txn extends uvm_sequence_item;
`uvm_object_utils(fast_txn) // compact — no field macros
rand logic [31:0] addr, data;
function string convert2string();
return $sformatf("addr=0x%0h data=0x%0h", addr, data);
endfunction
function void do_copy(uvm_object rhs);
fast_txn rhs_t; super.do_copy(rhs);
$cast(rhs_t, rhs);
addr = rhs_t.addr;
data = rhs_t.data;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
fast_txn rhs_t;
if (!$cast(rhs_t, rhs)) return 0;
return super.do_compare(rhs, comparer) &&
(addr === rhs_t.addr) &&
(data === rhs_t.data);
endfunction
endclass
// Rule of thumb:
// ≤ 8 scalar fields + low transaction rate → use field macros (convenience wins)
// > 8 fields OR high transaction rate (> 10K/ms) → hand-write do_copy/do_compare/convert2string§11 — Ready-to-Run: Field Macros vs Manual Implementation
Side-by-side: the same APB transaction implemented with field macros and with hand-written do_copy() / do_compare() / convert2string(). Run both and compare the print() output and compare() behaviour. Ready to Run — Questa / VCS / Xcelium
// field_macro_demo.sv
// Compile: vlog -sv field_macro_demo.sv
// Run: vsim -c work.tb_fm_top +UVM_TESTNAME=fm_test -do "run -all; quit"
`include "uvm_macros.svh"
import uvm_pkg::*;
// ── VERSION A: field macros ────────────────────────────────────────────
class apb_macro_txn extends uvm_sequence_item;
`uvm_object_utils_begin(apb_macro_txn)
`uvm_field_int(addr, UVM_DEFAULT)
`uvm_field_int(data, UVM_DEFAULT)
`uvm_field_int(write, UVM_DEFAULT)
`uvm_object_utils_end
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
function new(string n="apb_macro_txn"); super.new(n); endfunction
endclass
// ── VERSION B: manual implementation ──────────────────────────────────
class apb_manual_txn extends uvm_sequence_item;
`uvm_object_utils(apb_manual_txn)
rand logic [31:0] addr;
rand logic [31:0] data;
rand logic write;
function new(string n="apb_manual_txn"); super.new(n); endfunction
function string convert2string();
return $sformatf("[APB] addr=0x%08h data=0x%08h write=%0b",
addr, data, write);
endfunction
function void do_copy(uvm_object rhs);
apb_manual_txn r;
super.do_copy(rhs);
$cast(r, rhs);
addr = r.addr;
data = r.data;
write = r.write;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
apb_manual_txn r;
if (!$cast(r, rhs)) return 0;
if (addr !== r.addr) comparer.compare_field("addr", addr, r.addr, 32);
if (data !== r.data) comparer.compare_field("data", data, r.data, 32);
if (write !== r.write) comparer.compare_field("write", write, r.write, 1);
return (addr === r.addr) && (data === r.data) && (write === r.write);
endfunction
endclass
// ── Test: exercises both versions ─────────────────────────────────────
class fm_test extends uvm_test;
`uvm_component_utils(fm_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
task run_phase(uvm_phase phase);
apb_macro_txn a1, a2;
apb_manual_txn m1, m2;
phase.raise_objection(this);
// ── Macro version ─────────────────────────────────────────────
a1 = apb_macro_txn::type_id::create("a1");
a1.addr = 32'h4000; a1.data = 32'hA5A5; a1.write = 1;
`uvm_info("TEST", "=== Field Macro Version ===", UVM_LOW)
`uvm_info("TEST", $sformatf("convert2string: %s", a1.convert2string()), UVM_LOW)
a1.print();
$cast(a2, a1.clone());
a2.data = 32'hBEEF; // a1.data unchanged (clone = deep copy of scalars)
`uvm_info("TEST", $sformatf("compare (should differ): %0d",
a1.compare(a2)), UVM_LOW) // expect 0
// ── Manual version ────────────────────────────────────────────
m1 = apb_manual_txn::type_id::create("m1");
m1.addr = 32'h4000; m1.data = 32'hA5A5; m1.write = 1;
`uvm_info("TEST", "=== Manual Version ===", UVM_LOW)
`uvm_info("TEST", $sformatf("convert2string: %s", m1.convert2string()), UVM_LOW)
$cast(m2, m1.clone());
m2.data = 32'hBEEF;
`uvm_info("TEST", $sformatf("compare (should differ): %0d",
m1.compare(m2)), UVM_LOW) // expect 0
phase.drop_objection(this);
endtask
endclass
module tb_fm_top;
initial run_test();
endmodule
// Expected output:
// TEST: === Field Macro Version ===
// TEST: convert2string: addr=4000 data=a5a5 write=1 (macro format)
// Name Type Size Value
// apb_macro_txn object -
// addr integral 32 'h4000
// data integral 32 'ha5a5
// write integral 1 'h1
// TEST: compare (should differ): 0 ← data=0xa5a5 vs data=0xbeef
//
// TEST: === Manual Version ===
// TEST: convert2string: [APB] addr=0x00004000 data=0xa5a5a5a5 write=1
// TEST: compare (should differ): 0 ← same result, more control§12 — Interview Questions
Beginner Level
Intermediate Level
Senior / Architect Level
§13 — Best Practices
| Rule | Practice | Why It Matters |
|---|---|---|
| BP-1 | Always use uvm_object_utils_begin/end` when adding field macros — never uvm_object_utils` (compact form) | Field macros outside the begin/end block are silently ignored — print/compare/copy all appear to work but process no fields |
| BP-2 | Use UVM_DEFAULT for all primary protocol fields (addr, data, write, etc.) that must be compared | UVM_NOCOMPARE on a primary field silently disables scoreboard checking — one of the hardest bugs to find |
| BP-3 | Explicitly use UVM_DEFAULT | UVM_NOCOMPARE for fields that should not be compared — and add a comment explaining why | Documents intent; prevents future engineers from "fixing" the flag and breaking intentional exclusions |
| BP-4 | Never rely on ``uvm_field_object` for deep copy — always override do_copy() | Shallow copy via field macros creates shared object handles; modifying one modifies all, causing silent data corruption |
| BP-5 | For transactions with > 8 fields or used in tight loops, hand-write convert2string() | The macro-generated version goes through the recorder machinery; a direct $sformatf is 2–5× faster for high-frequency monitoring |
| BP-6 | Declare field macros in the same order as physical wire layout when using pack/unpack | Pack order follows declaration order; a different order from the wire protocol requires a manual do_pack() |
| BP-7 | Test your compare() by creating two objects with known differences and calling compare() — verify it returns 0 | A comparison that always returns 1 is worse than no comparison — it gives false confidence; always verify the mechanism works |
| BP-8 | Prefer hand-written do_compare() for production scoreboard transactions in shared VIPs | Explicit comparison logic is auditable; field macros hide the comparison logic behind flags that require understanding of macro internals to audit |
§14 — Summary
| Aspect | Field Macros | Manual Implementation |
|---|---|---|
| Setup effort | One line per field — minimal | Write do_copy, do_compare, convert2string — 20–40 lines |
| Object handle fields | Shallow copy — dangerous for nested objects | Full control — explicit deep copy in do_copy() |
| Comparison logic | Per-field flags only — no conditional comparisons | Any logic — compare field A only when field B has a specific value |
| Performance (high freq) | Overhead from recorder and format machinery | Direct $sformatf and === comparison — fastest possible |
| Pack/unpack order | Follows declaration order — may not match wire layout | Any bit layout expressible in do_pack() |
| Auditability | Requires understanding flag combinations to audit | Fully explicit — every field and condition visible |
| Best used for | Quick prototyping; transactions with ≤ 8 scalar fields at low transaction rate | Production VIPs; high-frequency monitors; complex nested objects |