Skip to content

UVM

Virtual Sequences

A top-level orchestrator that coordinates sub-sequences across multiple sequencers — running on a virtual sequencer that holds handles to the real agents' sequencers but drives no interface itself.

Advanced Sequences · Module 10 · Page 10.1

The Engineering Problem

Every sequence so far ran on one sequencer and drove one interface (Modules 9.1–9.7). But a real system-on-chip test isn't one interface — it's many, coordinated: configure the DUT over APB, then drive data traffic over AXI, while a third agent watches for interrupts, then check. No single-sequencer sequence can express that, because the stimulus spans multiple agents and must be synchronized across them. Hand-coordinating separate sequences from the test — starting each on its own sequencer with manual fork/join and ad-hoc timing — works but scatters the system scenario across the test and loses reuse.

The virtual sequence is the pattern for whole-system stimulus. It's a top-level orchestrator that coordinates sub-sequences across multiple sequencers — and it's called virtual because it drives no interface itself: it generates no items, owns no driver. Instead, it runs on a virtual sequencer — a sequencer with no driver that simply holds handles to the real agents' sequencers — and its body() starts sub-sequences on those real sequencers (reached via p_sequencer), sequentially or in parallel, to choreograph a system-level scenario. So the multi-interface coordination lives in one reusable sequence, started once on the virtual sequencer. This chapter is the virtual sequence: what it is, the virtual sequencer that holds the real-sequencer handles, how it orchestrates sub-sequences across interfaces, and why an unconnected virtual-sequencer handle is the bug that crashes it.

What is a virtual sequence — how does it coordinate sub-sequences across multiple sequencers, what is the virtual sequencer that holds the real-sequencer handles, and how do you wire and run it to choreograph a whole-system scenario?

Motivation — why coordination needs a level above

The virtual sequence exists because system-level stimulus has needs no single-sequencer sequence can meet:

  • System scenarios span multiple interfaces, so stimulus must reach multiple sequencers. "Configure then run traffic then check" touches APB, AXI, and an interrupt line — three agents, three sequencers. A normal sequence reaches one sequencer; coordinating three needs something that holds all their handles. That's the virtual sequencer.
  • Coordination is itself the content, so it deserves its own sequence. When the AXI traffic starts relative to the APB configuration is the test. Putting that choreography in a virtual sequence makes the system scenario a first-class, reusable object, rather than ad-hoc timing scattered in the test.
  • The orchestrator drives nothing, so it's "virtual." A coordinator doesn't itself produce interface activity — the sub-sequences it launches do, on the real sequencers. So the virtual sequence has no items and the virtual sequencer has no driver; "virtual" names exactly this: it coordinates without driving.
  • The real sequencers already exist, so the virtual sequencer just references them. Each agent already has its sequencer. The virtual sequencer doesn't replace them — it holds handles to them (wired in connect_phase), so the virtual sequence can reach each real sequencer to launch sub-sequences on it.
  • One entry point keeps system tests clean. The test starts one virtual sequence on the one virtual sequencer, and the whole multi-interface scenario unfolds — instead of the test juggling several sequence starts. The system scenario has a single, reusable launch point.

The motivation, in one line: whole-system stimulus must reach and synchronize multiple sequencers, and that coordination is itself the test — so UVM provides a virtual sequence (no driving of its own) running on a virtual sequencer (handles to the real sequencers), which choreographs sub-sequences across interfaces from one reusable place.

Mental Model

Hold a virtual sequence as the conductor of an orchestra:

A virtual sequence is the conductor: it plays no instrument itself, but from the podium it cues each section — when and how to play — and the coordinated result is the performance. The orchestra sections are the real agents (APB, AXI, interrupt), each with its own players (its sequencer and driver) who actually make sound (drive the interface). The conductor (virtual sequence) makes no sound — it has no instrument (no items, no driver) — yet it's in charge of the whole piece: it reads the score (its body()) and cues each section in turn or together (starts sub-sequences on each real sequencer). The podium (virtual sequencer) is where the conductor stands, and it has sightlines to every section — handles to every real sequencer — which is what lets the conductor cue them. The score says "strings configure the key (APB), then brass enters with the theme (AXI traffic), while percussion marks interrupts" — that coordination across sections is the conductor's entire job. And the one thing that ruins it: if a sightline to a section is missing (a virtual-sequencer handle not connected), the conductor cues an empty chair — nothing plays, and the cue crashes into silence.

