Skip to content

UVM

Sequence Control

Orchestrating stimulus above the single item — a sequence's body() runs sub-sequences (not just items), composed sequentially or in parallel, built with the uvm_do macros into a tree of reusable stimulus.

Sequences · Module 9 · Page 9.5

The Engineering Problem

The last two chapters drilled the single-item handshake — start_item and finish_item (Modules 9.3–9.4). But a real scenario is rarely one item, or even one flat stream of items. It's structured: a configuration phase, then a burst of traffic, then an interrupt, then a drain — and each of those is itself a meaningful unit you'd want to write once and reuse. Generating that by hand-coding every item in one giant body() is unmaintainable and unreusable. You need to compose and control stimulus above the item level.

Sequence control is exactly that: a sequence's body() can run other sequences, not just items — so small, reusable sequences (a read, a write, a burst) compose into larger ones (a traffic scenario, a whole test), forming a tree of stimulus. You build that tree with the uvm_do family of macros (which create, start, randomize, and finish an item or a sub-sequence in one call), and you control how sub-sequences run: sequentially (one after another, in order) or in parallel (concurrently, via fork/join). This chapter is sequence control: composing sub-sequences into a hierarchy, the uvm_do macros that build it, the difference between sequential and parallel execution, and why parallel sub-sequences launched without waiting get killed when the phase ends.

How do you control stimulus above the single item — running sub-sequences from a body(), building them with the uvm_do macros, and composing them sequentially or in parallel — and why must parallel sub-sequences be waited for, not fired and forgotten?

Motivation — why stimulus must compose

