Part 5 · Sequences · Intermediate

start_item / finish_item: apb_wr_seq Walkthrough

Sequence-side handshake API — start_item grant, randomize before finish_item, blocking semantics, and p_sequencer.

The four-step pattern

Every transaction in a sequence body() follows the same four steps: create item, start_item, fill item (randomize or assign), finish_item. This pattern is the most repeated code in UVM verification — mastering it is non-negotiable.

start_item negotiates with the sequencer for arbitration if multiple sequences are active. It associates req with this sequence instance for response routing. It does not randomize — you must fill the item before finish_item.

diagram
[SEQ] four-step pattern per beat

  1. req = apb_item::type_id::create("req")   [ITEM]
  2. start_item(req)                          [SEQ]  sequencer grant
  3. req.randomize() or direct assign         [ITEM] fill
  4. finish_item(req)                         [SEQ] block  [DRV]

Full apb_wr_seq walkthrough

Complete APB write sequence with p_sequencer declaration, factory create, randomize with directed write, and repeat loop:

systemverilog
class apb_wr_seq extends uvm_sequence #(apb_item);
  `uvm_declare_p_sequencer(apb_sequencer)

  rand int unsigned num_trans = 10;

  `uvm_object_utils(apb_wr_seq)

  task body();
    apb_item req;

    repeat (num_trans) begin
      // Step 1: factory create
      req = apb_item::type_id::create("req");

      // Step 2: request sequencer grant + item slot
      start_item(req);

      // Step 3: fill item — randomize or assign BEFORE finish_item
      if (!req.randomize() with { write == 1; })
        `uvm_fatal("RAND", "apb_item randomize failed")

      // Step 4: hand to driver; BLOCKS until item_done
      finish_item(req);

      // Safe to reuse req or create new one for next beat
    end
  endtask
endclass
  • uvm_declare_p_sequencer gives typed p_sequencer for lock/grab and config.

  • num_trans is a sequence knob — test sets it before start().

  • randomize() with { write==1; } forces writes in this write sequence.

  • finish_item returns only after driver item_done — entire drive duration.


What start_item does under the hood

start_item is not a simple function call — it interacts with the sequencer's arbitration state machine:

diagram
[SEQ] start_item internal effects

  1. Wait for sequencer arbitration grant (if competing sequences)
  2. Link req to this sequence instance (for response routing)
  3. Mark item as pending in sequencer FIFO
  4. Return to sequence — ready for randomize/assign

  Does NOT: randomize, call driver, or block for drive completion
  That is finish_item's job
diagram
[STIM] [SEQ] [ITEM] timeline — start_item vs finish_item

  start_item(req)     ──► fast return (arbitration wait only)
  randomize(req)      ──► [ITEM] fields set
  finish_item(req)    ──► LONG BLOCK ──► [DRV] drives entire beat
  (returns)           ──► safe to next create/randomize

pre_body and post_body hooks

  • pre_body runs before first start_item — raise objections, log banner.

  • post_body runs after last finish_item — summary, drop local state.

  • Override sparingly — most logic belongs in body().


finish_item blocking semantics

finish_item returns only after the driver calls seq_item_port.item_done() (or item_done(rsp) when using responses). Until then, the sequence thread is suspended — this is the most common source of 'sequence hang' bugs when item_done is missing.

systemverilog
// Sequence perspective — what finish_item waits for
start_item(req);
req.randomize() with { addr == 32'h4000; write == 1; data == 32'hDEAD; };
`uvm_info("SEQ", $sformatf("handing off addr=0x%08x", req.addr), UVM_MEDIUM)
finish_item(req);   // ← BLOCKS here until driver item_done
`uvm_info("SEQ", "beat complete", UVM_MEDIUM)  // prints AFTER drive
diagram
[SEQ] finish_item block duration

  finish_item called ─────────────────────────────────► returns
                     │◄── drive_apb entire duration ──►│
                     │   psel, pready wait, psel deassert │
                     [DRV] owns req fields — do not mutate
  • Do not modify req fields after finish_item until re-randomized for next beat.

  • During finish_item block, driver reads req — concurrent mutation races.

  • Log before finish_item for 'handing off'; log after for 'beat complete'.


Directed beat without randomize

Fully directed beats skip randomize — use rand_mode(OFF) and direct assignment, or assign to non-rand fields:

systemverilog
task write_reg(bit [31:0] addr, bit [31:0] data);
  apb_item req = apb_item::type_id::create("req");
  start_item(req);
  req.addr  = addr;
  req.data  = data;
  req.write = 1;
  finish_item(req);
endtask

task body();
  write_reg(32'h4000, 32'h1);   // enable
  write_reg(32'h4004, 32'hFF);  // threshold
  repeat (10) begin
    start_item(req);
    assert(req.randomize() with { write == 1; });
    finish_item(req);
  end
endtask

Sequence library hooks

Sequences intended for uvm_sequence_library should keep body() as the transaction loop and expose knobs as rand fields — num_trans, addr_range, etc. Tests configure knobs then call start().

systemverilog
// Test configures, sequence executes
apb_wr_seq seq = apb_wr_seq::type_id::create("seq");
seq.num_trans = 100;
seq.set_starting_phase(phase);  // optional phase jump
seq.start(env.apb_agent.sqr);

Key takeaways

  • Four steps: create, start_item, fill, finish_item — every beat, every protocol.

  • start_item grants slot; finish_item blocks until item_done.

  • Randomize or assign BEFORE finish_item — never after start_item without fill.

Common pitfalls

  • Calling finish_item without randomize/assign — driver gets stale item.

  • Mutating req during finish_item block — race with driver.

  • Missing uvm_declare_p_sequencer — p_sequencer is null in body().

  • Using start_item/finish_item in a virtual sequence — no driver connected.