So a virtual sequence is conducting, not playing: it coordinates the sections (agents) from the podium (virtual sequencer) according to the score (body()), cueing each via its sightline (the connected handle). The performance is the system scenario; the conductor never touches an instrument.

Visual Explanation — the virtual sequence orchestrating real sequencers

The defining picture is one orchestrator above many agents: the virtual sequence, on the virtual sequencer, reaching down to each real sequencer to launch sub-sequences.

A virtual sequence on a virtual sequencer launches sub-sequences on multiple real sequencers driving their interfacesrunsonhandle → startapb_seqhandle → startaxi_seqdrives APBdrives AXIVirtual sequence(orchestrator)no items — coordinates sub-sequencesVirtual sequencer (hub)no driver — holds real-sequencerhandlesAPB sequencerreal sequencerAXI sequencerreal sequencerAPB driver → DUTdrives the interfaceAXI driver → DUTdrives the interface12
Figure 1 — a virtual sequence orchestrates sub-sequences across multiple real sequencers. The virtual sequence runs on the virtual sequencer (which drives no interface and holds handles to the real sequencers). Its body() starts sub-sequences on each real agent's sequencer — an APB sub-sequence on the APB sequencer, an AXI sub-sequence on the AXI sequencer — which drive their interfaces. So one orchestrator coordinates many agents: it produces no items itself; the sub-sequences on the real sequencers do. The virtual sequencer is the hub holding the handles.

The figure shows the one-above-many structure that defines a virtual sequence. At the top-left, the virtual sequence is the orchestrator: it has no items of its own. It runs on the virtual sequencer — the hub in the middle — which has no driver and whose only job is to hold handles to the real sequencers. Through those handles, the virtual sequence's body() starts sub-sequences on each real agent's sequencer: an APB sub-sequence on the APB sequencer, an AXI sub-sequence on the AXI sequencer. Those sub-sequences are normal sequences producing items, and their drivers drive the actual interfaces to the DUT. So the interface activity comes entirely from the sub-sequences on the real sequencers; the virtual sequence and virtual sequencer produce nothing themselves — they coordinate. The crucial structural fact is the hub: the virtual sequencer is what gives the one orchestrator reach to all the real sequencers, by holding their handles. Without it, the virtual sequence would have no way to address multiple sequencers from one place. So the picture is a coordination layer sitting above the agents: the virtual sequence decides the system scenario, the virtual sequencer provides the reach, and the real sequencers and drivers do the driving — which is exactly the separation that lets whole-system stimulus be written as one reusable sequence.

RTL / Simulation Perspective — the virtual sequencer, the wiring, and the body

A virtual sequence has three code pieces: the virtual sequencer (handles), the connection (wiring handles to real sequencers), and the virtual sequence body (launching sub-sequences on them).