Sequence control exists because real scenarios are structured and reusable, and a flat stream of items captures neither:

  • Scenarios have structure, so stimulus needs hierarchy. A test is "configure, then run traffic, then inject an interrupt" — a sequence of sub-scenarios, each itself made of items. Letting a body() call sub-sequences lets the structure of the stimulus mirror the structure of the test, instead of flattening it into one undifferentiated item stream.
  • Sub-scenarios are reusable, so they should be separate sequences. A "burst read" or a "register configure" is useful in many tests. Making each a sequence — and composing them — means you write it once and reuse it, the same reuse argument as for sequences over hand-coded items, applied one level up.
  • The uvm_do macros remove boilerplate, uniformly. Every item or sub-sequence needs the same create/start/randomize/finish steps. `uvm_do encapsulates them in one call that works for both items and sequences, so composing is concise and the per-step order (especially randomize placement, Module 9.3) is always correct.
  • Order and concurrency are part of the scenario, so control must express both. Some sub-scenarios must run in order (configure before traffic); others should run together (background traffic plus a periodic interrupt). Ordinary procedural control — sequential calls vs fork/join — lets a body() express either.
  • Parallel stimulus needs lifetime management. Concurrent sub-sequences keep running until done; if the parent doesn't wait for them, they can be cut off when the parent finishes and the phase ends (Module 9.1's objection). Control includes waiting for what you launched.

The motivation, in one line: real scenarios are hierarchical, reusable, and a mix of ordered and concurrent — so sequence control lets a body() compose sub-sequences into a tree (built with uvm_do), run them sequentially or in parallel, and manage their lifetime, turning flat item generation into structured, reusable stimulus.

Mental Model

Hold sequence control as a program's call tree:

A sequence's body() is like a main() that can call other procedures (sub-sequences) — in order like function calls, or concurrently like spawned threads — building a call tree whose leaves emit items. A leaf sequence is a small routine that does the low-level work: it emits items (a burst of reads). A higher sequence is a routine that calls other routines: its body() invokes sub-sequences the way main() calls functions. Call them one after another and they run in order (configure(), then run_traffic(), then drain()) — each returns before the next starts. Spawn them with fork and they run concurrently (background_traffic() alongside periodic_interrupt()), like launching threads. Either way you're building a tree: the top routine orchestrates sub-routines, which orchestrate sub-routines, down to leaves that emit items — and the whole tree is the scenario. The one threading rule that bites: if you spawn concurrent routines (fork) and then return from main() without joining them, they get terminated when the program exits — so you must wait (join) for the threads you started, or they die unfinished.

So composing stimulus is writing a call tree: small leaf sequences do items, orchestrating sequences call sub-sequences sequentially (ordered) or via fork (concurrent), and you join what you fork. The uvm_do macros are the concise "call this routine" syntax that works at every level of the tree.

Visual Explanation — the sequence composition tree

The defining picture is a tree: a top sequence orchestrating sub-sequences, which orchestrate more, down to leaf sequences that emit items.

A top sequence runs sub-sequences which run leaf sequences which emit items — a composition treerunsrunsrunsrunsrunsemitstop_seq (orchestrator)body() runs sub-sequencesconfigure_seqsub-scenariotraffic_seqsub-scenariointerrupt_seqsub-scenarioburst_read_seqleaf sequenceburst_write_seqleaf sequenceitemsstart_item/finish_item12
Figure 1 — sequences compose into a tree. A top sequence's body() runs sub-sequences (configure, traffic, interrupt) rather than items directly. Each sub-sequence is itself a sequence: the traffic sub-sequence runs leaf sequences (burst_read, burst_write) that emit items. So the stimulus is a tree — orchestrating sequences at the top, leaf sequences emitting items at the bottom. Each node is a reusable sequence, composed by a parent's body(). The structure of the stimulus mirrors the structure of the scenario.

The figure shows stimulus as a composition tree, which is the central idea of sequence control. At the top, top_seq's body() doesn't emit items directly — it runs sub-sequences: configure_seq, traffic_seq, interrupt_seq, each a meaningful sub-scenario. Those are themselves sequences: traffic_seq's body() runs leaf sequences (burst_read_seq, burst_write_seq), and only at the leaves do sequences emit items via the handshake (Modules 9.3–9.4). So the stimulus forms a tree: orchestrating sequences at the top, leaf sequences at the bottom, items at the very bottom. Every node is a reusable sequenceburst_read_seq can be a leaf under many parents, configure_seq can be reused across tests — and the tree is assembled by each parent's body() calling its children. This is the same reuse principle as the whole module, applied recursively: just as a sequence is reusable stimulus on a fixed structure, a sub-sequence is reusable stimulus within a larger sequence. The shape of the tree is the shape of the scenario — "configure, then traffic (reads and writes), then an interrupt" reads directly off the structure — which is exactly what makes composed stimulus both maintainable and reusable. Everything else in this chapter is how you build and control this tree.

RTL / Simulation Perspective — running sub-sequences, sequentially and in parallel

A body() runs sub-sequences with the uvm_do macros (or explicit start), and ordinary procedural structure — sequential calls vs fork/join — controls how they run.

composing sub-sequences: sequential and parallel
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class top_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(top_seq)
  function new(string name="top_seq"); super.new(name); endfunction
 
  task body();
    configure_seq   cfg;
    traffic_seq     tr;
    interrupt_seq   irq;
 
    // ── SEQUENTIAL: each sub-sequence runs to completion before the next ──
    `uvm_do(cfg)                       // create + start (on this sequencer, as a child) + run
    `uvm_do(tr)                        // runs only after cfg finishes — ordered
 
    // ── PARALLEL: run two sub-sequences concurrently, and WAIT for both ──
    fork
      `uvm_do(tr)                      // background traffic
      `uvm_do(irq)                     // periodic interrupt, at the same time
    join                              // ← wait for BOTH to finish before body() returns
 
    // ── explicit form (what uvm_do does for a sequence) ──
    // sub = sub_seq::type_id::create("sub");
    // sub.start(m_sequencer, this);   // start on this sequencer, with THIS as parent sequence
  endtask
endclass
 
// ── uvm_do_with: a sub-sequence (or item) with inline constraints ──
//   `uvm_do_with(tr, { tr.num inside {[10:50]}; })

The code shows composition and control together. Running a sub-sequence is `uvm_do(cfg) — the macro creates the sub-sequence, starts it on the current sequencer as a child of this sequence (passing this as the parent so hierarchy, priority, and locks are inherited), and runs its body(); for a sub-sequence, `uvm_do calls start, while for an item the same macro does the start_item/randomize/finish_item handshake — one macro, both levels. Sequential composition is just consecutive `uvm_do calls: `uvm_do(cfg) then `uvm_do(tr) runs them in order, because each blocks until its sub-sequence finishes (a sub-sequence's start, like an item's finish_item, is blocking). Parallel composition wraps `uvm_do calls in fork/join: the tr and irq sub-sequences run concurrently, and join makes body() wait for both before continuing. The explicit form (commented) shows what `uvm_do does for a sequence — sub.start(m_sequencer, this) — making the parent linkage visible. And `uvm_do_with adds inline constraints, exactly like for items (Module 9.3). The shape to carry: consecutive uvm_do = sequential, fork/join around uvm_do = parallel, and the macro is the uniform "run this" at every node of the tree.

