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. The Coordination Problem

Part 5 · Sequences · Intermediate

The Coordination Problem

Why a DMA job needs one owner across APB, AXI-Stream and the interrupt: the test-sprawl trap, what a virtual sequence is, and the thin test that starts it.

On this page

    Your team's DMA regression has forty tests. Each one programs KEY and LEN, sets GO, sends a packet, waits for the interrupt and clears STATUS, and each one does it in its own run_phase, copied from the smoke test. Then the designers add a rule: software must read STATUS before clearing it, or a sticky error bit is lost. You now have forty places to change, and the three tests nobody remembered keep passing with the old order. Where should the order of a DMA job live, so that it is written exactly once?

    The idea: a conductor who plays no instrument

    A DMA job is one story told over three interfaces. Register writes go over APB, data goes over AXI-Stream, and the end of the job arrives on the interrupt line. Each agent knows its own protocol and nothing else: the APB agent has never heard of a packet, and the stream agent does not know that GO must come before the first word. The ordering across them is knowledge that belongs to no single agent.

    A virtual sequence holds that knowledge. It is an ordinary uvm_sequence, but it never calls start_item itself. It creates other sequences and starts each one on the right agent's sequencer, in the right order or in parallel. Think of an orchestra conductor: the conductor plays no instrument, but decides who comes in when. The violins (the APB agent) and the brass (the stream agent) only need to play their own parts well.

    "Virtual" here only means no driver underneath. A virtual sequence has no item type of its own that reaches pins, so it can start on a virtual sequencer (a component that only holds handles to real sequencers), or, as the course bench does, on no sequencer at all.

    The picture: one job, three interfaces

    Sequence diagram: the virtual sequence writes three registers on the APB sequencer, then runs a poller, the packet and the interrupt handler in parallel.
    The order of a DMA job as dma_vseq runs it: three register writes in sequence, then polling, data and the interrupt handler in parallel.

    Two kinds of ordering show up here. Sequential ordering is a plain series of start() calls: KEY before GO. Parallel ordering is a fork: the data, the polling and the interrupt handler all run at once. Neither kind can be expressed inside one agent's sequence, because each branch runs on a different sequencer.

    Build it: from a fat test to a thin one

    Where the order lives today

    The bench's base test already shows the problem in a small way. Its run_job task knows the whole job: the register order, the packet, the wait on the interrupt, the read and clear of STATUS. It is fine for one family of tests, but the order lives in a test class, and every new scenario (polling, two jobs back to back, an error injected mid-job) means another test that copies or overrides 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
    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
    49
    50
    // snippet: from the course DMA bench
    class dma_base_test extends uvm_test;
      `uvm_component_utils(dma_base_test)
      dma_env env;
      virtual irq_if irq_vif;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = dma_env::type_id::create("env", this);
        if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
          `uvm_fatal("NOVIF", "irq_if not set for the test")
      endfunction
    
      function void end_of_elaboration_phase(uvm_phase phase);
        uvm_root::get().print_topology();
      endfunction
    
      // Register helpers used by every test.
      task reg_write(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(env.apb.sqr);
      endtask
    
      task reg_read(bit [7:0] addr, output bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 0; s.data = 0;
        s.start(env.apb.sqr);
        data = s.rdata;
      endtask
    
      // One complete job: program the DMA, stream n words, wait for the interrupt,
      // return STATUS and clear its DONE/ERR bits.
      task run_job(dma_job_cfg job, int unsigned n_words, output bit [31:0] status);
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
        reg_write(KEY, job.key);
        reg_write(LEN, job.len);
        reg_write(CTRL, {30'b0, job.irq_en, 1'b1});
        if (!pkt.randomize() with { n == n_words; })
          `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.in_agt.sqr);
        wait (irq_vif.irq === 1'b1);
        reg_read(STATUS, status);
        reg_write(STATUS, 32'h6);                // write 1 to clear DONE and ERR
      endtask
    endclass

    Notice also what run_job cannot express: it runs strictly one step after another. A poller that reads COUNT while the data moves, with an interrupt handler that cuts in front of it, needs parallel branches on two sequencers and a way to stop one branch from another. That is scenario logic, and it wants its own class.

    Where it lives with a virtual sequence

    Move the job into dma_vseq (the next lessons take it apart line by line) and the test shrinks to four jobs: give the virtual sequence its sequencer handles, randomize its knobs, start it, and hold the objection while it runs.

    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
    // snippet: from the course DMA bench
    // A virtual sequence drives a whole job across both agents, with background
    // polling and a grab-based interrupt handler on the APB sequencer.
    class dma_vseq_test extends dma_base_test;
      `uvm_component_utils(dma_vseq_test)
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      task run_phase(uvm_phase phase);
        dma_vseq vseq = dma_vseq::type_id::create("vseq");
        phase.raise_objection(this);
        vseq.apb_sqr  = env.apb.sqr;
        vseq.axis_sqr = env.in_agt.sqr;
        vseq.irq_vif  = irq_vif;
        if (!vseq.randomize()) `uvm_fatal("RAND", "dma_vseq randomize failed")
        vseq.start(null);
        if (vseq.status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("expected DONE only, STATUS=%08h", vseq.status))
        #100ns;
        phase.drop_objection(this);
      endtask
    endclass

    That is the thin test. It decides what runs and for how long; the virtual sequence decides how a DMA job goes. A second test that wants three jobs in a row, or a different key distribution, reuses dma_vseq unchanged, with a constraint or a loop around start. The rule from the designers' change in the opening now lives in one class.

    Belongs in the test

    Belongs in the virtual sequence

    Belongs in an agent sequence

    Which scenario, how many times, which knobs

    Order across agents: KEY before GO, data in parallel with polling

    One protocol action: an APB access, a stream packet

    The objection for the whole run

    Waiting for the interrupt, then reacting to it

    Item fields and constraints

    Factory and config overrides

    Stopping a background branch when the job ends

    Nothing about other agents

    Watch it run

    Run the bench with +UVM_TESTNAME=dma_vseq_test, and add +UVM_VERBOSITY=UVM_MEDIUM to see the interrupt handler's line. The key and length are random, so your times and poll count differ by simulator and seed (times are in ps):

    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
    UVM_ERROR :    0
    • `vseq.isr` is the interrupt handler, named as a child of the virtual sequence because dma_vseq passed itself as the parent. It ran on the APB sequencer, so the message is reported through uvm_test_top.env.apb.sqr.

    • `reporter@@vseq`: the virtual sequence itself has no sequencer (it was started with start(null)), so its messages go through the global reporter.

    • STATUS=00000002 is DONE alone, which is what the test checks. 4 polls proves the poller really ran in parallel with the data.

    • 6 words matched means the scoreboard, which knows nothing about virtual sequences, checked every word. Coordination changes stimulus, never checking.

    What breaks: the interrupt that was still high

    A team writing their first two-job test called their copied job task twice in a row. The first job injected a short packet on purpose, so it ended with STATUS.ERR. The second job then misbehaved in a way that made no sense: STATUS was read before its data had moved, and the run ended with the scoreboard reporting words that never came out. The cause was one line missing from the copy. Someone had dropped the STATUS clear because "GO clears DONE anyway". It does, but it does not clear ERR, and the DMA's interrupt is a level: irq = IRQ_EN & (DONE | ERR). With ERR still set, the line was high when the second job started, so its wait (irq === 1) returned at once.

    The fix was not to add the clear back in that one test. It was to move the whole job, including read STATUS, then write 1 to clear DONE and ERR, into one virtual sequence that every test reuses. When ordering rules live in exactly one class, a missing step is a bug in one place that every test exposes, instead of a silent difference between forty copies.

    Interview

    "What is a virtual sequence, and why not just put the scenario in the test?"

    Weak: "A virtual sequence runs on a virtual sequencer and starts other sequences." That is a definition, not a reason.

    Strong: "A virtual sequence owns ordering across agents: KEY before GO on APB, data in parallel on the stream, react to the interrupt, clear it. It never drives items itself; it starts agent sequences on their sequencers, sequentially or in a fork. Keeping that out of the test means tests stay thin (knobs, objection, one start call) and the scenario is reusable: a two-job test, a stress test and a chip-level test all reuse the same class. On our DMA bench the virtual sequence doesn't even need a virtual sequencer: the test hands it the two sequencer handles and calls start(null)."

    Follow-ups to expect: Where does the objection live when a virtual sequence runs? (in the test around start, or in the sequence with set_automatic_phase_objection(1)), and What happens if two branches of your fork start sequences on the same sequencer? (they arbitrate item by item; covered in the arbitration and lock lessons).

    Check yourself A new test needs two DMA jobs back to back with different keys. With the virtual sequence in place, what does the new test contain?

    A loop (or two calls) around randomizing and starting dma_vseq, the handle assignments, and the objection. No register offsets, no waits on the interrupt, no STATUS handling: those live in dma_vseq, which also clears DONE and ERR before it finishes, so the second job's interrupt wait is not fooled by the first.

    Check yourself Someone proposes putting the DMA job order inside the APB agent's sequence library, "because most of it is register writes". What goes wrong?

    The APB agent would need to know about the stream sequencer and the interrupt line, which couples a reusable protocol agent to one design. At chip level, where that APB agent might talk to ten different blocks, the DMA story is meaningless. Cross-agent order belongs one level up, in a virtual sequence owned by the DMA environment.

    Check yourself The virtual sequence log line is reported by reporter@@vseq, but the interrupt handler's line by uvm_test_top.env.apb.sqr@@vseq.isr. Why the difference?

    Messages from a sequence are reported through its sequencer when it has one. dma_vseq was started with start(null), so it has none and falls back to the global reporter. dma_irq_seq runs on the APB sequencer, and its name includes vseq. because the virtual sequence passed itself as the parent.

    Next: a virtual sequence needs handles to real sequencers. The classic way to give it them is a component built for the purpose: Virtual Sequencer Wiring.

    Related topics

    • Virtual sequences without a virtual sequencer

    • Your first UVM testbench

    • The DMA spec

    • Objections and end of test

    Previous

    85 / 280

    Next: Virtual Sequencer Wiring

    Up next

    Up nextVirtual Sequencer Wiring

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Declaring p_sequencer
    • Scenario Patterns and Placement
    • Sub-sequences and Sequence Libraries