virtual sequencer, connection, and virtual sequence body
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── VIRTUAL SEQUENCER: no driver; just handles to the real sequencers ──
class virtual_sequencer extends uvm_sequencer;        // no item type — it drives nothing
  `uvm_component_utils(virtual_sequencer)
  apb_sequencer apb_sqr;                               // handles to the real sequencers
  axi_sequencer axi_sqr;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclass
 
// ── CONNECTION: wire the virtual sequencer's handles to the real sequencers (env connect_phase) ──
class my_env extends uvm_env;
  function void connect_phase(uvm_phase phase);
    v_sqr.apb_sqr = apb_agent.sequencer;               // ← MUST connect, or the handle is null
    v_sqr.axi_sqr = axi_agent.sequencer;
  endfunction
endclass
 
// ── VIRTUAL SEQUENCE: reaches the real sequencers via p_sequencer; starts sub-sequences ──
class system_vseq extends uvm_sequence;                // no item type — it orchestrates
  `uvm_object_utils(system_vseq)
  `uvm_declare_p_sequencer(virtual_sequencer)          // typed access to the virtual sequencer
  function new(string name="system_vseq"); super.new(name); endfunction
 
  task body();
    apb_cfg_seq  cfg = apb_cfg_seq::type_id::create("cfg");
    axi_traffic_seq tr = axi_traffic_seq::type_id::create("tr");
    cfg.start(p_sequencer.apb_sqr);        // ① configure over APB first (sequential)
    tr.start (p_sequencer.axi_sqr);        // ② then run AXI traffic
    // parallel form: fork cfg.start(p_sequencer.apb_sqr); tr.start(p_sequencer.axi_sqr); join
  endtask
endclass
 
// ── TEST: start the ONE virtual sequence on the ONE virtual sequencer ──
//   vseq.start(env.v_sqr);   // (with the objection raised — Module 9.1)

The three pieces fit together. The virtual sequencer (virtual_sequencer) is a uvm_sequencer with no item type (it drives nothing) whose members are handles to the real sequencers (apb_sqr, axi_sqr). The connection in the env's connect_phase assigns those handles to the actual agent sequencers (v_sqr.apb_sqr = apb_agent.sequencer) — this wiring is essential, and omitting it leaves the handle null (the DebugLab). The virtual sequence (system_vseq) declares `uvm_declare_p_sequencer(virtual_sequencer) (Module 9.7) so it can reach the handles via p_sequencer.apb_sqr / p_sequencer.axi_sqr, and its body() starts sub-sequences on the real sequencers: here cfg.start(p_sequencer.apb_sqr) then tr.start(p_sequencer.axi_sqr)configure over APB, then traffic over AXI, sequentially (or in parallel with fork/join, Module 9.5). The test starts the one virtual sequence on the one virtual sequencer (vseq.start(env.v_sqr)), with an objection (Module 9.1). The shape to carry: a virtual sequencer holds handles, you connect them in connect_phase, and the virtual sequence reaches them via p_sequencer to launch sub-sequences across interfaces — the whole-system scenario in one place.

Verification Perspective — wiring the virtual sequencer

The piece that makes virtual sequences work — and the piece most often gotten wrong — is the wiring: the virtual sequencer's handles must be connected to the real sequencers, or the virtual sequence reaches nothing.

The virtual sequencer's handles are connected to the real agent sequencers in connect_phasev_sqr.apb_sqr =apb_agent.sequencerv_sqr.axi_sqr =axi_agent.sequencerv_sqr.apb_sqr (handle)null until connectedv_sqr.axi_sqr (handle)null until connectedapb_agent.sequencerthe real sequenceraxi_agent.sequencerthe real sequencer12
Figure 2 — the virtual sequencer must be wired to the real sequencers. The virtual sequencer declares handles (apb_sqr, axi_sqr) but they start null. In the env's connect_phase, each handle is assigned the corresponding real agent's sequencer — v_sqr.apb_sqr = apb_agent.sequencer. Only after this wiring can the virtual sequence reach a real sequencer via p_sequencer.apb_sqr to start a sub-sequence on it. An unconnected handle stays null, so starting a sub-sequence on it crashes. The handles are references you must connect, not sequencers themselves.