Verification Perspective — what the uvm_do macros do

The uvm_do macros are the workhorse of composition, and their value is that one macro handles the full create/start/randomize/finish ritual for both items and sub-sequences — so the tree is built uniformly and correctly.

uvm_do creates x, then runs the item handshake or starts the sub-sequence, uniformlyuvm_do(x): create → (item: handshake) or (sequence: start) → doneuvm_do(x): create → (item: handshake) or (sequence: start) → done1Create xtype_id::create — works for an item or a sub-sequence.2Item or sub-sequence?the macro handles both — the same call composes at the item leveland the sequence level.3aItem → handshakestart_item → randomize (in the window) → finish_item (Modules9.3–9.4).3bSub-sequence → startstart(m_sequencer, this) — runs its body() as a child of thissequence.
Figure 2 — what uvm_do does, for an item and for a sub-sequence. uvm_do(x) creates x, then: if x is a sequence item, it runs the handshake (start_item → randomize → finish_item); if x is a sub-sequence, it starts it (start on the current sequencer, as a child) which runs its body(). Either way it's one call that encapsulates the full ritual, with randomization placed correctly (in the window). uvm_do_with adds inline constraints. The macro makes composing items and sub-sequences uniform.

The macros earn their place by making composition uniform and correct. `uvm_do(x) first creates x (via the factory, so it's overridable, Module 6). Then it branches on what x is: if x is a sequence item, the macro runs the full per-item handshakestart_item, then randomize in the window, then finish_item (Modules 9.3–9.4) — so the item is generated correctly, with randomization in exactly the right place. If x is a sub-sequence, the macro starts it — start(m_sequencer, this) — running its body() as a child of the current sequence (inheriting sequencer, parent linkage, priority). The power is that the same macro composes at both levels: a body() builds its part of the tree with `uvm_do calls whether the children are items (at a leaf) or sub-sequences (at an orchestrator), without the author writing the create/start/randomize/finish boilerplate or risking misplaced randomization. `uvm_do_with adds inline constraints (e.g., constrain a sub-sequence's transaction count), and variants like `uvm_do_pri add priority (Module 9.2). The trade-off — the same as any macro — is some hidden mechanism: when you need fine control (split create and send, custom timing), you drop to the explicit create/start/start_item/finish_item calls the macro wraps. But for building the tree, `uvm_do is the concise, correct, uniform tool, which is why composed sequences are written almost entirely in terms of it.

Runtime / Execution Flow — sequential vs parallel control

At run time, the control structure in a body() — consecutive calls vs fork/join — determines whether sub-sequences run one-after-another or concurrently. The two produce very different stimulus.

Sequential consecutive uvm_do runs in order; fork/join runs concurrently and waitsA done → B startsconcurrentconcurrentSequential: `uvm_do(A)runs to completion firstthen `uvm_do(B)starts after A — orderedParallel: fork `uvm_do(A)runs concurrently`uvm_do(B)runs concurrentlyjoin — wait for bothbody() continues after bothfinish12
Figure 3 — sequential vs parallel sub-sequence control. Sequential: consecutive uvm_do calls run sub-sequences in order — each blocks until done, so A fully completes before B starts. Parallel: a fork around uvm_do calls runs them concurrently — A and B's items interleave (arbitrated by the sequencer, Module 9.2) — and join waits for both before body() continues. Use sequential for ordered phases (configure then run); parallel for concurrent traffic (background plus interrupt). The catch: forked sub-sequences not joined can be killed when the phase ends.

The control structure is ordinary SystemVerilog process control, applied to sub-sequences. Sequential composition is consecutive `uvm_do calls: because running a sub-sequence blocks until it finishes (its start returns only when its body() is done), `uvm_do(A) then `uvm_do(B) runs A completely before B begins — the right structure for ordered phases (configure then run traffic then drain). Parallel composition wraps the calls in fork/join: A and B run concurrently, their items interleaving on the shared sequencer (arbitrated per Module 9.2), and join makes body() wait for both to finish before continuing — the right structure for concurrent traffic (background reads while an interrupt fires). The choice is part of the scenario's meaning: sequential expresses "this, then that"; parallel expresses "these, together." The runtime catch is in the parallel case: fork/join waits, but fork/join_none (or join_any) does not wait for all branches — and a forked sub-sequence that the parent doesn't wait for can still be running when the parent body() returns. If that return leads to the objection dropping and the phase ending (Module 9.1), the unfinished sub-sequence is killed (the DebugLab). So parallel control isn't just "start them" — it's "start them and ensure they complete," which join does and join_none-without-a-later-wait does not. Sequential is automatically safe (each blocks); parallel requires you to manage the wait.

