Designing the foundation of all UVM stimulus — the four field categories, rand vs non-rand convention, the `uvm_field automation macros, implementing do_copy / do_compare / do_print / convert2string manually, constraints, and the complete APB sequence item as a worked example.
A sequence item (transaction) is the fundamental data object that flows between a sequence and a driver. It carries all the information the driver needs to execute one atomic bus transfer, and all the information the monitor needs to report one observed transfer. It is the shared language of the stimulus generation flow.
Sequence items extend uvm_sequence_item, which in turn extends uvm_transaction and uvm_object. Being a uvm_object, a sequence item has no phases and no parent — it is created on demand inside a sequence body, passed to the driver, used, and then garbage-collected.
Every field in a sequence item belongs to one of four functional categories. Understanding which category a field belongs to determines whether it should be rand, and whether the monitor or the driver populates it.
Determines what type of transaction to perform. read_not_write, transfer type, burst mode, data width selection. Always rand — sequences randomise control fields to generate different transfer types.
The actual data content of the transfer. write_data, address, byte enables. Always rand — sequences constrain and randomise payload for data coverage.
Sets up protocol behaviour for this and subsequent transfers. Error injection mode, flow control settings. Often rand with tight constraints, or set directly by sequences.
Populated by the driver or monitor after the transfer completes. read_data, error_status, timing measurements. Never rand — must not be corrupted by randomisation.
The most important design rule for sequence items: request fields are rand, response fields are not.
Request fields carry stimulus information from the sequence to the driver — they must be randomisable so sequences can generate varied traffic. Response fields carry result information from the driver back to the sequence — they must not be randomised because they contain the actual DUT response that the sequence (and scoreboard) will use.
rand, randomising the item inside a sequence will corrupt the DUT response. After the driver populates read_data, any subsequent item.randomize() call will overwrite it with a random value. Even if no explicit randomise is called, certain UVM internal operations (like clone() followed by randomize()) can corrupt rand fields. Always mark response fields as plain logic, never rand logic.
The `uvm_field_* macros, placed inside a `uvm_object_utils_begin/end block, automatically implement do_copy(), do_compare(), do_print(), do_pack(), and do_unpack() for each registered field. This eliminates repetitive boilerplate for standard utility methods.
class apb_seq_item extends uvm_sequence_item; `uvm_object_utils_begin(apb_seq_item) `uvm_field_int(addr, UVM_ALL_ON) `uvm_field_int(write_data, UVM_ALL_ON) `uvm_field_int(read_not_write, UVM_ALL_ON) `uvm_field_int(byte_en, UVM_ALL_ON) `uvm_field_int(read_data, UVM_ALL_ON | UVM_NORAND) `uvm_field_int(error, UVM_ALL_ON | UVM_NORAND) `uvm_object_utils_end // ... field declarations and constraints below endclass
The second argument to each macro is a set of flag bits that control which operations include this field:
| Flag | Meaning |
|---|---|
UVM_ALL_ON | Enable this field for all operations: copy, compare, print, pack, unpack |
UVM_NOCOPY | Exclude from do_copy() |
UVM_NOCOMPARE | Exclude from do_compare() — useful for timestamp fields |
UVM_NOPRINT | Exclude from do_print() |
UVM_NORAND | Exclude from randomisation — use for response fields |
UVM_HEX | Print in hexadecimal (default for integers) |
UVM_DEC | Print in decimal |
UVM_BIN | Print in binary |
| Macro | Field type |
|---|---|
`uvm_field_int(F, FLAGS) | Integral types: logic, bit, int, longint, byte, enum |
`uvm_field_string(F, FLAGS) | string type |
`uvm_field_real(F, FLAGS) | real / shortreal |
`uvm_field_object(F, FLAGS) | uvm_object handles (nested objects) |
`uvm_field_array_int(F, FLAGS) | Dynamic arrays of integral types |
`uvm_field_queue_int(F, FLAGS) | Queues of integral types |
`uvm_field_enum(T, F, FLAGS) | Enumerated types — requires type T as first arg |
When you need full control over copy, compare, or print behaviour — or when field macros have performance overhead — implement the utility methods manually by overriding the virtual functions inherited from uvm_object.
class apb_seq_item extends uvm_sequence_item; `uvm_object_utils(apb_seq_item) // no _begin/end = no field macros rand logic [11:0] addr; rand logic [31:0] write_data; rand logic read_not_write; rand logic [3:0] byte_en; logic [31:0] read_data; // response — no rand logic error; // response — no rand // ── do_copy: deep-copy all fields ───────────────────── function void do_copy(uvm_object rhs); apb_seq_item rhs_; if (!$cast(rhs_, rhs)) `uvm_fatal("TYPE", "Wrong type in do_copy") super.do_copy(rhs); // copy uvm_object fields addr = rhs_.addr; write_data = rhs_.write_data; read_not_write = rhs_.read_not_write; byte_en = rhs_.byte_en; read_data = rhs_.read_data; error = rhs_.error; endfunction // ── do_compare: compare REQUEST fields only ──────────── // Exclude response fields — they are DUT outputs, not stimulus function bit do_compare(uvm_object rhs, uvm_comparer comparer); apb_seq_item rhs_; if (!$cast(rhs_, rhs)) return 0; return super.do_compare(rhs, comparer) && (addr == rhs_.addr) && (write_data == rhs_.write_data) && (read_not_write == rhs_.read_not_write) && (byte_en == rhs_.byte_en); // Note: read_data and error deliberately excluded endfunction // ── do_print: human-readable output ─────────────────── function void do_print(uvm_printer printer); super.do_print(printer); printer.print_field_int("addr", addr, 12, UVM_HEX); printer.print_field_int("write_data", write_data, 32, UVM_HEX); printer.print_field_int("read_not_write", read_not_write, 1, UVM_BIN); printer.print_field_int("byte_en", byte_en, 4, UVM_BIN); printer.print_field_int("read_data", read_data, 32, UVM_HEX); printer.print_field_int("error", error, 1, UVM_BIN); endfunction // ── convert2string: inline debug string ─────────────── function string convert2string(); return $sformatf( "[APB] %s addr=0x%03h data=0x%08h be=%04b err=%0b rd=0x%08h", read_not_write ? "READ" : "WRITE", addr, write_data, byte_en, error, read_data); endfunction endclass
`uvm_info when you use item.convert2string(), by scoreboards when reporting mismatches, and by wave viewers. A well-formatted convert2string that shows all relevant fields makes debugging dramatically faster — you can understand the full transaction at a glance from the log file.
Constraints embedded in the sequence item define the default legal space for randomisation. Sequences use inline constraints to further restrict this space for specific test scenarios. The combination of base constraints and inline constraints defines what gets generated.
class apb_seq_item extends uvm_sequence_item; `uvm_object_utils(apb_seq_item) rand logic [11:0] addr; rand logic [31:0] write_data; rand logic read_not_write; rand logic [3:0] byte_en; // ── Base constraints — always apply ──────────────────── // Only allow accesses to valid register addresses constraint valid_addr_c { addr inside {12'h000, 12'h004, 12'h008, 12'h00C, 12'h010}; } // PSTRB must have at least one lane active on writes constraint write_strobe_c { !read_not_write -> (byte_en != 4'b0000); } // Reads must have zero strobe (per APB spec) constraint read_strobe_c { read_not_write -> (byte_en == 4'b0000); } // Even distribution of read and write operations constraint rw_dist_c { read_not_write dist {0 := 50, 1 := 50}; } function new(string name = "apb_seq_item"); super.new(name); endfunction endclass // ── In sequence: inline constraints override base ───────── task body(); apb_seq_item item; item = apb_seq_item::type_id::create("item"); start_item(item); // Inline constraint forces write-only + byte 0 only item.randomize() with { read_not_write == 0; byte_en == 4'b0001; addr == 12'h000; }; finish_item(item); endtask
The same sequence item object carries both the request (filled by the sequence before sending) and the response (filled by the driver after the DUT responds). This dual-use is possible because the item handle is shared — the driver receives the same object handle that the sequence created.
// ── Driver fills response fields after DUT responds ────── task drive_transfer(apb_seq_item item); // Phase 1: use request fields to drive DUT vif.PADDR <= item.addr; vif.PWRITE <= ~item.read_not_write; vif.PWDATA <= item.write_data; vif.PSTRB <= item.byte_en; vif.PSELx <= 1; @(posedge vif.PCLK); vif.PENABLE <= 1; do @(posedge vif.PCLK); while (!vif.PREADY); // Phase 2: populate response fields from DUT outputs item.error = vif.PSLVERR; // response fields if (item.read_not_write) item.read_data = vif.PRDATA; vif.PSELx <= 0; vif.PENABLE <= 0; endtask // ── Sequence reads response after finish_item returns ───── task body(); apb_seq_item item; item = apb_seq_item::type_id::create("item"); start_item(item); item.randomize() with { read_not_write == 1; }; // force read finish_item(item); // blocks until driver calls item_done() // After finish_item returns, driver has populated read_data `uvm_info("SEQ", $sformatf("Read returned: 0x%08h", item.read_data), UVM_LOW) endtask
class apb_seq_item extends uvm_sequence_item; `uvm_object_utils_begin(apb_seq_item) `uvm_field_int(addr, UVM_ALL_ON) `uvm_field_int(write_data, UVM_ALL_ON) `uvm_field_int(read_not_write, UVM_ALL_ON) `uvm_field_int(byte_en, UVM_ALL_ON) `uvm_field_int(pprot, UVM_ALL_ON) `uvm_field_int(read_data, UVM_ALL_ON | UVM_NORAND) `uvm_field_int(error, UVM_ALL_ON | UVM_NORAND) `uvm_field_real(start_time, UVM_ALL_ON | UVM_NORAND | UVM_NOCOMPARE) `uvm_object_utils_end // ── Request fields (rand) ────────────────────────────── rand logic [11:0] addr; rand logic [31:0] write_data; rand logic read_not_write; // 1=read 0=write rand logic [3:0] byte_en; // PSTRB rand logic [2:0] pprot; // PPROT — security // ── Response fields (non-rand) ───────────────────────── logic [31:0] read_data; // PRDATA from DUT logic error; // PSLVERR from DUT real start_time; // timestamp (ns) // ── Constraints ──────────────────────────────────────── constraint valid_addr_c { addr inside {12'h000, 12'h004, 12'h008, 12'h00C, 12'h010}; } constraint strobe_write_c { !read_not_write -> (byte_en != 4'b0000); } constraint strobe_read_c { read_not_write -> (byte_en == 4'b0000); } constraint pprot_default_c { pprot == 3'b001; // secure privileged data (default) } constraint rw_dist_c { read_not_write dist {0 := 60, 1 := 40}; } function new(string name = "apb_seq_item"); super.new(name); endfunction function string convert2string(); return $sformatf( "APB %s @0x%03h data=0x%08h be=%04b pprot=%03b | rdata=0x%08h err=%0b", read_not_write ? "RD" : "WR", addr, write_data, byte_en, pprot, read_data, error); endfunction endclass
The standard pattern for sending one item from a sequence to the driver. This is the atomic unit of stimulus generation — everything the driver does starts with one of these cycles:
task body(); apb_seq_item item; repeat(10) begin // ① Create a new item through the factory item = apb_seq_item::type_id::create("item"); // ② Request access to sequencer — blocks until granted start_item(item); // ③ Randomise with optional inline constraints if (!item.randomize() with { read_not_write == 0; }) `uvm_fatal("SEQ", "Randomisation failed!") // ④ Record timestamp before sending item.start_time = $realtime; // ⑤ Send to driver — blocks until driver calls item_done() finish_item(item); // ⑥ After finish_item: response fields are populated `uvm_info("SEQ", item.convert2string(), UVM_MEDIUM) if (item.error) `uvm_error("SEQ", "DUT returned PSLVERR!") end endtask
| Item | Key fact |
|---|---|
| Base class | uvm_sequence_item → uvm_transaction → uvm_object |
| Registration macro | `uvm_object_utils(ClassName) or `uvm_object_utils_begin/end with field macros |
| Constructor | function new(string name = "classname") — no parent argument |
| Request fields | Declare as rand — sequences randomise and constrain them |
| Response fields | Never rand — driver populates after DUT responds |
| Field categories | Control, Payload, Configuration (rand) + Analysis/Response (non-rand) |
| `uvm_field_int flags | UVM_ALL_ON, UVM_NORAND, UVM_NOCOMPARE, UVM_NOCOPY, UVM_HEX, UVM_DEC, UVM_BIN |
| do_copy must call | super.do_copy(rhs) first, then $cast rhs to correct type |
| do_compare excludes | Response fields — compare only request fields for matching |
| convert2string | Most useful method — implement for readable log output and scoreboard mismatch messages |
| Base constraints | Protocol legality only — not test-specific. Keep permissive. |
| Inline constraints | Added in sequence body() via item.randomize() with { ... } |
| start_item() | Requests sequencer access — blocks until granted |
| finish_item() | Sends item to driver — blocks until driver calls item_done() |
| After finish_item() | Driver has populated response fields — sequence can read them immediately |