The wiring is what connects the orchestrator to the agents, and it's a distinct, easy-to-miss step. The virtual sequencer declares handles — apb_sqr, axi_sqr — but a declared handle is just a reference that starts null; it is not automatically pointed at anything. The connection happens in the env's connect_phase, where each handle is assigned the corresponding real agent's sequencer: v_sqr.apb_sqr = apb_agent.sequencer. This is structural plumbing, parallel to connecting a driver to a sequencer (Module 9.2) or a TLM port (the env wires up its components in connect_phase). Only after this wiring does p_sequencer.apb_sqr point at the real APB sequencer, so the virtual sequence can start a sub-sequence on it. The failure mode is forgetting the assignment: an unconnected handle stays null, and when the virtual sequence calls sub.start(p_sequencer.apb_sqr) with a null sequencer, it crashes (null dereference) — the virtual sequence appears broken when the bug is actually the missing wiring in the env. The mental model to keep is that the virtual sequencer's handles are references you must connect, not sequencers in their own right: the virtual sequencer borrows the real sequencers by holding handles to them, and the borrowing only works once the env hands over those handles. So building a virtual sequence is always three steps — declare the handles, connect them, and use them — and the middle step is the one that, omitted, makes everything else fail.

Runtime / Execution Flow — coordinating sub-sequences across interfaces

At run time, the virtual sequence's body() choreographs the sub-sequences across the real sequencers — sequencing them (configure before traffic) or running them concurrently — which is the system-level coordination it exists for.

The virtual sequence runs sub-sequences across real sequencers in the scenario's order, sequential and parallelstart vseq → configure (APB) → traffic (AXI) [± parallel interrupt] → donestart vseq → configure (APB) → traffic (AXI) [± parallel interrupt] → done1Test starts the virtual sequencevseq.start(env.v_sqr) — one launch point for the whole systemscenario (with objection).2Configure over APB (sequential)start the APB config sub-sequence on p_sequencer.apb_sqr; it mustfinish before traffic.3Run AXI traffic (± parallel interrupt)start the AXI traffic sub-sequence on p_sequencer.axi_sqr; fork aninterrupt sub-sequence if concurrent.4Scenario completethe coordinated multi-interface scenario has run from one virtualsequence; drop the objection.
Figure 3 — the virtual sequence choreographs sub-sequences across interfaces. Started on the virtual sequencer, its body() runs sub-sequences on the real sequencers in the order the scenario requires: configure over APB first (sequential — it must finish before traffic), then run AXI traffic, possibly with an interrupt sub-sequence in parallel. Sequential composition (Module 9.5) orders dependent steps; fork/join runs independent ones together. The virtual sequence is where cross-interface ordering and concurrency are expressed.

The run-time flow is cross-interface choreography, which is the virtual sequence's reason to exist. Step 1, the test starts the one virtual sequence on the virtual sequencer (vseq.start(env.v_sqr)), with an objection (Module 9.1) — a single launch point for the whole scenario. Step 2, the virtual sequence's body() runs the APB configuration sub-sequence on p_sequencer.apb_sqr first — and because configuration must complete before traffic can run, this is sequential (Module 9.5): the start blocks until the APB config finishes. Step 3, it then runs the AXI traffic sub-sequence on p_sequencer.axi_sqr, optionally forking a concurrent interrupt sub-sequence on a third sequencer if the scenario needs them to overlap (fork/join). Step 4, the coordinated scenario is complete, and the objection drops. The key is that all the cross-interface ordering and concurrency — what must come before what, what runs together — is expressed in the virtual sequence's body(), using the same sequential-vs-parallel control from Module 9.5, just applied across multiple sequencers instead of one. This is why the virtual sequence is the system-scenario abstraction: it's the single place where the relationships between interfaces are written, so a whole-system test ("bring up, configure, run, interrupt, check") is one coordinated, reusable sequence rather than timing scattered across the test. The virtual sequence conducts; the sub-sequences play.

Waveform Perspective — a coordinated multi-interface scenario

A virtual sequence's value is cross-interface coordination, and it's visible on a timeline: activity on different interfaces, sequenced and overlapped exactly as the virtual sequence's body() dictates.

A virtual sequence coordinates APB configure, then AXI traffic with a parallel interrupt

