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 theuvm_domacros, 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_domacros remove boilerplate, uniformly. Every item or sub-sequence needs the same create/start/randomize/finish steps.`uvm_doencapsulates 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 abody()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 amain()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: itsbody()invokes sub-sequences the waymain()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 withforkand 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 frommain()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.
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 sequence — burst_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.
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.
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 handshake — start_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.
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 cyclesThe 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
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.
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:
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 completeThis 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."
The tell is parallel sub-sequences truncated, but fine when run alone. Diagnose parallel-lifetime bugs:
- Check the
forkjoin type. Afork/join_none(orjoin_any) that isn't followed by a later wait letsbody()return while branches still run — the signature.joinwaits for all. - 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. - 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.
- 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.
Wait for what you launch:
- Use
joinfor parallel sub-sequences you need to complete. It blocksbody()until all branches finish, so the parent (and the objection) outlive the sub-sequences. Reservejoin_none/join_anyfor cases where you deliberately manage lifetime otherwise. - 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.
- Prefer sequential unless concurrency is needed. Sequential composition is automatically safe (each blocks); reach for
forkonly when sub-sequences genuinely must overlap. - Mind atomicity within parallel branches. Concurrent sub-sequences interleave on the sequencer (Module 9.2); if a branch's items must stay together,
lock/grabit.
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_nonewithout a later wait. The parent returns while forked sub-sequences run, and the phase end kills them; usejointo 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); omittingthisloses 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_dosuffices.`uvm_do/`uvm_do_withhandle 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
- 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 — usinguvm_doand the correct join. - Sequential vs parallel. For (a) "configure then run traffic" and (b) "traffic while interrupts fire," state which control structure each needs and why.
- Fix the truncation. A parent forks two sub-sequences with
join_noneand they truncate. Rewrite it so both complete, and explain what was killing them. - Trace the macro. Explain what
`uvm_do(x)does whenxis an item versus whenxis 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_domacros build the tree:`uvm_do(x)createsxand either runs the item handshake (ifxis an item) or starts it as a child sub-sequence (ifxis a sequence) — one uniform call at every node, with randomization placed correctly;`uvm_do_withadds inline constraints. - Control is ordinary procedural structure: consecutive
`uvm_docalls run sub-sequences sequentially (each blocks until done — ordered phases); afork/joinaround them runs them in parallel (concurrent, interleaved on the sequencer), withjoinwaiting for all. - Parallel sub-sequences must be waited for:
fork/join_nonewithout a later wait lets the parentbody()return while branches run, and the phase end then kills them —join(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/joinfor concurrent traffic — always joining (or keeping the objection raised for) parallel branches, andlock/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.