Module 1011 min

UVM Sequences and the Sequencer

How stimulus is generated, arbitrated, and layered

In the big picture the sequence was just "the script of what to test". Now you need the real mechanics, because this is where most UVM stimulus bugs and most interview questions live. The driver does not invent transactions; it asks for them. The thing it asks is a sequence, routed through a sequencer. The sequence item carries one transaction, the sequence (a uvm_sequence with a body task) decides which items to send and in what order, and the sequencer arbitrates between sequences and hands items to the driver.

The body task and the item handshake

A sequence does its work inside body(). For each item it follows a fixed four-step handshake with the sequencer: create the item, ask permission to send it (start_item), randomize it, then finish (finish_item). The finish_item call blocks until the driver has called item_done, which is what naturally paces the stimulus to the DUT.

systemverilog
class write_seq extends uvm_sequence #(my_item);
  `uvm_object_utils(write_seq)
  function new(string name = "write_seq"); super.new(name); endfunction

  task body();
    my_item req;
    repeat (10) begin
      req = my_item::type_id::create("req"); // factory create
      start_item(req);                       // request the bus from sequencer
      if (!req.randomize() with { kind == WRITE; })
        `uvm_error("SEQ", "randomize failed")
      finish_item(req);                      // blocks until driver item_done
    end
  endtask
endclass

Virtual sequences - coordinating many interfaces

The `uvm_do family of macros collapses create plus start_item plus randomize plus finish_item into one line, but writing it out once is the only way to understand what those macros expand to. A real chip also has several interfaces, each with its own sequencer (one for the bus, one for the interrupt line, one for config). To run a scenario that spans all of them in a controlled order you write a virtual sequence: it owns handles to the individual sequencers (gathered on a virtual sequencer) and starts sub-sequences on each, choreographing the whole environment rather than a single interface.

systemverilog
class top_vseq extends uvm_sequence;
  `uvm_object_utils(top_vseq)
  bus_sequencer bus_sqr;   // handles to the real sequencers, set by the test
  irq_sequencer irq_sqr;
  function new(string name = "top_vseq"); super.new(name); endfunction

  task body();
    config_seq  cfg = config_seq::type_id::create("cfg");
    traffic_seq tr  = traffic_seq::type_id::create("tr");
    cfg.start(bus_sqr);              // configure first, on the bus sequencer
    fork
      tr.start(bus_sqr);            // then drive traffic ...
      irq_seq::type_id::create("i").start(irq_sqr); // ... while irq fires
    join
  endtask
endclass
Pro tip

A virtual sequence carries no sequencer of its own to drive a DUT directly; its power is purely orchestration. When asked "how do you coordinate two interfaces", the answer is a virtual sequence holding handles to both sequencers, calling start() on each, and using fork-join to overlap them when the scenario needs concurrency.

Watch out

Do not confuse start_item arbitration with item ordering inside one sequence. Within a single body() the items are strictly in order. But if several sequences run on the same sequencer at once, the sequencer arbitrates between their start_item requests using a policy (SEQ_ARB_FIFO by default), so items from different sequences can interleave. If interleaving corrupts your scenario, raise priority or take the sequencer with lock() or grab().