Skip to main content

Virtual Sequences & Sequencers

Every sequence throughout this section has targeted exactly one sequencer, driving exactly one interface. Real environments often outgrow that — imagine the FIFO testbench extended with a second, simple control interface (a mode-select signal, say) that has to be configured before certain FIFO operations run. Neither fifo_write_read_seq nor a hypothetical control sequence can coordinate that ordering on its own, because neither one can see the other's sequencer. Virtual sequences exist specifically to coordinate stimulus across more than one sequencer at once.

There is no distinct "virtual sequencer" class​

The most important fact to internalize before anything else: uvm_virtual_sequencer doesn't exist as a real class. A "virtual sequencer" is an ordinary uvm_sequencer — usually parameterized on nothing meaningful, or on a placeholder type — used purely by convention, holding plain member-variable handles to the real sequencers it needs to coordinate:

class fifo_virtual_sequencer extends uvm_sequencer;
`uvm_component_utils(fifo_virtual_sequencer)

uvm_sequencer #(fifo_txn) fifo_seqr;
uvm_sequencer #(ctrl_txn) ctrl_seqr;

function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass

These handles get wired up in the environment's connect_phase, the same pattern used for every other TLM/sequencer connection in this section:

function void fifo_soc_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
v_seqr.fifo_seqr = fifo_agent.sequencer;
v_seqr.ctrl_seqr = ctrl_agent.sequencer;
endfunction

Reaching sub-sequencers with p_sequencer​

A virtual sequence needs a typed handle to the virtual sequencer above, so it can reach fifo_seqr/ctrl_seqr without casting. `uvm_declare_p_sequencer generates exactly that:

class fifo_config_then_write_seq extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(fifo_config_then_write_seq)
`uvm_declare_p_sequencer(fifo_virtual_sequencer)

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

task body();
ctrl_mode_seq cfg_seq = ctrl_mode_seq::type_id::create("cfg_seq");
fifo_write_read_seq wr_seq = fifo_write_read_seq::type_id::create("wr_seq");

cfg_seq.start(p_sequencer.ctrl_seqr); // configure mode first, and wait for it to finish
wr_seq.start(p_sequencer.fifo_seqr); // only then start driving FIFO writes
endtask
endclass
  • `uvm_declare_p_sequencer(fifo_virtual_sequencer) generates the typed p_sequencer member used throughout body() — without it, the sequence would only have the generic, untyped sequencer handle every uvm_sequence gets by default (named m_sequencer, typed as the base uvm_sequencer_base), with no fifo_seqr/ctrl_seqr members visible on it. p_sequencer and m_sequencer end up pointing at the same object — the macro's whole job is producing a typed alias to it.
  • Sequential by default, concurrent when wrapped in fork/join. The example above runs cfg_seq to completion before starting wr_seq — exactly the ordering guarantee this whole scenario needed. Wrapping both start() calls in fork ... join instead would run them concurrently, appropriate when two sequencers genuinely need to be driven at the same time rather than in sequence.
  • This virtual sequence is started on the virtual sequencer itself, from a test: seq.start(env.v_seqr) — not on fifo_seqr or ctrl_seqr directly, since its own body() is what dispatches to each real sequencer in turn.
Defensive null-checking

Before dereferencing p_sequencer.fifo_seqr (or any sub-sequencer handle), checking it against null first is worth the extra line — a passive agent (see The Agent & Environment) never builds a sequencer at all, and a virtual sequence that assumes one exists will fail with a much less obvious error if the environment configuration doesn't match what the sequence expects.

Two ways to structure the coordination​

The pattern above routes everything through a dedicated virtual sequencer class. The alternative — embedding direct sequencer handles inside the virtual sequence itself, skipping a separate virtual sequencer entirely — trades away the p_sequencer/ `uvm_declare_p_sequencer indirection for simplicity, at the cost of the virtual sequence now needing to be told its sequencer handles some other way (typically via uvm_config_db, from Resource DB & Config DB) rather than inheriting them automatically. The virtual-sequencer approach shown above scales better once more than a couple of sequencers are involved, or once virtual sequencers start nesting inside larger virtual sequencers for multi-level coordination — genuinely large SoC-level environments do this, though it's well beyond what the FIFO testbench in this section needs.

What's next​

Coordinating across sequencers is one axis of complexity. The next page covers coordinating within one sequencer — organizing many sequences into a reusable, selectable group, and controlling which one wins when several are competing for the same driver at once.