Verification

UVM virtual sequences

By CourseTron Team ·  8 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: A UVM virtual sequence is a top-level sequence that owns no driver of its own; it runs on a virtual sequencer that holds handles to every real sequencer in your agents, so one piece of code can start and synchronize stimulus across the whole testbench.

    Once a verification environment grows past a single interface, the hard part is no longer generating transactions on one bus — it is orchestrating several buses at once. A PCIe DUT needs a configuration write on an APB agent, then a DMA descriptor pushed on an AXI agent, then an interrupt expected back on a third interface. None of those agents knows about the others. The mechanism UVM gives you to coordinate them is the virtual sequence.

    Why a plain sequence is not enough

    A normal uvm_sequence runs on exactly one uvm_sequencer, which is bound to one driver, which drives one interface. That one-to-one chain is deliberate and clean. But a realistic scenario — "boot the chip, program four registers, then stream traffic while toggling reset" — spans interfaces that each have their own sequencer. You cannot express that from inside a single agent's sequence, because that sequence has no legal way to reach another agent's driver.

    The virtual sequence breaks the deadlock by sitting one level above the agents. It does not produce transactions directly. Instead it calls `uvm_do_on (or raw start()) on child sequences, targeting whichever real sequencer each child belongs to. The "virtual" in the name simply means it has no transaction type and no driver of its own.

    Virtual sequencer routes to real sequencers Virtual Sequence runs on Virtual Sequencer APB sequencer p_sequencer.apb_sqr AXI sequencer p_sequencer.axi_sqr IRQ sequencer p_sequencer.irq_sqr CourseTron · coursetron.com
    The virtual sequencer holds handles to each agent's real sequencer; the virtual sequence reaches them through p_sequencer.

    The virtual sequencer

    A virtual sequencer is just a uvm_sequencer that you subclass and populate with handles to the real sequencers:

    class top_vsqr extends uvm_sequencer;
      `uvm_component_utils(top_vsqr)
      apb_sequencer apb_sqr;   // handles, not instances
      axi_sequencer axi_sqr;
      irq_sequencer irq_sqr;
      function new(string name, uvm_component parent);
        super.new(name, parent); endfunction
    endclass

    Crucially the virtual sequencer does not instantiate those agents. The agents are built normally inside the environment, and in the env's connect_phase you wire the handles:

    vsqr.apb_sqr = apb_agent.sequencer;
    vsqr.axi_sqr = axi_agent.sequencer;
    vsqr.irq_sqr = irq_agent.sequencer;

    ⭐ A virtual sequencer owns no driver and parameterizes to no transaction type — extends uvm_sequencer with no type argument. If you accidentally give it a transaction type, tools like Synopsys VCS and Cadence Xcelium will still elaborate it, but you have quietly created a real sequencer that nothing drives.

    p_sequencer: the typed handle

    Inside a sequence, m_sequencer is the generic uvm_sequencer_base you are running on. You cannot reach apb_sqr through it because the base class knows nothing about your fields. The `uvm_declare_p_sequencer macro solves this by creating a typed, down-cast copy called p_sequencer:

    class cfg_then_dma_vseq extends uvm_sequence;
      `uvm_object_utils(cfg_then_dma_vseq)
      `uvm_declare_p_sequencer(top_vsqr)   // creates p_sequencer of type top_vsqr
      function new(string name="cfg_then_dma_vseq"); super.new(name); endfunction
    
      task body();
        apb_write_seq  cfg;
        axi_burst_seq  dma;
        irq_wait_seq   irq;
        `uvm_do_on(cfg, p_sequencer.apb_sqr)   // program the DUT
        `uvm_do_on(dma, p_sequencer.axi_sqr)   // kick a 256-beat burst
        `uvm_do_on(irq, p_sequencer.irq_sqr)   // block until completion IRQ
      endtask
    endclass

    The macro expands, roughly, to $cast(p_sequencer, m_sequencer) in the sequence's m_set_p_sequencer hook. That one cast is why a virtual sequence must be started on a virtual sequencer of the declared type — start it on the wrong sequencer and the cast fails at run time with a fatal, not at compile time.

    Running sequences: parallel vs. serial

    Because body() is just SystemVerilog, you choose the concurrency. The three `uvm_do_on calls above are serial. To stress the DUT with simultaneous traffic, wrap them in fork...join:

    fork
      `uvm_do_on(dma_a, p_sequencer.axi_sqr)
      `uvm_do_on(dma_b, p_sequencer.axi2_sqr)
    join

    This is where virtual sequences earn their keep. Coordinated reset injection, back-pressure during a burst, or a register poll that races against DMA completion are all just fork/join patterns over handles — and because every child sequence still arbitrates for its own driver through its own sequencer, UVM's arbitration and grab()/lock() semantics keep working per interface.

    ConceptOwns a driver?Typed handleTypical use
    Real sequenceYes (via its sequencer)n/aDrive one interface
    Virtual sequenceNop_sequencerCoordinate many interfaces
    Real sequencerConnected to driver—Arbitrate items for one driver
    Virtual sequencerNoholds sub-sequencer handlesRouting hub only
    Serial vs fork...join inside body() Serial: three uvm_do_on calls cfg: apb dma: axi irq wait Parallel: fork ... join dma_a on axi_sqr dma_b on axi2_sqr join: waits for slowest time CourseTron · coursetron.com
    Serial calls run one after another; wrapped in fork...join they overlap and the block ends when the slowest child finishes.

    A common alternative: skip the virtual sequencer

    You do not strictly need a virtual sequencer. Many modern environments give the virtual sequence direct handles to the sequencers (set via the config DB or by the test) and start it on null. This drops the p_sequencer cast entirely and is simpler to maintain, which is the style the Siemens UVM Cookbook now recommends for new code. The classic p_sequencer approach remains the one interviewers ask about and the one you will meet in legacy IP, so you must know both.

    Transaction path: sequence to driver Virtual Seq AXI Sequencer AXI Driver 1. item created 2. arbitrated 3. driven on pins tx CourseTron · coursetron.com
    Animated: a transaction leaves the virtual sequence, is routed through the AXI sequencer, and is finally consumed by the driver.

    ⭐ Interview trap: m_sequencer always points at the sequencer you were started on; p_sequencer is the same object cast to the declared type. If p_sequencer is null in your body(), you forgot `uvm_declare_p_sequencer or started on the wrong sequencer.

    For the mechanics of the underlying one-interface building block, review our companion guide on the UVM sequence and sequencer, and for timing-sensitive coordination see UVM phasing and objections. Teams that want hands-on, PDK-backed lab practice often pair this with structured training from ChipXpert's VLSI programs.

    Practical gotchas

    • Objections: raise/drop objections in the virtual sequence (or the test), not in every child — otherwise the run ends when the first child finishes.
    • Reset handling: if a child sequence can be mid-flight during reset, guard it so the virtual sequence can disable fork cleanly; dangling items leave the sequencer locked.
    • Randomization: randomize the virtual sequence's knobs once at the top, then pass values down, so the whole scenario stays reproducible from one seed.

    Ready to build multi-agent testbenches that actually scale? Our CourseTron UVM Verification track walks you from a single sequencer to full chip-level virtual sequences with graded labs on VCS and Xcelium.

    FAQ

    Is a virtual sequencer mandatory for a virtual sequence?

    No. You can give the virtual sequence direct sequencer handles via the config DB and start it on null. The virtual sequencer plus p_sequencer is the classic pattern and is cleaner when many sequences share the same set of targets.

    What is the difference between m_sequencer and p_sequencer?

    m_sequencer is the base-class handle to whatever sequencer you were started on. p_sequencer is a typed down-cast of that same object, created by `uvm_declare_p_sequencer, so you can reach your custom fields like the sub-sequencer handles.

    Can child sequences run in parallel?

    Yes. Since body() is ordinary SystemVerilog, wrap `uvm_do_on calls in fork...join. Each child still arbitrates independently on its own real sequencer, so per-interface locking and grabbing behave normally.

    Why does my p_sequencer come back null?

    Almost always because the virtual sequence was started on a sequencer whose type does not match the one in `uvm_declare_p_sequencer, so the internal $cast silently fails. Start it on the correct virtual sequencer type.

    Advertisement