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 typedp_sequencermember used throughoutbody()— without it, the sequence would only have the generic, untyped sequencer handle everyuvm_sequencegets by default (namedm_sequencer, typed as the baseuvm_sequencer_base), with nofifo_seqr/ctrl_seqrmembers visible on it.p_sequencerandm_sequencerend 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 runscfg_seqto completion before startingwr_seq— exactly the ordering guarantee this whole scenario needed. Wrapping bothstart()calls infork ... joininstead 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 onfifo_seqrorctrl_seqrdirectly, since its ownbody()is what dispatches to each real sequencer in turn.
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.