UVM-12: Sequences — VLSI Trainers
VLSI Trainers UVM Series · UVM-12
UVM Series · UVM-12

Sequences

The sequence as a transient object — body() task, the start_item/finish_item handshake, how sequences start on a sequencer, rand fields in sequences, sub-sequences, the API sequence pattern, linear and parallel execution, and sequence hierarchy from atomic to test-level.

📋 What a Sequence Is

A sequence is a transient object that encapsulates a unit of stimulus. It is created, its body() task runs, it generates sequence items and sends them to the driver, and then it goes out of scope. Unlike a component, it has no phases and no fixed place in the component hierarchy.

Sequences solve the fundamental problem of test reuse: the same driver can execute any sequence — a single write, a burst, a complete register initialisation, or a randomised traffic generator — without knowing in advance which one it will receive. The sequencer arbitrates between concurrent sequences and presents one item at a time to the driver.

In terms of class inheritance, uvm_sequence extends uvm_sequence_item which extends uvm_object. This means a sequence is itself a uvm_object — it can be randomised, copied, and passed around just like any other object.

Sequence Lifetime vs Component Lifetime uvm_component — Persistent (whole simulation) build_phase ── connect_phase ── run_phase ────────────────── check_phase Component exists for entire simulation from elaboration to $finish Driver, Monitor, Agent, Env, Test, Scoreboard Has phases · Has parent · Has dot-path name uvm_sequence — Transient (during run_phase only) ─── created ─── body() runs ─── GC’d ─── Created on demand inside test’s run_phase No phases · No parent · No dot-path name Extends uvm_object — is randomisable vlsitrainers.com
Figure 1 — Sequence lifetime vs component lifetime. A component exists for the entire simulation; it has phases, a parent, and a path. A sequence is transient — created during run_phase, body() executes, then it goes out of scope. This transience is what makes sequences flexible: any number of different sequences can execute on the same driver without changing the testbench structure.

📋 Sequence Anatomy

Every sequence has the same basic structure. The only required override is body() — everything else has a default implementation.

class apb_write_seq extends uvm_sequence #(apb_seq_item);
  `uvm_object_utils(apb_write_seq)

  // ── Optional: rand fields control sequence behaviour ────
  rand int unsigned num_transfers = 10;
  rand logic [11:0] target_addr;

  constraint num_c   { num_transfers inside {[1:100]}; }
  constraint addr_c  { target_addr   inside {12'h000, 12'h004, 12'h008}; }

  function new(string name = "apb_write_seq");
    super.new(name);
  endfunction

  // ── body() is the ONLY required override ────────────────
  task body();
    apb_seq_item item;
    repeat(num_transfers) begin
      item = apb_seq_item::type_id::create("item");
      start_item(item);
      if (!item.randomize() with {
            read_not_write == 0;          // force write
            addr           == target_addr;  // use sequence field
          })
        `uvm_fatal("SEQ", "Randomise failed")
      finish_item(item);
    end
  endtask
endclass

Key properties of body():

📋 start_item / finish_item Handshake

Every item sent from a sequence to the driver goes through a two-step handshake. Understanding what each call does is critical for debugging stuck simulations.

start_item / finish_item — Sequence-Sequencer-Driver Handshake Sequence (body) Sequencer Driver start_item(item) request arbitration wait for driver ready grant (blocks until driver calls get_next_item) randomize(item) (after grant — free to randomise) finish_item(item) send item to driver get_next_item → drive → item_done item_done() — finish_item unblocks vlsitrainers.com
Figure 2 — The start_item/finish_item/get_next_item/item_done handshake. start_item requests access from the sequencer and blocks until the driver is ready. After the grant, the sequence randomises the item. finish_item sends the item and blocks until the driver calls item_done(). Only then does body() continue to the next iteration.
CallWho callsWhat it doesBlocks?
start_item(item)Sequence body()Requests sequencer arbitration — waits until the driver calls get_next_item()Yes — until driver is ready
finish_item(item)Sequence body()Delivers item to driver — waits until driver calls item_done()Yes — until driver finishes
get_next_item(item)Driver run_phaseRetrieves the next ready item from the sequencer — grants the start_item() blockYes — until sequence calls finish_item()
item_done()Driver run_phaseSignals the sequencer that the item is consumed — unblocks finish_item()No — returns immediately
Randomise the item between start_item() and finish_item() — not before start_item(). start_item() calls pre_do() on the item, which may modify constraints. Randomising before start_item() means those modifications are not applied. The correct order is always: create → start_item → randomise → finish_item.

📋 Starting a Sequence

A sequence is started with its start() method. This associates the sequence with a sequencer, then calls body(). The caller blocks until body() completes.

// ── Basic: start on a sequencer, block until done ────────
seq.start(m_env.m_agent.m_seqr);

// ── With parent sequence (for proper response routing) ───
// Always pass 'this' as parent when starting from body() of another seq
child_seq.start(m_sequencer, this);

