UVM-11: Sequence Items — VLSI Trainers
VLSI Trainers UVM Series · UVM-11
UVM Series · UVM-11

Sequence Items

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.

📋 What a Sequence Item Is

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.

Sequence Item — Position in the Stimulus Flow Sequence body() creates and randomises items Seq Item ★ rand addr, data, cmd status, response fields Sequencer arbitrates and queues items Driver reads item fields drives DUT signals create start_item finish_item get_next_item item_done response data populated by driver → returned to sequence vlsitrainers.com
Figure 1 — Sequence item in the stimulus flow. The sequence creates and randomises items, sends them through the sequencer to the driver (via start_item/finish_item), the driver drives DUT signals and populates response fields, then the item is returned to the sequence with the response data.

📋 Four Field Categories

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.

Control

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.

Payload

The actual data content of the transfer. write_data, address, byte enables. Always rand — sequences constrain and randomise payload for data coverage.

Configuration

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.

Analysis / Response

Populated by the driver or monitor after the transfer completes. read_data, error_status, timing measurements. Never rand — must not be corrupted by randomisation.

📋 rand vs non-rand Convention

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.

If a response field is declared 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.
rand vs non-rand — Request and Response Field Separation Request Fields — rand ✓ rand logic [31:0] addr; rand logic [31:0] write_data; rand logic read_not_write; Sequence randomises → driver reads → DUT driven Response Fields — NOT rand ✓ logic [31:0] read_data; // no rand logic error_status; // no rand time start_time; // no rand Driver populates after DUT responds → sequence reads vlsitrainers.com
Figure 2 — rand vs non-rand separation. Request fields (left, green) are rand — they are randomised by sequences and read by the driver. Response fields (right, red) are plain logic — they are populated by the driver after the DUT responds and read by the sequence or scoreboard. Never mark response fields as rand.

📋 The `uvm_field Macros

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:

FlagMeaning
UVM_ALL_ONEnable this field for all operations: copy, compare, print, pack, unpack
UVM_NOCOPYExclude from do_copy()
UVM_NOCOMPAREExclude from do_compare() — useful for timestamp fields
UVM_NOPRINTExclude from do_print()
UVM_NORANDExclude from randomisation — use for response fields
UVM_HEXPrint in hexadecimal (default for integers)
UVM_DECPrint in decimal
UVM_BINPrint in binary
MacroField 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
Field macros have a performance cost — for very high-throughput environments consider manual methods. Each macro call adds code to the utility methods. With thousands of items per second, the overhead of automated do_compare() and do_print() can be measurable. For most designs the macros are the right choice. Profile before switching to manual implementations.

📋 Manual Utility Methods

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
convert2string() is the most useful method to implement. It is called by `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

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
Keep base constraints minimal and permissive. Base constraints should only enforce protocol legality — not test-specific scenarios. Tight base constraints make it hard to write tests that explore corner cases. Reserve tight constraints for inline randomisation inside specific sequences. This keeps the item reusable across many different test scenarios.

📋 Request and Response Fields

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

📋 Complete APB Sequence Item

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

📋 How Items Are Used in Sequences

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

📋 Quick Reference

ItemKey fact
Base classuvm_sequence_item → uvm_transaction → uvm_object
Registration macro`uvm_object_utils(ClassName) or `uvm_object_utils_begin/end with field macros
Constructorfunction new(string name = "classname") — no parent argument
Request fieldsDeclare as rand — sequences randomise and constrain them
Response fieldsNever rand — driver populates after DUT responds
Field categoriesControl, Payload, Configuration (rand) + Analysis/Response (non-rand)
`uvm_field_int flagsUVM_ALL_ON, UVM_NORAND, UVM_NOCOMPARE, UVM_NOCOPY, UVM_HEX, UVM_DEC, UVM_BIN
do_copy must callsuper.do_copy(rhs) first, then $cast rhs to correct type
do_compare excludesResponse fields — compare only request fields for matching
convert2stringMost useful method — implement for readable log output and scoreboard mismatch messages
Base constraintsProtocol legality only — not test-specific. Keep permissive.
Inline constraintsAdded 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
Scroll to Top