Waveform Perspective — sequential vs parallel on a timeline

The two control structures produce visibly different stimulus: sequential sub-sequences run in non-overlapping blocks; parallel ones overlap and interleave.

Sequential sub-sequences (non-overlapping) vs parallel (interleaved)

12 cycles
Sequential sub-sequences (non-overlapping) vs parallel (interleaved)sequential: A runs to completion (A0,A1)sequential: A runs to …sequential: then B runs (B0,B1) — non-overlappingsequential: then B run…parallel: A and B run concurrently (fork/join)parallel: A and B run …parallel: their items interleave (A,B,A,B), arbitrated by the sequencerparallel: their items …clkmodeSEQSEQSEQSEQ--PARPARPARPARPARPAR--seqAseqBitemA0A1B0B1--A2B2A3B3A4B4--t0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — sequential vs parallel control. In the SEQUENTIAL region, sub-sequence A runs to completion (seqA high), then sub-sequence B (seqB high) — non-overlapping blocks, A fully before B. In the PARALLEL region, A and B run concurrently (both high together), and their items interleave on the driver (item shows A,B,A,B). Sequential expresses 'A then B'; parallel expresses 'A and B together'. The control structure in body() — consecutive calls vs fork/join — chooses which.

The waveform contrasts the two control structures directly. In the SEQUENTIAL region, seqA is high while sub-sequence A runs (emitting A0, A1), and only after A finishes does seqB go high and B run (B0, B1) — non-overlapping blocks, because consecutive `uvm_do calls block. This is "A then B." In the PARALLEL region, seqA and seqB are high together — both sub-sequences run concurrently (fork/join) — and their items interleave on the driver (A2, B2, A3, B3…), because both feed the one sequencer, which arbitrates between them per item (Module 9.2). This is "A and B together." The picture makes the control choice concrete: the same two sub-sequences produce a serialized stimulus stream or an interleaved one purely based on whether body() calls them consecutively or forks them. It also previews two earlier ideas: the parallel interleaving is exactly the arbitration of Module 9.2 (and if A were an atomic operation, it would need lock/grab to keep its items together), and the parallel branches must be joined (or the phase kept alive) or they'd be cut off mid-stream. The control structure in body() is the knob; the waveform is its effect.

DebugLab — the parallel sub-sequences that got cut off

Forked sub-sequences killed because the parent didn't wait for them

Symptom

A top sequence launched a long background-traffic sub-sequence and a periodic-interrupt sub-sequence to run concurrently. But both produced only a fraction of their expected stimulus before the simulation ended — the background traffic sent a few items instead of hundreds, the interrupt fired once instead of repeatedly. Run individually (not forked), each sub-sequence ran to completion fine. The truncation happened only when they were launched in parallel from the parent.

Root cause

The parent forked the sub-sequences with fork/join_none and then returned without waiting — so when body() returned and the objection dropped, the still-running sub-sequences were killed:

why forked sub-sequences were truncated
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
top_seq::body():
  fork
    `uvm_do(bg_traffic)     // long-running — hundreds of items
    `uvm_do(periodic_irq)   // long-running — repeated interrupts
  join_none                 // ✗ does NOT wait — body() continues immediately
  // body() returns NOW, while bg_traffic and periodic_irq are still running
 
test::run_phase:  seq.start(...) returns → drop_objection → run_phase ENDS
                  → the still-running forked sub-sequences are KILLED mid-stream
 
result:  background traffic and interrupt are truncated to whatever ran before the phase ended
fix — wait for the forked sub-sequences with join:
  fork
    `uvm_do(bg_traffic)
    `uvm_do(periodic_irq)
  join                      // ✓ body() waits for BOTH to finish before returning
                            //   → objection stays raised → run_phase stays alive → they complete

This is a lifetime bug in parallel control, and it ties to the objection mechanism (Module 9.1). fork/join_none launches the sub-sequences concurrently but does not wait for them — body() continues (and returns) immediately after the fork. When top_seq.body() returns, control goes back to the test's seq.start(...), which returns, and the test drops its objection; with no objections, run_phase ends — and ending the phase kills the still-running forked sub-sequences. So the sub-sequences are correct; they're simply terminated before they finish because nothing waited for them or kept the phase alive. (Running them individually works because each blocks to completion.) The fix is to wait for what you fork: join makes body() block until both branches finish, so the parent doesn't return — and the objection isn't dropped — until the sub-sequences are done. The general rule for parallel control: a forked sub-sequence must be waited for (join) or otherwise have the phase kept alive for its duration, because fork/join_none without a later wait lets the parent (and the phase) finish out from under it. Parallel control is "start them and ensure they complete," not just "start them."

Diagnosis

The tell is parallel sub-sequences truncated, but fine when run alone. Diagnose parallel-lifetime bugs:

  1. Check the fork join type. A fork/join_none (or join_any) that isn't followed by a later wait lets body() return while branches still run — the signature. join waits for all.
  2. Trace the objection to the parent's return. If body() returns while forked sub-sequences run, and the test then drops its objection, the phase ends and kills them. Confirm the objection covers the whole parallel duration.
  3. Compare individual vs parallel runs. A sub-sequence that completes alone but truncates in parallel points at the wait/lifetime, not the sub-sequence's own logic.
  4. Confirm where it's cut off. Truncation at the moment the phase ends (rather than a hang or error mid-sequence) is the cut-off-by-phase-end signature.
Prevention

Wait for what you launch:

  1. Use join for parallel sub-sequences you need to complete. It blocks body() until all branches finish, so the parent (and the objection) outlive the sub-sequences. Reserve join_none/join_any for cases where you deliberately manage lifetime otherwise.
  2. Keep the objection raised for the whole parallel duration. If sub-sequences must run to a point, ensure an objection is held until then — don't drop it while they're still running.
  3. Prefer sequential unless concurrency is needed. Sequential composition is automatically safe (each blocks); reach for fork only when sub-sequences genuinely must overlap.
  4. Mind atomicity within parallel branches. Concurrent sub-sequences interleave on the sequencer (Module 9.2); if a branch's items must stay together, lock/grab it.

The one-sentence lesson: parallel sub-sequences launched with fork/join_none and not waited for are killed when the parent body() returns and the phase ends — so join the sub-sequences you fork (or keep the objection raised for their duration), because parallel control means "start them and ensure they complete," not just start them.

Common Mistakes

  • fork/join_none without a later wait. The parent returns while forked sub-sequences run, and the phase end kills them; use join to wait for what must complete.
  • Dropping the objection while parallel sub-sequences run. Ending the phase kills them mid-stream; keep the objection raised for their whole duration.
  • Not passing the parent (this) when starting a sub-sequence. sub.start(m_sequencer, this) links the child to the parent (priority, locks, hierarchy); omitting this loses that context.
  • Hand-coding deep item streams instead of composing. A flat body() of items isn't reusable; factor reusable sub-scenarios into sub-sequences and compose them.
  • Forgetting that parallel branches interleave. Concurrent sub-sequences share the sequencer and arbitrate per item (Module 9.2); an atomic branch needs lock/grab.
  • Reaching for the explicit form when uvm_do suffices. `uvm_do/`uvm_do_with handle the common case correctly; use the explicit create/start only when you need control they don't give.

Senior Design Review Notes

Interview Insights

By composing sequences into a hierarchy: a sequence's body() can run other sequences, not just items, so small reusable sequences combine into larger ones, forming a tree of stimulus. At the leaves, sequences emit items through the start_item/finish_item handshake; higher up, orchestrating sequences run sub-sequences. You build this tree with the uvm_do family of macros, which create, start, randomize, and finish an item or a sub-sequence in one call — the same macro composes at both levels. And you control how sub-sequences run with ordinary procedural structure: calling them consecutively runs them sequentially, in order, because running a sub-sequence blocks until it finishes; wrapping the calls in fork/join runs them concurrently and waits for all to complete. So a scenario like "configure, then run traffic, then inject an interrupt" is a top sequence whose body() runs a configure sub-sequence, then a traffic sub-sequence (which itself runs leaf read/write sequences), then an interrupt sub-sequence — with sequential composition for the ordered phases and fork/join where things must overlap, like background traffic alongside a periodic interrupt. The benefit is the same reuse argument as sequences over hand-coded items, applied one level up: each sub-scenario is a sequence written once and reused, and the structure of the stimulus mirrors the structure of the test. The one lifetime rule is that parallel sub-sequences must be waited for with join, or they get killed when the parent finishes and the phase ends.

uvm_do is a macro that performs the full create-and-run ritual for a sequence item or a sub-sequence in a single call. For an item, uvm_do(item) creates it via the factory, then runs the handshake — start_item, randomize in the window, finish_item — so the item is generated with randomization in exactly the right place. For a sub-sequence, uvm_do(sub) creates it, then starts it with start on the current sequencer as a child of the current sequence, which runs its body(). The key point is that the same macro handles both, so you compose a tree uniformly: a body() builds its part with uvm_do calls whether the children are items at a leaf or sub-sequences at an orchestrator, without writing the create/start/randomize/finish boilerplate or risking misplaced randomization. There are variants: uvm_do_with adds inline constraints, for example constraining a sub-sequence's transaction count or an item's address; and priority variants like uvm_do_pri add an arbitration priority. The trade-off is the usual macro trade-off — it hides the mechanism — so when you need finer control, like splitting create from send or custom timing, you drop to the explicit create, start, start_item, and finish_item calls the macro wraps. There are also uvm_create and uvm_send for that split. But for the common case of building the stimulus tree, uvm_do and uvm_do_with are the concise, correct, uniform tools, which is why composed sequences are written almost entirely in terms of them.

Exercises

  1. Compose a scenario. Write a top sequence body() that runs a configure sub-sequence, then runs background-traffic and interrupt sub-sequences concurrently to completion — using uvm_do and the correct join.
  2. Sequential vs parallel. For (a) "configure then run traffic" and (b) "traffic while interrupts fire," state which control structure each needs and why.
  3. Fix the truncation. A parent forks two sub-sequences with join_none and they truncate. Rewrite it so both complete, and explain what was killing them.
  4. Trace the macro. Explain what `uvm_do(x) does when x is an item versus when x is a sub-sequence.