12 cycles
A virtual sequence coordinates APB configure, then AXI traffic with a parallel interruptAPB configuration runs first (sequential)APB configuration runs…then AXI traffic begins — ordered after configthen AXI traffic begin…interrupt sub-sequence runs in parallel with traffic (fork)interrupt sub-sequence…coordinated scenario complete — from one virtual sequencecoordinated scenario c…clkapb_activeaxi_activeirq_activephaseCFGCFGCFG--TRFTRFTRFTRFTRFTRF----t0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — coordinated multi-interface activity from one virtual sequence. apb_active shows the APB configuration running first (sequential — it must finish before traffic). axi_active then shows AXI traffic, and irq_active shows an interrupt sub-sequence running in parallel with the traffic (fork/join). The ordering — APB before AXI — and the overlap — AXI with interrupts — are both expressed in the virtual sequence's body(). One orchestrator produces this coordinated cross-interface scenario; each interface's activity comes from its own sub-sequence.

The waveform shows the coordination that single-sequencer sequences can't produce. apb_active is high first — the APB configuration runs before anything else, because the virtual sequence started it sequentially (it must finish before traffic). Then axi_active goes high — AXI traffic follows the configuration, the ordering dictated by the virtual sequence's body(). And irq_active pulses during the AXI traffic — an interrupt sub-sequence running in parallel (the virtual sequence forked it), so it overlaps the traffic. The result is a coordinated cross-interface scenario: APB before AXI (a dependency), AXI with interrupts (a concurrency) — relationships between interfaces that only exist because one orchestrator expressed them. Each interface's activity still comes from its own sub-sequence on its own real sequencer (the APB config drives APB, the AXI traffic drives AXI); the virtual sequence contributes no activity of its own — it contributes the timing relationships. This is the visual signature of a virtual sequence: multiple interfaces, active in a deliberate ordered/overlapped pattern that reflects a system scenario, all traceable to one conducting sequence. The waveform is the performance; the virtual sequence was the score.

DebugLab — the virtual sequence that crashed on a null sequencer

A virtual sequence that crashed because a virtual-sequencer handle was never connected

Symptom

A virtual sequence was written to configure over APB then run AXI traffic. It crashed immediately with a null-handle dereference the moment it tried to start the AXI sub-sequence — tr.start(p_sequencer.axi_sqr) faulted because p_sequencer.axi_sqr was null. Oddly, the APB part worked: cfg.start(p_sequencer.apb_sqr) ran fine and configured the DUT. Only the AXI sub-sequence's start crashed, on a null sequencer.

Root cause

The env's connect_phase connected the APB handle but forgot the AXI handle — so v_sqr.axi_sqr stayed null, and starting a sub-sequence on it crashed:

why one sub-sequence start crashed on null
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
env connect_phase:
  v_sqr.apb_sqr = apb_agent.sequencer;   // ✓ APB handle connected
  // ✗ MISSING: v_sqr.axi_sqr = axi_agent.sequencer;   ← AXI handle NEVER connected
 
→ v_sqr.axi_sqr stays NULL (a declared handle is null until assigned)
 
virtual sequence body:
  cfg.start(p_sequencer.apb_sqr);   // ✓ apb_sqr connected → works
  tr.start (p_sequencer.axi_sqr);   // ✗ axi_sqr is NULL → start on null sequencer → CRASH
 
fix — connect EVERY virtual-sequencer handle in connect_phase:
  v_sqr.apb_sqr = apb_agent.sequencer;
  v_sqr.axi_sqr = axi_agent.sequencer;   // ✓ now p_sequencer.axi_sqr points at the real sequencer

