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.
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
endclassVirtual 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.
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
endclassA 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.
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().