// ── start() signature ─────────────────────────────────────
// task start(
//   uvm_sequencer_base sequencer,  // target sequencer
//   uvm_sequence_base  parent = null, // parent sequence (for response routing)
//   int                this_priority = -1, // arbitration priority (-1 = inherit)
//   bit                call_pre_post = 1   // whether to call pre/post_body
// );

// ── Starting from test run_phase ─────────────────────────
task run_phase(uvm_phase phase);
  apb_write_seq seq;
  phase.raise_objection(this);

  seq = apb_write_seq::type_id::create("seq");
  if (!seq.randomize() with { num_transfers == 20; })
    `uvm_fatal("TEST", "Seq randomise failed")
  seq.start(m_env.m_agent.m_seqr);   // blocks until seq completes

  phase.drop_objection(this);
endtask

📋 Rand Fields in Sequences

A sequence can contain rand fields that control its behaviour — how many items to generate, what address range to target, what data patterns to use. Randomising the sequence before starting it changes the whole scenario the sequence generates, without changing the sequence code.

class mem_fill_seq extends uvm_sequence #(apb_seq_item);
  `uvm_object_utils(mem_fill_seq)

  // ── Rand fields control this sequence's behaviour ───────
  rand logic [11:0] start_addr;
  rand logic [11:0] end_addr;
  rand int unsigned num_writes;
  rand logic [31:0] fill_value;

  constraint range_c {
    end_addr > start_addr;
    num_writes inside {[1:256]};
  }

  function new(string name = "mem_fill_seq");
    super.new(name);
  endfunction

  task body();
    apb_seq_item item;
    logic [11:0] addr = start_addr;
    repeat(num_writes) begin
      item = apb_seq_item::type_id::create("item");
      start_item(item);
      item.randomize() with {
        read_not_write == 0;
        addr           == local::addr;
        write_data     == local::fill_value;
        byte_en        == 4'b1111;
      };
      finish_item(item);
      addr += 4;
    end
  endtask
endclass