This is the canonical virtual-sequence wiring bug. A virtual sequencer declares handles to the real sequencers, but those handles are null until connected — the connection is a manual assignment in the env's connect_phase (Figure 2). Here the env connected the APB handle but omitted the AXI one, so v_sqr.axi_sqr stayed null. The virtual sequence's body() reached the APB sequencer fine (p_sequencer.apb_sqr was connected, so the config ran) but, on the next line, tried to start the AXI sub-sequence on p_sequencer.axi_sqr — a null sequencer — which crashed with a null dereference. The telltale is exactly this partial behavior: some interfaces work and one crashes, which points not at the virtual sequence (it's correct) but at a missing connection for the failing interface. The fix is to connect every handle in connect_phasev_sqr.axi_sqr = axi_agent.sequencer — so each p_sequencer.<sqr> points at its real sequencer. The general rule, and the three-step discipline from the Verification section: a virtual sequencer's handles must be declared, connected, and used — and a crash starting a sub-sequence on p_sequencer.<sqr> almost always means the connect step was skipped for that handle, leaving it null. The bug is in the env's wiring, not the virtual sequence.

Diagnosis

The tell is a null crash starting a sub-sequence on p_sequencer.<sqr>. Diagnose virtual-sequencer wiring bugs:

  1. Identify which handle is null. The crash is at sub.start(p_sequencer.X_sqr)X_sqr is the unconnected handle. A partial failure (some interfaces work, one crashes) points right at the missing connection.
  2. Check connect_phase for that handle's assignment. Confirm the env assigns v_sqr.X_sqr = X_agent.sequencer. Its absence is the cause.
  3. Confirm the working handles were connected. The interfaces that do work had their handles connected — contrast them with the failing one to spot the omission.
  4. Verify p_sequencer itself isn't null. If all p_sequencer.* accesses fail, the `uvm_declare_p_sequencer may be missing or mismatched (Module 9.7) — a different bug from one unconnected handle.
Prevention

Connect every handle, and verify the wiring:

  1. Assign every virtual-sequencer handle in connect_phase. For each real sequencer the virtual sequence will use, add v_sqr.X_sqr = X_agent.sequencer — none can be skipped.
  2. Keep the handle list and the connections in sync. When you add a handle to the virtual sequencer (for a new interface), add its connection too; a declared-but-unconnected handle is a latent null crash.
  3. Declare p_sequencer on the virtual sequence. With the correct virtual-sequencer type, so p_sequencer.<sqr> access works at all (Module 9.7).
  4. Test the wiring early. A quick scenario that touches every interface surfaces an unconnected handle immediately, rather than later when a specific sub-sequence runs.

The one-sentence lesson: a virtual sequencer's handles to the real sequencers are null until connected in the env's connect_phase, so starting a sub-sequence on an unconnected p_sequencer.<sqr> crashes on a null sequencer — connect every handle (v_sqr.X_sqr = X_agent.sequencer), because a partial failure where one interface crashes while others work is the signature of one missing connection.

Common Mistakes

  • Forgetting to connect a virtual-sequencer handle. A declared handle is null until assigned in connect_phase; starting a sub-sequence on a null handle crashes. Connect every handle.
  • Adding a handle without its connection. A new interface's handle on the virtual sequencer needs a matching connect_phase assignment, or it's a latent null crash.
  • Not declaring p_sequencer on the virtual sequence. Without `uvm_declare_p_sequencer(virtual_sequencer), the virtual sequence can't reach the handles (Module 9.7).
  • Giving the virtual sequencer/sequence an item type or driver. A virtual sequencer drives nothing and a virtual sequence has no items; they coordinate, not drive.
  • Scattering coordination in the test instead of a virtual sequence. Ad-hoc multi-sequence timing in the test loses reuse; put the cross-interface scenario in one virtual sequence.
  • Wrong sequential/parallel choice across interfaces. Dependent steps (configure before traffic) must be sequential; independent ones can fork — the same control rules as Module 9.5, applied across sequencers.

Senior Design Review Notes

Interview Insights

A virtual sequence is a top-level orchestrator that coordinates sub-sequences across multiple sequencers, used to express whole-system scenarios that span several interfaces — for example, configure the DUT over APB, then run data traffic over AXI, while a third agent watches for interrupts. It's called virtual because it drives no interface itself: it has no items and owns no driver, so it produces no interface activity directly. Instead, it runs on a virtual sequencer, which is a sequencer with no driver whose only role is to hold handles to the real agents' sequencers, and the virtual sequence's body() starts sub-sequences on those real sequencers, reached through p_sequencer. Those sub-sequences are normal sequences that produce items, and their drivers drive the actual interfaces, so all the real activity comes from them; the virtual sequence contributes the coordination — the ordering and concurrency between interfaces — not the driving. This is why "virtual" fits: it coordinates without driving, the way a conductor leads an orchestra without playing an instrument. The benefit is that a multi-interface system scenario, with all its cross-interface timing relationships, lives in one reusable sequence started once on the virtual sequencer, rather than being scattered as ad-hoc multi-sequence timing in the test. So a virtual sequence is the system-scenario abstraction: the single place where the relationships between interfaces are written, sitting a level above the per-interface sequences that actually drive.

A virtual sequencer is a sequencer with no driver that holds handles to the real agents' sequencers, serving as the hub through which a virtual sequence reaches multiple interfaces. Setting it up has three parts. First, you declare it as a sequencer whose members are handles to the real sequencers — for example, an apb_sequencer handle and an axi_sequencer handle — and it has no item type because it drives nothing. Second, and critically, you connect those handles to the actual agent sequencers in the env's connect_phase, assigning each one: v_sqr.apb_sqr = apb_agent.sequencer, v_sqr.axi_sqr = axi_agent.sequencer. This wiring is essential because a declared handle is just a reference that starts null; it isn't automatically pointed at anything, so without the assignment it stays null. Third, the virtual sequence declares p_sequencer with the virtual sequencer type so it can reach the handles, and uses p_sequencer.apb_sqr and p_sequencer.axi_sqr to start sub-sequences on the real sequencers. The most common setup bug is forgetting the connect step for one of the handles, which leaves that handle null and crashes the virtual sequence when it tries to start a sub-sequence on it — and the giveaway is that some interfaces work while one crashes. So the discipline is declare the handles, connect every one in connect_phase, and use them via p_sequencer; the virtual sequencer borrows the real sequencers by holding handles, and the borrowing only works once the env hands those handles over.

Exercises

  1. Build the virtual sequencer. Sketch a virtual sequencer with handles to an APB and an AXI sequencer, and the env connect_phase lines that wire them.
  2. Write the virtual sequence. Write a virtual sequence body() that configures over APB then runs AXI traffic, reaching each via p_sequencer, and say where the objection goes.
  3. Diagnose the partial crash. A virtual sequence configures over APB fine but crashes starting AXI traffic on a null sequencer. Name the cause and fix.
  4. Order vs overlap. For "configure (APB), then run traffic (AXI) with concurrent interrupts," state which sub-sequences are sequential and which are forked, and why.

Summary

  • A virtual sequence is a top-level orchestrator that coordinates sub-sequences across multiple sequencers; it's virtual because it drives no interface itself — no items, no driver — and contributes only the coordination.
  • It runs on a virtual sequencer: a driverless sequencer that holds handles to the real agents' sequencers, which the virtual sequence reaches via `uvm_declare_p_sequencer + p_sequencer.<real_sqr> to start sub-sequences on each interface.
  • The handles must be connected in the env's connect_phase (v_sqr.X_sqr = X_agent.sequencer) — a declared handle is null until assigned, so starting a sub-sequence on an unconnected handle crashes (the partial-failure signature).
  • The virtual sequence's body() choreographs sub-sequences across interfaces with the same sequential/parallel control as single-sequencer composition (Module 9.5) — dependent steps sequential (configure before traffic), independent ones forked — so cross-interface ordering and concurrency live in one place.
  • The durable rule of thumb: express a whole-system scenario as one virtual sequence on a virtual sequencer — declare handles to every real sequencer, connect them all in connect_phase, declare p_sequencer, and start sub-sequences across interfaces in the scenario's order — because the virtual sequence conducts (coordinates) while the sub-sequences on the real sequencers play (drive), and an unconnected handle is the one wiring miss that crashes it.

Next — Layered Sequences: virtual sequences coordinate across interfaces; layering structures stimulus within one — the next chapter goes deeper on building abstraction tiers, where high-level sequences translate into lower-level ones through a layering protocol, refining the composition introduced here.