TO Be coming

Nov 8, 2026

UVM Course : Day 8: Virtual Sequences and Virtual Sequencers — Coordinating Multiple UVM Agents

Day 8: Virtual Sequences and Virtual Sequencers — Coordinating Multiple UVM Agents
Advertisement
Top banner — drop your 728×90 or responsive ad unit here.

Day 8: Virtual Sequences and Virtual Sequencers — Coordinating Multiple UVM Agents

A 30-day SystemVerilog/UVM verification series. Week 1 built a working testbench. Week 2 makes it smart. Today the testbench starts driving behaviour, not just stimulus.

Yesterday you had a working FIFO environment with two agents. Each could run a sequence. But they ran them independently — the write sequence did not know the read sequence existed, and there was no way to say "write three items, then read two, then verify." That is the gap virtual sequences fill.

A virtual sequence is a sequence that does not drive any interface itself. Its job is to start sub-sequences on the real sequencers of the real agents, in whatever order, at whatever time, the test dictates. That is how real testbenches produce coordinated stimulus across a bus, a memory, and an interrupt line — behaviour, not just data.


1. Why a Plain Sequence Isn't Enough

A sequence's body() calls start_item() and finish_item(). Those calls go through one sequencer, which forwards to one driver. That is perfect for "send a write to the write agent," and useless for "send a write to the write agent, wait for it to land, then send a read to the read agent."

You could, of course, write two sequences and start them from the test with a delay between the start() calls. That works exactly once. It has no way to react to a full condition, no way to wait on a signal from the read side, and no way to be reused in another test without copy-pasting the delay.

A virtual sequence fixes this by removing the sequence from the interface altogether. The virtual sequence:

  • Has no seq_item_port of its own — it does not talk to any driver.
  • Holds a handle to a virtual sequencer, which in turn holds handles to the real sequencers.
  • Starts sub-sequences on the real sequencers, waiting between them however it likes.

2. The Virtual Sequencer

A virtual sequencer is a plain uvm_sequencer. Its only job is to hold the handles to the real sequencers so that a virtual sequence can find them.

class fifo_vseqr extends uvm_sequencer;
    `uvm_component_utils(fifo_vseqr)
    fifo_sequencer wr_sqr;
    fifo_sequencer rd_sqr;
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
endclass

That is the entire class. There is nothing to override, nothing to build in build_phase. The env is what fills in the handles, and it can only do so after both agents exist — so in connect_phase:

class fifo_env extends uvm_env;
    `uvm_component_utils(fifo_env)
    fifo_agent      wr_agent;
    fifo_agent      rd_agent;
    fifo_scoreboard sb;
    fifo_vseqr      vseqr;
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        wr_agent = fifo_agent::type_id::create("wr_agent", this);
        rd_agent = fifo_agent::type_id::create("rd_agent", this);
        sb       = fifo_scoreboard::type_id::create("sb", this);
        vseqr    = fifo_vseqr::type_id::create("vseqr", this);
    endfunction
    virtual function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        // Observation path
        wr_agent.mon.ap.connect(sb.exp_imp);
        rd_agent.mon.ap.connect(sb.exp_imp);
        // Virtual sequencer handles — only for active agents
        if (wr_agent.get_is_active() == UVM_ACTIVE)
            vseqr.wr_sqr = wr_agent.sqr;
        if (rd_agent.get_is_active() == UVM_ACTIVE)
            vseqr.rd_sqr = rd_agent.sqr;
    endfunction
endclass
Why the get_is_active() guard? In passive mode the agent has no sequencer, so wr_agent.sqr is null. Assigning it unconditionally leaves the virtual sequencer holding a null handle, which then crashes the first virtual sequence that tries to start on it. Guarding means a virtual sequence can safely null-check p_sequencer.wr_sqr and skip that sub-sequence.

