Skip to main content

The Sequencer & Sequence

The driver's get_next_item()/item_done() loop from two pages ago was written without ever explaining where a transaction actually comes from. That's this page's subject: the sequencer, which sits between stimulus generation and the driver, and the sequence, which decides what stimulus to generate in the first place.

Why this is two components, not one​

It would be simpler, on paper, for the driver to just generate its own random transactions directly. The reason UVM splits this into a separate sequence and sequencer instead comes down to reuse: a driver that generates its own stimulus can only ever run one kind of test. Separating "what to send" (the sequence) from "how to physically send it" (the driver) means the same driver can be reused, unmodified, under an all-writes sequence, an all-reads sequence, an error-injection sequence, or anything else — and the same sequence's logic doesn't change if the driver underneath it is later swapped via the factory (see The UVM Factory).

The sequencer: a pull-model middleman​

uvm_sequencer #(fifo_txn) sequencer;

uvm_sequencer is generic enough that it rarely needs subclassing for a simple environment like this one — parameterizing it with fifo_txn is usually enough. It's a pull model: the driver asks the sequencer for work (get_next_item(), from the previous page), rather than the sequencer pushing transactions at the driver on its own schedule. That direction matters — it means the driver controls pacing (one transaction per however many clock cycles it takes to drive one), not the sequence.

The sequence: deciding what to send​

class fifo_write_read_seq extends uvm_sequence #(fifo_txn);
`uvm_object_utils(fifo_write_read_seq)

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

task body();
fifo_txn txn;

repeat (4) begin
txn = fifo_txn::type_id::create("txn");
start_item(txn);
if (!txn.randomize() with { op == WRITE; })
`uvm_error("SEQ", "Randomization failed")
finish_item(txn);
end

repeat (4) begin
txn = fifo_txn::type_id::create("txn");
start_item(txn);
if (!txn.randomize() with { op == READ; })
`uvm_error("SEQ", "Randomization failed")
finish_item(txn);
end
endtask
endclass
  • uvm_sequence #(fifo_txn), extended the same way uvm_driver was parameterized on the previous page — sequence and driver agree on a transaction type independently, with the sequencer as the only thing that has to know about both.
  • `uvm_object_utils, not `uvm_component_utils — a sequence is transient, created and run, not a permanent fixture of the component tree, so it follows Base Classes' object branch, same as fifo_txn itself.
  • body() is where all of a sequence's actual logic lives — a task, so it can pace itself against however long each transaction actually takes to drive, rather than generating everything instantaneously and dumping it into a queue.
  • start_item(txn) / finish_item(txn) is the sequence-side half of the handshake whose driver-side half (get_next_item()/item_done()) appeared on the previous page. start_item() blocks until the sequencer is ready to accept a new item; finish_item() blocks until the driver has actually finished driving it (signaled by the driver's item_done()) — which is what makes this sequence's repeat loops naturally throttle to the driver's real pace instead of racing ahead of it.
  • Randomizing between start_item() and finish_item(), not before — this ordering is deliberate, not incidental: it's what lets the sequencer's arbitration (which transaction from which sequence goes next, if more than one is active) happen before this specific transaction's contents are locked in.

Starting a sequence​

A sequence doesn't run on its own — something has to construct it and hand it a sequencer:

fifo_write_read_seq seq = fifo_write_read_seq::type_id::create("seq");
seq.start(env.agent.sequencer);

This line — typically issued from a test's run_phase, shown in full on the Test & Testbench-Top page — is the moment stimulus generation actually begins. Everything before it (driver, monitor, sequencer, the sequence class itself) is just structure sitting idle until something calls start().

An alternative to this explicit construct-and-start() pattern is a default sequence, configured through the same uvm_config_db mechanism used throughout this section rather than written into run_phase by hand:

uvm_config_db#(uvm_object_wrapper)::set(this, "env.agent.sequencer.run_phase",
"default_sequence", fifo_write_read_seq::type_id::get());

Set before the sequencer's run_phase begins (typically in a test's build_phase), this tells the sequencer to automatically construct and start an instance of the named sequence type on its own, with no explicit start() call anywhere. It's most useful when a base test wants a subclass to be able to swap in a different default sequence purely through configuration — via a factory override on the sequence type, or a different config_db value — without touching or overriding run_phase at all.

What's next​

Transactions are now being generated, driven, and independently observed. The next page adds the piece that actually judges whether any of it was correct — the scoreboard.