Summary

  • Sequence control composes stimulus above the item: a sequence's body() runs sub-sequences, not just items, so reusable sequences form a tree — orchestrating sequences at the top, leaf sequences emitting items at the bottom, mirroring the scenario's structure.
  • The uvm_do macros build the tree: `uvm_do(x) creates x and either runs the item handshake (if x is an item) or starts it as a child sub-sequence (if x is a sequence) — one uniform call at every node, with randomization placed correctly; `uvm_do_with adds inline constraints.
  • Control is ordinary procedural structure: consecutive `uvm_do calls run sub-sequences sequentially (each blocks until done — ordered phases); a fork/join around them runs them in parallel (concurrent, interleaved on the sequencer), with join waiting for all.
  • Parallel sub-sequences must be waited for: fork/join_none without a later wait lets the parent body() return while branches run, and the phase end then kills themjoin (or keeping the objection raised) ensures they complete.
  • The durable rule of thumb: factor reusable sub-scenarios into sub-sequences and compose them with uvm_do — sequentially for ordered phases, fork/join for concurrent traffic — always joining (or keeping the objection raised for) parallel branches, and lock/grab-ing any branch whose items must stay atomic, because composed stimulus is a tree you both build and must keep alive.

Next — Random Traffic Generation: with composition and control in hand, the next chapter turns to generating random stimulus at scale — constrained-random sequences, randomizing sub-sequence selection and counts, and building the varied traffic that drives coverage closure.