Skip to content

Sequences & Sequencers

The problem this solves

If a driver both generated stimulus and drove it onto the DUT, every new test scenario would require editing the driver. Splitting these apart means: the driver only knows how to pin one transaction onto pins, and a sequence (which can be swapped per-test, layered, and randomized independently) decides what transactions to generate and in what order. The sequencer sits between them, arbitrating when multiple sequences compete to send the driver something next.

The basic flow

class my_seq_item extends uvm_sequence_item;
  rand bit [31:0] addr;
  rand bit [31:0] data;
  `uvm_object_utils(my_seq_item)
  function new(string name = "my_seq_item");
    super.new(name);
  endfunction
endclass

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

  task body();
    my_seq_item item;
    repeat (10) begin
      item = my_seq_item::type_id::create("item");
      start_item(item);
      assert(item.randomize());
      finish_item(item);
    end
  endtask
endclass
  • start_item(item) requests the sequencer's arbitration lock — it blocks until this sequence is granted permission to drive the next item (relevant when multiple sequences run concurrently on one sequencer).
  • finish_item(item) hands the item to the driver and blocks until the driver signals it's done (item_done()), keeping the sequence and driver in lockstep.

The driver side:

class my_driver extends uvm_driver #(my_seq_item);
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);   // blocks until a sequence provides one
      drive_to_dut(req);                   // your pin-wiggling task
      seq_item_port.item_done();           // unblocks the sequence's finish_item()
    end
  endtask
endclass

get_next_item/item_done on the driver side and start_item/finish_item on the sequence side are two ends of the same handshake — they always come in matched pairs.

Starting a sequence on a sequencer

my_sequence seq = my_sequence::type_id::create("seq");
seq.start(sequencer_handle);

start() is what actually triggers body() to run. A sequence created but never .start()ed does nothing — a surprisingly common mistake when refactoring test code (the object exists, body() is defined correctly, but nothing ever calls start).

Virtual sequences: coordinating multiple interfaces

A real DUT often has several interfaces (e.g. a config bus and a data bus) that need coordinated stimulus — "write this config register, then start this data transfer." A virtual sequence doesn't drive any interface directly; it runs on a virtual sequencer and starts sub-sequences on the real sequencers underneath it:

class virtual_seq extends uvm_sequence;
  `uvm_object_utils(virtual_seq)
  virtual_sequencer v_sqr;   // has handles to cfg_sqr and data_sqr

  task body();
    cfg_seq   c = cfg_seq::type_id::create("c");
    data_seq  d = data_seq::type_id::create("d");
    c.start(v_sqr.cfg_sqr);    // runs to completion first
    d.start(v_sqr.data_sqr);   // starts only after c finishes
  endtask
endclass

This is the standard mechanism for any multi-interface ordering requirement — without it, you'd need ad-hoc cross-sequencer synchronization (events, semaphores) scattered through individual sequences, which doesn't scale past two interfaces.

Sequence layering: reusing lower-level sequences

Higher-level sequences can call lower-level ones from inside body(), building complex scenarios out of simple, independently-tested building blocks:

class burst_seq extends uvm_sequence #(my_seq_item);
  task body();
    repeat (4) begin
      my_sequence base = my_sequence::type_id::create("base");
      base.start(m_sequencer);   // reuse, don't reimplement
    end
  endtask
endclass

Prefer composing sequences over one giant sequence

A single 500-line sequence covering an entire test scenario is much harder to reuse, review, or randomly recombine than 5 small sequences of ~100 lines each, layered together. Layering also means a bug found and fixed in a low-level sequence (say, a reset sequence) automatically benefits every higher-level sequence built on it.