In short: A UVM sequence item is the randomizable transaction object — derived from uvm_sequence_item — that carries stimulus between sequences and drivers, and its power comes from rand fields plus constraints and the copy/compare/print infrastructure you wire up through the field automation macros or hand-written do_* hooks.
If you have ever run a constrained-random testbench on VCS, Xcelium, or Questa, nearly every packet, bus burst, or instruction that moves through it started life as a UVM sequence item. Understanding this object well separates engineers who merely assemble a testbench from those who can debug a mismatch at 2 a.m. without guessing. This article walks through what the transaction object actually is, how its fields are declared and randomized, and what the four core data operations — copy, compare, print, and pack — really do under the hood.
What a sequence item is
A sequence item is a pure data object. It extends uvm_sequence_item (which in turn extends uvm_transaction and ultimately uvm_object), so it lives on the heap, is created with the factory, and has no ports, no clock, and no notion of time. It describes one atomic unit of stimulus or response: an AXI write beat, a SPI frame, an Ethernet packet, a floating-point operand pair. The driver's job is to translate that abstract description into pin wiggles; the item itself never touches a wire.
Rule of thumb: if a field would help you describe a transaction on a whiteboard, it belongs in the sequence item. If it describes how the transaction is driven on pins (setup time, phase), it belongs in the driver or the config object — not the item.
Declaring fields and registering with the factory
A minimal item declares its data as class properties, marks the random ones rand, and registers with the factory. Most teams use the field-automation block so the base class can synthesize the data operations:
class bus_txn extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data[]; // variable-length burst
rand enum {READ, WRITE} kind;
int unsigned id; // non-random bookkeeping
`uvm_object_utils_begin(bus_txn)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_array_int(data, UVM_ALL_ON)
`uvm_field_enum(kind, kind, UVM_ALL_ON)
`uvm_object_utils_end
constraint c_burst { data.size() inside {[1:16]}; }
function new(string name="bus_txn");
super.new(name);
endfunction
endclass
The `uvm_field_* macros are convenient but not free: they expand into generic, reflection-style loops that walk a field table at run time. On large items driven millions of times, that overhead is measurable. Many production shops (and the UVM 1.2/IEEE 1800.2 guidance) therefore prefer hand-written do_* methods for hot-path transactions and reserve the macros for occasional config objects.
Randomization and constraints
Marking a field rand only makes it a candidate; constraints decide the legal space. When a sequence calls item.randomize(), the SystemVerilog constraint solver (Synopsys, Cadence, and Siemens each ship their own) picks a value for every active rand field that satisfies all active constraints simultaneously. You layer additional constraints at the call site with randomize() with { ... }, and you can switch individual variables off using rand_mode(0) or whole constraints with constraint_mode(0).
Always check the return value: if(!item.randomize()) `uvm_fatal(...). A silent randomization failure is one of the most common causes of a test that "passes" while driving all-zero stimulus.
Functional coverage then closes the loop — covergroups sample these same fields so you can prove the random space you intended was actually exercised. For a deeper treatment of how items are produced and arbitrated, see our companion piece on the UVM sequence and sequencer.
do_copy, do_compare, do_print, do_pack
Every uvm_object exposes public wrappers — copy(), compare(), print(), pack()/unpack() — that call protected virtual hooks you override. This is the heart of transaction handling in a scoreboard.
| Public call | Virtual hook | What you implement |
|---|---|---|
copy(rhs) | do_copy | Deep-copy each field so the clone is independent (no shared dynamic arrays) |
compare(rhs) | do_compare | Field-by-field equality via a uvm_comparer; return 1 only if all match |
print() | do_print | Emit each field through a uvm_printer for readable logs |
pack() | do_pack | Serialize fields into a bitstream (register models, reference-model I/O) |
A hand-written pair looks like this — note that do_copy must duplicate the dynamic array, not alias it:
function void do_copy(uvm_object rhs);
bus_txn t;
super.do_copy(rhs);
$cast(t, rhs);
addr = t.addr; kind = t.kind;
data = new[t.data.size()]; // fresh storage
foreach(data[i]) data[i] = t.data[i];
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
bus_txn t;
if(!$cast(t, rhs)) return 0;
return super.do_compare(rhs, comparer)
& comparer.compare_field_int("addr", addr, t.addr, 32)
& comparer.compare_field("kind", kind, t.kind, 2);
endfunction
Why does this matter in practice? A shallow copy that aliases data is a classic bug: the scoreboard stores a "golden" clone, the driver later mutates the original, and both now point at the same array — so the comparison always matches and real mismatches vanish. Routing do_compare through the uvm_comparer (rather than ad-hoc if statements) also gives you miscompare logging, a configurable abort count, and a verbosity knob for free.
Common pitfalls
- Forgetting the deep copy for dynamic arrays, queues, or sub-objects — the single most frequent scoreboard defect.
- Mixing stimulus and control — putting a
vifhandle or awaitinside the item. Items are timeless data. - Macro overuse —
UVM_ALL_ONon every field inflates simulation time and log noise; turn offUVM_COMPAREorUVM_PRINTbits where they add nothing. - Not overriding
convert2string()— a one-line, human-readable dump beats paging throughuvm_printeroutput during triage.
For teams building real silicon, clean transaction modelling is also the foundation for reuse across blocks — an approach we apply in production at Avecas VLSI design services. If you want to go further into reusable stimulus layers, the UVM virtual sequence article is the natural next step, and the full curriculum lives on our courses page.
Ready to build a scoreboard that catches real bugs? Join the CourseTron UVM Verification track and code every one of these do_* methods on a live AXI testbench.
Frequently asked questions
What is the difference between uvm_sequence_item and uvm_transaction?
uvm_sequence_item extends uvm_transaction and adds the handshaking fields the sequencer needs — sequence id, transaction id, and the use_response_handler plumbing. You always extend uvm_sequence_item for stimulus so items can flow through a sequencer.
Should I use the field macros or write do_copy/do_compare by hand?
Macros are fine for config objects and low-frequency items. For high-throughput transactions, hand-written do_* methods run faster and give you exact control, which is why most mature verification teams write them explicitly.
Why does my randomization produce the same values every run?
Usually the seed is fixed. Change the simulator seed (+ntb_random_seed on VCS, equivalents on Xcelium/Questa), confirm the fields are actually rand, and verify randomize() returned 1 rather than silently failing a constraint.
Does a sequence item consume simulation time?
No. It is a data object with no clock or ports. Only the driver advances time when it converts the item into pin activity.