3. `uvm_declare_p_sequencer — The Bridge

A virtual sequence needs to reach the virtual sequencer's fields. UVM provides the macro `uvm_declare_p_sequencer to do exactly that: it declares a typed p_sequencer handle inside the sequence and casts the running sequencer to that type when start() is called.

class fifo_write_then_read_seq extends uvm_sequence #(fifo_tx);
    `uvm_object_utils(fifo_write_then_read_seq)
    `uvm_declare_p_sequencer(fifo_vseqr)
    fifo_write_seq wseq;
    fifo_read_seq  rseq;
    function new(string name = "fifo_write_then_read_seq");
        super.new(name);
    endfunction
    virtual task body();
        // Write phase
        if (p_sequencer.wr_sqr != null) begin
            wseq = fifo_write_seq::type_id::create("wseq");
            wseq.num_items = 4;
            wseq.start(p_sequencer.wr_sqr);
        end
        // Read phase
        if (p_sequencer.rd_sqr != null) begin
            rseq = fifo_read_seq::type_id::create("rseq");
            rseq.num_items = 4;
            rseq.start(p_sequencer.rd_sqr);
        end
    endtask
endclass
A note about "wait for drain." A sequence does not normally have a clock handle. The clean way to wait is either (a) put the drain delay into a uvm_event that the monitor signals when it publishes a matching transaction, or (b) let the scoreboard's check_phase assert that every write eventually has a matching read. The safest pattern in a beginner testbench is to make the read sequence longer than the write sequence — drain happens automatically as the read sequence keeps pulling. Do not put repeat (4) @(posedge clk) with a hardcoded clock handle into a sequence; it breaks reusability.

4. The Sub-Sequences

Virtual sequences start sub-sequences. Those are normal uvm_sequence classes, each bound to one sequencer. Here is a concrete write sequence:

class fifo_write_seq extends uvm_sequence #(fifo_tx);
    `uvm_object_utils(fifo_write_seq)
    rand int unsigned num_items = 10;
    function new(string name = "fifo_write_seq");
        super.new(name);
    endfunction
    virtual task body();
        repeat (num_items) begin
            `uvm_do_with(req, { req.wr_en == 1; req.rd_en == 0; })
        end
    endtask
endclass
class fifo_read_seq extends uvm_sequence #(fifo_tx);
    `uvm_object_utils(fifo_read_seq)
    rand int unsigned num_items = 10;
    function new(string name = "fifo_read_seq");
        super.new(name);
    endfunction
    virtual task body();
        repeat (num_items) begin
            `uvm_do_with(req, { req.wr_en == 0; req.rd_en == 1; })
        end
    endtask
endclass

Those are the sequences you already had in Week 1. The virtual sequence is what makes them compose.


5. The Test Starts the Virtual Sequence

Nothing about the test's structure changes. The base test still creates the env, still raises and drops objection in run_phase, and still calls start(). The only difference is which sequencer it starts on and which sequence class it creates.

class fifo_base_test extends uvm_test;
    `uvm_component_utils(fifo_base_test)
    fifo_env env;
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = fifo_env::type_id::create("env", this);
    endfunction
    virtual task run_phase(uvm_phase phase);
        fifo_write_then_read_seq vseq;
        phase.raise_objection(this);
        vseq = fifo_write_then_read_seq::type_id::create("vseq");
        vseq.start(env.vseqr);   // start on the virtual sequencer
        phase.drop_objection(this);
    endtask
endclass

Note vseq.start(env.vseqr). That is the only line that differs from Day 7.


6. When You Don't Need a Virtual Sequencer

Virtual sequencers earn their keep when a test must coordinate two or more active agents. They are overhead when:

  • Only one agent is active. Start the sequence directly on that agent's sequencer. No virtual layer.
  • The test is not coordinating stimulus — for example, a scoreboard-only test that just runs the passive monitors against a preloaded DUT.
  • The DUT has only one interface. A virtual sequence adds indirection with no payoff.
Rule of thumb: if a sequence's body() would call start() on more than one sequencer, make it virtual. If it would call start() on exactly one, keep it a normal sequence.

7. Common Virtual Sequence Bugs

SymptomCause
Null-handle crash inside body()Virtual sequencer field never assigned in env's connect_phase
p_sequencer is nullstart() was called on the wrong sequencer type, so the cast inside `uvm_declare_p_sequencer returned null
Sub-sequence runs but nothing drivesSub-sequence started on a passive agent's (nonexistent) sequencer
Read sub-sequence hangs foreverWrite sub-sequence didn't produce data, so the FIFO stays empty and the read waits
Test ends before anything happensObjection dropped before vseq.start() returned
Simulation runs foreverVirtual sequence forgot to raise or drop an objection, or the sub-sequence deadlocks on a missing handshake

