Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
    • Sequence Items and the Driver Handshake
    • Sequence Mechanics: Lifecycle, Sequencer, Responses and Control
    • Virtual Sequences and Coordination
      • The Coordination Problem
      • Virtual Sequencer Wiring
      • Declaring p_sequencer
      • Scenario Patterns and Placement
      • Sub-sequences and Sequence Libraries
      • Virtual Sequences Without a Virtual Sequencer
      • lock vs grab: Who Gets the Sequencer
      • Protocol Layering with Translator Sequences
      • Reactive (Slave) Sequences
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Virtual Sequences and Coordination
  1. Topics
  2. UVM Verification
  3. UVM Sequences & Stimulus
  4. Virtual Sequences and Coordination
  5. Virtual Sequences Without a Virtual Sequencer

Part 5 · Sequences · Intermediate

Virtual Sequences Without a Virtual Sequencer

The bench's dma_vseq as a worked example: handles assigned by the test, start(null), the parent argument for every child, a base vseq class, handles from config_db, and the trade-offs against p_sequencer.

On this page

    In a design review, a senior engineer looks at the DMA bench and asks: "Where's the virtual sequencer?" There isn't one. The test hands dma_vseq two sequencer handles and an interface, and calls vseq.start(null). Is that a shortcut that will hurt at chip level, or a legitimate design? And if it is legitimate, what does the virtual sequencer buy you that this does not?

    The idea: a sequence needs handles, not a home

    A virtual sequence never sends an item of its own. Everything it does goes through the start() calls of its children, and each of those names a real sequencer. So what the virtual sequence needs is handles to real sequencers. A virtual sequencer is one way to deliver them: a component that holds the handles, which the virtual sequence reaches through p_sequencer. Assigning them straight to fields of the virtual sequence is another.

    Think of a contractor working on a house. The owner can hand over the keys directly (handles assigned by the test), or leave them on a rack at reception for the contractor to collect (a virtual sequencer). Either way the contractor gets in. The rack is worth having when many contractors come and go, or when the keys change; for one job, handing them over is simpler and leaves less to get wrong.

    The picture: where the handles come from

    Flowchart: the test assigns sequencer handles to dma_vseq and starts it with null; dma_vseq starts APB children on the APB sequencer and the packet on the stream sequencer.
    dma_vseq gets its handles as plain fields from the test and is started with no sequencer. Every child starts on a real sequencer with the vseq as its parent.

    Build it: dma_vseq, line by line

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    // snippet: from the course DMA bench
    // One whole DMA job across two agents. It has no sequencer of its own: the
    // test hands it the two agent sequencers and the interrupt interface, and
    // starts it with start(null).
    class dma_vseq extends uvm_sequence;
      `uvm_object_utils(dma_vseq)
      apb_sequencer  apb_sqr;
      axis_sequencer axis_sqr;
      virtual irq_if irq_vif;
      rand bit [31:0]   key;
      rand int unsigned len;
      constraint len_c { len inside {[2:16]}; }
      bit [31:0] status;                         // STATUS as read by the interrupt handler
    
      function new(string name = "dma_vseq");
        super.new(name);
      endfunction
    
      task write_reg(bit [7:0] addr, bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 1; s.data = data;
        s.start(apb_sqr, this);                  // this vseq is the parent
      endtask
    
      task body();
        apb_poll_seq poll = apb_poll_seq::type_id::create("poll");
        axis_pkt_seq pkt  = axis_pkt_seq::type_id::create("pkt");
        dma_irq_seq  isr  = dma_irq_seq::type_id::create("isr");
        if (apb_sqr == null || axis_sqr == null)
          `uvm_fatal("VSEQ", "set apb_sqr and axis_sqr before starting")
    
        write_reg(KEY, key);
        write_reg(LEN, len);
        write_reg(CTRL, 32'h3);                  // IRQ_EN | GO
        if (!pkt.randomize() with { n == len; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        fork
          poll.start(apb_sqr, this);             // background polling
          pkt.start(axis_sqr, this);             // the data, in parallel
          begin                                  // the interrupt handler
            wait (irq_vif.irq === 1'b1);
            isr.start(apb_sqr, this);
            status   = isr.status;
            poll.stop = 1;
          end
        join
        `uvm_info("VSEQ", $sformatf("job done: STATUS=%08h after %0d polls", status, poll.polls), UVM_LOW)
      endtask
    endclass

    Walk through it in the order it runs:

    • `extends uvm_sequence` with no parameter: the item type defaults to uvm_sequence_item, a reminder that this class sends no items of its own.

    • The three handle fields are typed: apb_sequencer, axis_sequencer, virtual irq_if. Assigning the wrong kind of sequencer is a compile error in the test, not a run-time fatal.

    • The null check at the top of body turns a forgotten assignment into one clear fatal: VSEQ with "set apb_sqr and axis_sqr before starting". It checks only the sequencer handles. Comparing a virtual interface handle with null is legal SystemVerilog, but it miscompiles in Verilator 5.048, which the bench is verified on; on a commercial simulator you would check irq_vif too.

    • `write_reg` starts one apb_rw_seq per register with s.start(apb_sqr, this). Passing this as the parent is not decoration: it names the child vseq.s, and it makes the child part of the vseq's family for lock and priority purposes.

    • The fork runs the poller, the packet and the interrupt branch; the interrupt branch starts dma_irq_seq, copies the STATUS it read, and stops the poller (Scenario Patterns covers the shape).

    The test side is four lines, shown in The Coordination Problem: assign apb_sqr, axis_sqr and irq_vif, randomize, then vseq.start(null). With a null sequencer, start skips everything that needs one (there is no arbitration for the vseq itself) and just runs the hooks and body.

    A base class so every vseq gets the handles once

    With more than one virtual sequence, copying three fields and a check into each is how a bench drifts. Put them in a base class with one helper the test calls, and derive every scenario from it:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // snippet
    virtual class dma_base_vseq extends uvm_sequence;
      apb_sequencer  apb_sqr;
      axis_sequencer axis_sqr;
      virtual irq_if irq_vif;
    
      function new(string name = "dma_base_vseq");
        super.new(name);
      endfunction
    
      // The test calls this once; child vseqs copy from their parent the same way.
      function void set_handles(apb_sequencer a, axis_sequencer x, virtual irq_if i);
        apb_sqr = a; axis_sqr = x; irq_vif = i;
      endfunction
    
      task pre_body();
        if (apb_sqr == null || axis_sqr == null)
          `uvm_fatal("VSEQ", {get_full_name(), ": call set_handles() before start()"})
      endtask
    endclass
    
    // usage in a test:  vseq.set_handles(env.apb.sqr, env.in_agt.sqr, irq_vif);  vseq.start(null);

    The class is virtual (abstract) and has no registration macro; each concrete scenario registers itself with ` uvm_object_utils `. pre_body runs only for a sequence started with call_pre_post set (the default for start`), so the check covers top-level vseqs and children started normally.

    Getting the handles from config_db instead

    If you would rather the test not know agent paths, the env can publish the sequencer handles in connect_phase and the vseq can fetch them. Mind the path: a sequence started with start(null) and no parent has a full name that is just its own name (vseq), with no component path in front. A set scoped to "uvm_test_top.env.*" never matches it.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet
    // dma_env::connect_phase
    uvm_config_db#(apb_sequencer)::set(null, "*", "apb_sqr", apb.sqr);
    uvm_config_db#(axis_sequencer)::set(null, "*", "axis_sqr", in_agt.sqr);
    
    // dma_base_vseq::pre_body, before the null check
    if (apb_sqr == null)
      void'(uvm_config_db#(apb_sequencer)::get(null, get_full_name(), "apb_sqr", apb_sqr));
    if (axis_sqr == null)
      void'(uvm_config_db#(axis_sequencer)::get(null, get_full_name(), "axis_sqr", axis_sqr));

    The "*" scope is deliberately broad: these are handles, not knobs, and there is one env. Configuring Sequences explains how a sequence's full name is formed and how to scope per-sequence knobs.

    The trade-offs, side by side

    Handles in the vseq (the bench)

    Virtual sequencer + p_sequencer

    Wrong handle type

    Compile error in the test

    DCLPSQ fatal at start() if the vseq starts on the wrong sequencer type

    Missing handle

    Your own null check in the vseq

    Your own null check in the env's connect_phase

    Extra component

    None

    One per env, plus a derived env or factory override

    Reporting path

    reporter@@vseq

    uvm_test_top.env.v_sqr@@vseq

    Config_db for the vseq

    Full name is just vseq

    Full name starts with the virtual sequencer's path

    Default sequence per phase

    Not possible: needs a sequencer

    Possible through config_db on the virtual sequencer

    Moving to chip level

    The chip test or chip vseq passes handles down

    The chip env builds a bigger virtual sequencer

    Neither is wrong. Handles in the vseq are simpler and fail at compile time; a virtual sequencer gives you a place in the hierarchy for reporting, config and default sequences, which matters most in large benches with many virtual sequences and several engineers.

    Watch it run

    The real log of dma_vseq_test with +UVM_VERBOSITY=UVM_MEDIUM (random values differ by seed):

    Diagram
    UVM_INFO dma_tb.sv(572) @ 405000: uvm_test_top.env.apb.sqr@@vseq.isr [IRQ] handler has the APB sequencer
    UVM_INFO dma_tb.sv(623) @ 575000: reporter@@vseq [VSEQ] job done: STATUS=00000002 after 4 polls
    UVM_INFO dma_tb.sv(411) @ 675000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched

    The two name forms tell the whole story of this lesson. reporter@@vseq is a sequence with no sequencer; uvm_test_top.env.apb.sqr@@vseq.isr is its child, named under its parent and reported through the real sequencer it ran on. Compare the virtual-sequencer version from Virtual Sequencer Wiring, where the same line reads uvm_test_top.env.v_sqr@@vseq.

    What breaks: the config_db set that matched nothing

    A team moved their DMA vseq handles into config_db, set from the env with set(this, "*", "apb_sqr", apb.sqr). Every run died at time 0 with their own "handle not set" fatal. The env's context made the scope uvm_test_top.env.*, while the vseq, started with start(null), looked itself up as plain vseq. Nothing was wrong with the set or the get on their own; the two names simply never met. Printing get_full_name() from the vseq made it obvious in one line. They fixed it with a null context and "*" scope for the handles, which is the pattern above. With a virtual sequencer the same get would have worked, because the vseq's full name would then start with uvm_test_top.env.v_sqr.

    Interview

    "Do you always need a virtual sequencer for a virtual sequence?"

    Weak: "Yes, a virtual sequence has to run on a virtual sequencer."

    Strong: "No. A virtual sequence only needs handles to the real sequencers. On our DMA bench the test assigns typed handle fields and calls start(null); children start on real sequencers with the vseq as parent, so naming and lock inheritance still work. A wrong handle type is a compile error instead of a DCLPSQ fatal. I'd use a virtual sequencer when I want a hierarchy path for reporting and config_db, or default sequences per phase, or when many vseqs and engineers share the handles. Either way I put the handles in a base vseq class with one null check."

    Follow-ups: What is get_full_name() of a vseq started with start(null)? (just its name, which matters for config_db), and How do child vseqs get the handles? (the parent copies them, or the base class helper does).

    Check yourself dma_vseq is started with start(null). Inside it, write_reg starts apb_rw_seq with s.start(apb_sqr), without this. What changes in a normal run, and what changes if the vseq ever grabs the APB sequencer?

    In a normal run, only the names: the child is reported as uvm_test_top.env.apb.sqr@@s instead of ...@@vseq.s, and config_db lookups for it change. If the vseq holds a lock or grab on the APB sequencer, the child is no longer its descendant, so it is blocked by its own parent's lock and the vseq deadlocks.

    Check yourself Why does the bench check apb_sqr and axis_sqr for null but not irq_vif?

    The comparison would be legal SystemVerilog, but it miscompiles in Verilator 5.048, which the bench is verified with. A missing irq_vif would instead show up as a null access at the wait on the interrupt. On a commercial simulator, check it too.

    Next: inside dma_vseq, the interrupt handler calls grab() while the poller keeps queuing reads. Why grab and not lock, and what each really does to the queue, is lock vs grab: Who Gets the Sequencer.

    Related topics

    • Declaring p_sequencer

    • Configuring sequences

    • The coordination problem

    Previous

    90 / 280

    Next: lock vs grab: Who Gets the Sequencer

    Up next

    Up nextlock vs grab: Who Gets the Sequencer

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Coordination Problem
    • Virtual Sequencer Wiring
    • Declaring p_sequencer