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. Scenario Patterns and Placement

Part 5 · Sequences · Intermediate

Scenario Patterns and Placement

The fork shapes a virtual sequence uses: sequential steps, parallel branches, a background branch stopped by a flag, races with join_any and a guarded disable fork plus kill(), layered virtual sequences, and who owns which sequence.

On this page

    A new virtual sequence passes its first run, then hangs on the second seed. The data has all come out, the scoreboard is happy, the interrupt was handled, and still the run phase never ends. Eventually the timeout fires. The culprit is a background poller that nobody told to stop, keeping a fork ... join open forever. How do you shape the branches of a scenario so that every one of them is guaranteed to finish?

    The idea: every branch needs an exit

    A virtual sequence is a small concurrent program. Some steps must happen in order (program the registers, then start), some happen side by side (data, polling, the interrupt handler), and some race (the interrupt against a watchdog). SystemVerilog gives you three tools for that: plain sequential calls, fork ... join (wait for all branches), and fork ... join_any (continue when the first finishes).

    The trap is the branch with no natural end. A poller reads a register forever; a responder answers forever. Think of a security guard doing rounds in a building: the rounds never end on their own. You can tell the guard the shift is over and let them finish the current round (a stop flag), or you can remove them mid-corridor (kill). The first is clean; the second is sometimes necessary and always needs cleanup.

    The picture: the fork inside dma_vseq

    Flowchart: sequential register writes, then a fork of a poller, the packet and an interrupt branch that sets the poller's stop flag, then a join.
    dma_vseq's three branches. The data and interrupt branches end on their own; the poller ends only because the interrupt branch sets its stop flag.

    The dashed arrow is the whole design. Without it, join waits for a branch that never ends.

    Build it

    A background branch that can be told to stop

    apb_poll_seq checks a stop bit before every access. Whoever owns it sets the bit; the poller finishes the access in flight, sleeps its 50 ns, sees the bit, and returns. Nothing is cut off mid-item, so the sequencer and driver are never left holding half a transfer.

    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
    // snippet: from the course DMA bench
    // Reads COUNT every 50 ns until told to stop: background register traffic that
    // keeps the APB sequencer busy, the way firmware polling would.
    class apb_poll_seq extends uvm_sequence #(apb_item);
      `uvm_object_utils(apb_poll_seq)
      bit          stop;
      int unsigned polls;
    
      function new(string name = "apb_poll_seq");
        super.new(name);
      endfunction
    
      task body();
        while (!stop) begin
          apb_item it = apb_item::type_id::create("it");
          start_item(it);
          if (!it.randomize() with { addr == COUNT; write == 0; })
            `uvm_fatal("RAND", "apb_item randomize failed")
          finish_item(it);
          polls++;
          #50ns;
        end
      endtask
    endclass

    Inside dma_vseq, the fork is three lines plus the handler block. The interrupt branch is the one that knows the job is over, so it is the one that stops the poller:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: the fork inside dma_vseq::body
    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

    Two of the branches share the APB sequencer. That is fine: they arbitrate item by item, and the interrupt handler uses grab() to go first when it matters (lock vs grab is the deep dive). Branches on different sequencers really run in parallel; branches on the same sequencer take turns, by the rules in Inside the Sequencer.

    A race: join_any, then clean up the loser

    Sometimes the first finisher decides the outcome: the interrupt arrives, or a watchdog fires. join_any continues after the first branch, and the others keep running unless you stop them. A bare disable fork kills every child of the current process, which in a virtual sequence can include branches you started earlier and still need. Wrap the race in its own fork begin ... end join so the disable fork reaches only the race.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: wait for the interrupt, or give up after 20 us
    task wait_irq_or_timeout(output bit timed_out);
      timed_out = 0;
      fork begin                             // guard: disable fork stops here
        fork
          wait (irq_vif.irq === 1'b1);
          begin #20us; timed_out = 1; end
        join_any
        disable fork;                        // stops only the losing branch
      end join
    endtask

    When the loser is a sequence, killing its thread is not enough. A sequence blocked in start_item has a request queued in the sequencer, and it may hold a lock. If the thread dies without telling the sequencer, the next arbitration finds a request whose process is dead, reports SEQREQZMB and removes it; a lock held by a dead thread is never released. Call kill() on the losing sequence instead, or as well: it removes the sequence's requests and locks from the sequencer before stopping it. If the sequence might be killed with an item already in the driver, the driver must be reset-aware, which the Agents section covers.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet: race two sequences, then kill the one still running
    fork begin
      fork
        isr.start(apb_sqr, this);
        watchdog.start(apb_sqr, this);
      join_any
      disable fork;
    end join
    if (isr.get_sequence_state() != UVM_FINISHED)      isr.kill();
    if (watchdog.get_sequence_state() != UVM_FINISHED) watchdog.kill();

    Layered virtual sequences

    A scenario often is a sequence of smaller scenarios. A two-job test wants dma_vseq twice; a chip stress test wants the DMA job beside a PCIe boot. Write the bigger scenario as a virtual sequence that starts the smaller ones as children, passing the handles down and itself as the parent:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // snippet
    class dma_two_job_vseq extends uvm_sequence;
      `uvm_object_utils(dma_two_job_vseq)
      apb_sequencer  apb_sqr;
      axis_sequencer axis_sqr;
      virtual irq_if irq_vif;
    
      function new(string name = "dma_two_job_vseq");
        super.new(name);
      endfunction
    
      task body();
        repeat (2) begin
          dma_vseq job = dma_vseq::type_id::create("job");
          job.apb_sqr = apb_sqr; job.axis_sqr = axis_sqr; job.irq_vif = irq_vif;
          if (!job.randomize()) `uvm_fatal("RAND", "dma_vseq randomize failed")
          job.start(null, this);           // no sequencer, but a parent
        end
      endtask
    endclass

    Each dma_vseq clears DONE and ERR before it returns, which is what makes back-to-back jobs safe. A base class that holds the handles once, instead of copying them in every vseq, is in Virtual Sequences Without a Virtual Sequencer.

    Who owns which sequence

    Owner

    Owns

    On the DMA bench

    Agent package

    Protocol sequences that know one interface

    apb_rw_seq, apb_poll_seq, axis_pkt_seq

    Env package

    Scenario (virtual) sequences that know the design

    dma_vseq, dma_irq_seq, dma_two_job_vseq

    Test

    Knobs, which scenario, how many, the objection

    dma_vseq_test

    dma_irq_seq is an APB sequence, but it knows the DMA's STATUS register and its W1C bits, so it belongs with the DMA environment, not the APB agent. A good rule: if a sequence mentions a register name or another interface, it is not agent code. On objections: the test usually raises one around vseq.start(). A sequence can also hold one, with set_automatic_phase_objection(1) in UVM 1.2 and IEEE 1800.2; starting_phase is deprecated in 1.2 and removed in 1800.2. See Objections and end of test.

    Watch it run

    dma_vseq_test shows the pattern working: the job finishes, the poller has stopped, and the join returned so the vseq could print its summary. The poll count depends on the random length.

    Diagram
    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

    If the VSEQ line is missing and the run ends with a timeout instead, one of the three branches never returned. The poller is the first suspect.

    What breaks: the poller that outlived the job

    The hang in the opening came from a refactor. Someone moved the interrupt handling into a helper task and forgot to carry poll.stop = 1 along. Data, interrupt and STATUS clear all worked; the poller kept reading COUNT every 50 ns; the join waited for it; the test's objection stayed raised. With no timeout set, a run like that burns CPU until the 9200 s default. With one set, it ends with the same fatal that dma_timeout_test produces on purpose:

    Diagram
    UVM_FATAL uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issue

    Two habits prevent it. Give every open-ended branch an explicit exit and set it on every path that ends the scenario. And set a test timeout (uvm_root::get().set_timeout() or +UVM_TIMEOUT) sized to the scenario, so a hang costs seconds instead of a night of farm time. Run_test and timeouts has the details.

    Interview

    "join versus join_any in a virtual sequence: how do you stop the branches that lose?"

    Weak: "Use join_any and then disable fork." A bare disable fork kills every child of the process, including branches started earlier in the same body.

    Strong: "join waits for all branches, so each needs an exit; for open-ended ones like a poller I use a stop flag that the deciding branch sets, so the poller finishes its current item cleanly. For a real race I use join_any inside a fork begin ... end join guard so disable fork reaches only the race, then call kill() on any losing sequence so the sequencer drops its queued requests and locks. If a killed sequence could have an item in the driver, the driver has to be reset-aware."

    Follow-ups: What does disable fork kill exactly? (every descendant of the current process), and What happens if a thread dies while waiting in start_item? (the sequencer finds the dead request, reports SEQREQZMB and removes it).

    Check yourself A virtual sequence runs a poller and a packet in a fork ... join_any, and the author expects the poller to stop when the packet ends. What actually happens?

    join_any returns when the packet finishes, but the poller keeps running in the background, still issuing APB reads after the vseq has moved on, and possibly after the test thinks the scenario is over. It needs a stop flag (or a guarded disable fork plus kill()).

    Check yourself Why does apb_poll_seq check stop at the top of the loop instead of being killed by the vseq?

    A flag lets the current access finish: the item completes, item_done is called, and no request is left in the sequencer. Killing could interrupt it while blocked in start_item (leaving a dead request) or while its item is in the driver. The flag costs at most one extra 50 ns wait.

    Next: every scenario is built from smaller sequences. Composing them, and letting the seed choose among them, is Sub-sequences and Sequence Libraries.

    Related topics

    • Objections and end of test

    • run_test and timeouts

    • Reusable VIP

    Previous

    88 / 280

    Next: Sub-sequences and Sequence Libraries

    Up next

    Up nextSub-sequences and Sequence Libraries

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Coordination Problem
    • Virtual Sequencer Wiring
    • Virtual Sequences Without a Virtual Sequencer