// ── In test: randomise before starting ────────────────────
task run_phase(uvm_phase phase);
  mem_fill_seq seq;
  phase.raise_objection(this);
  seq = mem_fill_seq::type_id::create("seq");
  seq.randomize() with { start_addr == 12'h000; num_writes == 10; };
  seq.start(m_env.m_agent.m_seqr);
  phase.drop_objection(this);
endtask

📋 Sub-Sequences

A sequence can start other sequences inside its body(). This is equivalent to a function calling a subroutine — it allows complex behaviour to be built from simple reusable pieces. Sub-sequences always pass this as the parent sequence for proper response routing.

class gpio_init_seq extends uvm_sequence #(apb_seq_item);
  `uvm_object_utils(gpio_init_seq)
  function new(string name="gpio_init_seq"); super.new(name); endfunction

  task body();
    apb_write_seq wr_seq;
    apb_read_seq  rd_seq;

    // ── Sequential sub-sequences ──────────────────────────
    // Start write seq — blocks until it completes
    wr_seq = apb_write_seq::type_id::create("wr_seq");
    wr_seq.target_addr = 12'h004;  // set DIR register
    wr_seq.start(m_sequencer, this);  // pass 'this' as parent

    // Start read seq to verify — blocks until it completes
    rd_seq = apb_read_seq::type_id::create("rd_seq");
    rd_seq.target_addr = 12'h004;
    rd_seq.start(m_sequencer, this);

    if (rd_seq.read_data != 32'hFFFF0000)
      `uvm_error("SEQ", "DIR register mismatch!")
  endtask
endclass
Pass this as the parent when calling start() from inside body(). The parent argument tells the sequencer how to route response items back to the correct sequence. Without it, response routing breaks in bidirectional protocols where the driver returns data to the sequence.

📋 Linear and Parallel Execution

Sub-sequences can run in any order. Linear (sequential) execution is the default — each sub-sequence completes before the next starts. Parallel execution uses SystemVerilog fork/join to run multiple sub-sequences simultaneously on the same or different sequencers.

// ── Linear (sequential) — default ────────────────────────
task body();
  seq_a.start(m_sequencer, this);   // A completes
  seq_b.start(m_sequencer, this);   // then B starts
endtask

// ── Parallel (fork/join) — both run simultaneously ────────
task body();
  fork
    seq_a.start(m_sequencer, this);
    seq_b.start(m_sequencer, this);
  join        // wait for BOTH to finish
endtask

// ── fork/join_any — continue when FIRST finishes ──────────
task body();
  fork
    seq_a.start(m_sequencer, this);
    timeout_seq.start(m_sequencer, this);
  join_any    // whichever finishes first
  disable fork;  // WARNING: use with care — see pitfalls section
endtask
Never use fork / join_any / disable fork with sequences unless the cancelled sequences have completed all their item handshakes. Killing a sequence mid-handshake (between start_item and finish_item, or between get_next_item and item_done) permanently locks the sequencer. The locked state cannot be recovered — the simulation must be restarted. If you need a timeout pattern, implement it outside the sequence architecture (e.g., a parallel monitor that sets a flag).

📋 Sequence Hierarchy Pattern

Well-structured sequence libraries follow a three-level hierarchy. Each level builds on the one below it, and any level can be used standalone by a test.

Three-Level Sequence Hierarchy Level 3 — Test / Virtual Sequence controls overall scenario gpio_test1_seq: init → randomise → stress → check Calls worker sequences in order/parallel on correct sequencers Level 2 — Worker Sequence task-level operation gpio_init_seq: write DIR reg, write IEN reg, verify readback Calls API sequences; handles errors; single sequencer Level 1 — API Sequence one item, one transfer apb_write_seq: create item → start_item → randomize → finish_item Returns response in item fields; reused by all worker sequences vlsitrainers.com
Figure 3 — Three-level sequence hierarchy. Level 1 (API): one atomic bus operation — create item, send, return response. Level 2 (Worker): a meaningful DUT operation built from API calls — configure a register block, fill memory. Level 3 (Test/Virtual): the complete test scenario — initialise, stress, verify. Each level is independently reusable.

📋 API Sequence Pattern

The API sequence pattern creates a task wrapper around the start_item/finish_item handshake, giving worker sequences a clean functional interface instead of raw item manipulation. This is the recommended style for reusable protocol VIP.

// ── API sequence: one write operation ─────────────────────
class apb_write_seq extends uvm_sequence #(apb_seq_item);
  `uvm_object_utils(apb_write_seq)

  rand logic [11:0] addr;
  rand logic [31:0] data;
  rand logic [3:0]  strobe;

  function new(string name="apb_write_seq"); super.new(name); endfunction

  task body();
    apb_seq_item req;
    req = apb_seq_item::type_id::create("req");
    start_item(req);
    req.randomize() with {
      read_not_write == 0;
      addr           == local::addr;
      write_data     == local::data;
      byte_en        == local::strobe;
    };
    finish_item(req);
  endtask

  // ── Convenience task — hides start() from callers ────────
  task write(
    input logic [11:0]          addr,
    input logic [31:0]          data,
    input uvm_sequencer_base    seqr,
    input uvm_sequence_base     parent = null,
    input logic [3:0]           strobe = 4'b1111
  );
    this.addr   = addr;
    this.data   = data;
    this.strobe = strobe;
    this.start(seqr, parent);
  endtask
endclass

// ── Worker calls the convenience task — clean interface ───
task body();
  apb_write_seq wr = apb_write_seq::type_id::create("wr");
  wr.write(12'h004, 32'hFFFF0000, m_sequencer, this);  // set DIR reg
  wr.write(12'h008, 32'h0000000F, m_sequencer, this);  // set IEN reg
endtask

📋 Common Sequence Pitfalls

PitfallSymptomFix
Randomising item before start_item()pre_do() modifications not applied. Constraints may be wrong.Always: create → start_item → randomise → finish_item. Never randomise before start_item.
Missing item_done() in driverfinish_item() never returns. Sequence hangs forever. Simulation timeout.Every get_next_item() must be paired with exactly one item_done().
Forgetting this as parent in sub-sequence start()Response items lost. Bidirectional protocols silently fail.Always child_seq.start(m_sequencer, this) when calling from inside body().
fork/join_any then disable forkSequencer deadlock. Simulation appears to run but no more items delivered.Never use disable fork with active sequences. Let all forked sequences complete naturally.
Using null as sequencer in start()Null pointer dereference. Fatal runtime error.Always pass a valid sequencer handle to start(). Retrieve it from the component hierarchy before calling start().
Starting sequence with no sequencer handleNull handle crash or compilation error.Test’s run_phase must have a valid path to the sequencer: m_env.m_agent.m_seqr.
Running the same sequence instance twiceRace condition — sequence state is not reset between runs.Create a new sequence instance for each start(). Do not reuse the same handle.

📋 Quick Reference

ItemKey fact
Base classuvm_sequence #(REQ, RSP = REQ) extends uvm_sequence_item → uvm_object
Registration`uvm_object_utils(ClassName) — sequences are objects, not components
Constructorfunction new(string name = "classname") — no parent
body() typeTask — can consume simulation time and call blocking operations
Correct item creation ordercreate() → start_item() → randomize() → finish_item()
start_item() blocks untilDriver calls get_next_item() — sequencer grants access
finish_item() blocks untilDriver calls item_done()
start() signatureseq.start(sequencer, parent_seq, priority, call_pre_post)
Parent in sub-sequencesAlways pass this: child.start(m_sequencer, this)
m_sequencerHandle to the sequencer this sequence is running on — available in body()
Rand seq fieldsDeclare fields as rand in the sequence class, randomise the sequence before start()
fork/join warningNever kill a sequence mid-handshake — locks sequencer permanently
Level 1 — API seqOne atomic operation: start_item → randomize → finish_item. Returns response.
Level 2 — Worker seqMeaningful DUT operation built from API sequence calls
Level 3 — Test seqFull test scenario: calls worker sequences in order or parallel
Scroll to Top