8. What Day 9 Will Add

Tomorrow: Advanced Scoreboards. The FIFO scoreboard worked because write and read were distinguishable by the enable bit. Real protocols don't give you that. You will learn the two-import pattern, out-of-order comparison, and uvm_tlm_analysis_fifo — the buffering component that decouples the monitor from the scoreboard so a slow comparison never blocks a fast DUT.


9. Quiz

Sixteen questions on virtual sequences, virtual sequencers, p_sequencer, and running the same testbench on Synopsys VCS, Siemens Questa, and Cadence Xcelium. Options shuffled per load. Submit reveals all answers with explanations.

Question 1 of 16
Loading…
    0 / 16

    10. Run This Day on Synopsys VCS, Siemens Questa, or Cadence Xcelium

    The virtual sequence code from today — uvm_declare_p_sequencer, p_sequencer handles, sub-sequence start() calls — is identical on every major simulator, because UVM is an IEEE 1800.2 standard class library that Synopsys VCS, Siemens EDA's Questa, and Cadence's Xcelium all ship. Only the compile and run flow differs:

    SimulatorCompileRun
    Synopsys VCSvcs -full64 -sverilog -ntb_opts uvm-1.2 fifo_tb_top.sv./simv +UVM_TESTNAME=fifo_base_test
    Siemens Questavlog -sv -L mtiUvm +incdir+$UVM_HOME/src $UVM_HOME/src/uvm_pkg.sv fifo_tb_top.svvsim -L mtiUvm fifo_tb_top +UVM_TESTNAME=fifo_base_test
    Cadence Xceliumxrun -uvm -sv fifo_tb_top.sv +UVM_TESTNAME=fifo_base_test (single-step compile and run)
    Portable means portable. A virtual sequence that runs on the Synopsys VCS simulator runs unchanged on Questa and Xcelium. If behaviour differs between tools, suspect a race or an uninitialized signal in the testbench — not the UVM library. Use the native debuggers to step through the sequencer–driver handshake: Verdi with VCS, Questa Visualizer with the Questa simulator, or SimVision with Cadence Xcelium.
    Advertisement
    Bottom banner — drop your 728×90 or responsive ad unit here.

    Coming Up Next: Day 9 — Advanced Scoreboards

    • Two-import pattern — separate wr_exp and rd_exp imports for scoreboards where writes and reads are not distinguishable from the transaction alone.
    • Out-of-order comparison — how to compare when the DUT reorders reads, using uvm_tlm_analysis_fifo to buffer expected vs observed.
    • Analysis fifo — the standard component for decoupling a slow scoreboard from a fast monitor.
    • Reference model integration — chaining a model between the monitor and the scoreboard to produce expected transactions from observed ones.
    ← Back to the 30-Day UVM Course
    See all 30 days, week by week.

    Series: 30 days of SystemVerilog/UVM verification. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 put it in motion. Day 4 wired the plumbing. Day 5 installed the knobs. Day 6 made it modular. Day 7 made it yours. Day 8 made it coordinated.

    No comments:

    Post a Comment

    SiteMap