UVM
Sequence Basics
What a sequence is — a transient stimulus program whose body() task generates a stream of sequence items, run on a sequencer with start(), separating the 'what stimulus' from the structure that drives it.
Sequences · Module 9 · Page 9.1
The Engineering Problem
The previous module modeled the unit of stimulus — the transaction (Module 8). But a single transaction is not a test. A test is a scenario: a burst of reads followed by a write, a flood of randomized traffic, an error injected at a precise moment. Something has to produce the stream of transactions in a meaningful order — and crucially, that "what to generate" has to be separable from the agent that drives it, or every new scenario would mean rebuilding the testbench. That something is the sequence.
A sequence is a transient stimulus program: a uvm_sequence object whose body() task generates a stream of sequence items — creating them, randomizing them, and sending them through the handshake. You run a sequence on a sequencer with start(), and the sequencer routes its items to the driver. The key idea is separation: the sequence is the what (the stimulus scenario), the driver is the how (pin-level driving, Module 8.1), and the agent is the where (the permanent structure). Because the sequence is decoupled from all of that, one testbench runs many scenarios by swapping sequences — a smoke test, a stress test, and an error test are three sequences run on the same agent, with no structural change. This chapter is the sequence: what it is, how its body() produces items, how start() launches it on a sequencer, and why a sequence that's started without an objection gets killed before it finishes.
What is a sequence — how does its
body()task generate a stream of sequence items, how doesstart()run it on a sequencer, and why does separating the stimulus (the sequence) from the structure (the agent) let one testbench run many scenarios?
Motivation — why stimulus is a separate, transient thing
The sequence exists because stimulus generation has different requirements from the testbench structure, and conflating them would destroy reuse:
- Scenarios change constantly; structure doesn't. The agent, driver, and sequencer are built once and stay fixed; the scenarios you run on them change with every test. Making stimulus a separate, swappable object means new scenarios don't touch the structure — you write a new sequence, not a new testbench.
- Stimulus is a procedure, so it needs a body. A scenario is inherently procedural: do this, then that, randomize this, loop that many times. The sequence's
body()is a task — a procedure with loops, conditionals, and randomization — which is exactly what's needed to express a scenario, and which a static component's structure can't. - Stimulus is transient; components are permanent. A scenario runs, produces its items, and is done; it has no place in the permanent hierarchy. So a sequence is a transient object — created, run via
start(), and discarded — not auvm_componentthat lives for the whole simulation. - Decoupling from the driver is what enables reuse across implementations. The sequence generates transactions (the "what"); the driver turns them into pins (the "how", Module 8.1). Because the sequence never touches pins, the same sequence runs against a different driver or timing — and the same driver runs any sequence. They meet only at the sequencer.
- One sequencer, many sequences, is the whole point. Because a sequence is bound to a sequencer only at
start()time, any compatible sequence can run on a given agent. That's how a single testbench yields a whole regression: each test picks a different sequence to run.
The motivation, in one line: stimulus is a procedural, transient, structure-independent thing — so UVM makes it a separate object (the sequence) with a procedural body(), run on a sequencer via start(), which is precisely what lets one fixed testbench run an open-ended set of scenarios.
Mental Model
Hold a sequence as a program you load and run on a machine:
The testbench — agent, sequencer, driver — is a fixed machine; a sequence is a program you load and run on it to make it do something specific. The machine is built once and doesn't change: the driver knows how to operate the equipment (the pins), the sequencer is the loading dock that feeds it work. A sequence is the software — a program whose
body()is itsmain(), a procedure that says "generate this read, then that write, then ten random transfers." You load and run the program on the machine withstart(sequencer), and the machine executes the work the program produces. The power is that the same machine runs different programs: load the smoke-test program, you get a gentle run; load the stress program, you get a flood; load the error program, you get fault injection — all without rebuilding the machine. The program is transient (it runs and finishes); the machine is permanent (it waits for the next program). And like any program started as a background job, it needs the system to keep running while it works — start it without keeping the run alive (an objection) and the system shuts down before the program finishes.
So writing a scenario is writing a program (a sequence with a body()) and running it on the agent's machine (start() on the sequencer). Swap the program, change the behavior; the machine stays the same — which is the reuse the whole structure exists to provide.
Visual Explanation — stimulus program above the structure
The defining picture is separation: the sequence (transient stimulus program) sits above the permanent structure (sequencer, driver, agent), producing items that flow down through it to the DUT.
The figure shows the separation that makes sequences powerful. The sequence sits at the top as a transient program: its body() generates the stream of items — the what — but it is not part of the component hierarchy; it's created, run, and discarded. Below it is the permanent structure: the sequencer (which routes and arbitrates items), the driver (which translates items into pin activity — the how, Module 8.1), and the agent that contains them. The arrow from sequence to sequencer is start() — the act of running the program on the machine — after which items flow down through the sequencer to the driver to the DUT. The crucial reading is the dashed line between the transient top and the permanent bottom: the sequence depends on the structure (it needs a sequencer to run on), but the structure does not depend on the sequence (the sequencer and driver work with any compatible sequence). That one-directional dependency is what lets you swap the top: replace the sequence with a different one — a stress scenario, an error scenario — and the entire structure below is unchanged. The sequence is the layer you vary; the structure is the layer you fix. Everything else about sequences elaborates this one picture.
RTL / Simulation Perspective — the sequence class and its body()
A sequence is a class extending uvm_sequence, and its substance is the body() task — the procedure that generates items. Running it is create then start().
// ── THE SEQUENCE: a transient program; body() generates the item stream ──
class read_burst_seq extends uvm_sequence #(bus_item); // parameterized by the item type
`uvm_object_utils(read_burst_seq) // a uvm_object — transient, not a component
rand int unsigned num = 8; // a knob: how many reads
function new(string name="read_burst_seq"); super.new(name); endfunction
task body(); // the scenario: a procedure
bus_item req;
for (int i = 0; i < num; i++) begin
req = bus_item::type_id::create($sformatf("req%0d", i));
start_item(req); // handshake (Modules 8.2–8.3)
assert(req.randomize() with { is_read == 1; }); // randomize between start & finish
finish_item(req);
end
endtask
endclass
// ── RUNNING IT: a test creates the sequence and starts it on a sequencer ──
task my_test::run_phase(uvm_phase phase);
read_burst_seq seq = read_burst_seq::type_id::create("seq");
phase.raise_objection(this); // keep run_phase alive while the sequence runs
seq.start(env.agent.sequencer); // run the program on the agent's sequencer (blocks)
phase.drop_objection(this); // done — let the phase end
endtaskThe class is small but expresses the whole concept. read_burst_seq extends uvm_sequence #(bus_item) — parameterized by the item type it generates — and is registered as a uvm_object (it's transient, not a component). Its body() task is the scenario: here, a loop that creates, randomizes, and sends num read items through the handshake (start_item/finish_item from Modules 8.2–8.3). Because body() is a task (it consumes time, blocking at each finish_item), it can express any procedural scenario — loops, conditionals, randomization, even calling sub-sequences. Running it is two steps: a test creates the sequence (type_id::create) and starts it on a sequencer (seq.start(env.agent.sequencer)), which binds the sequence to that sequencer and runs its body(), routing each item to the driver. Note the objection: start() runs in run_phase, and the test raises an objection before start and drops it after — because without an objection, run_phase would end at time 0 and the sequence would be killed before producing stimulus (the DebugLab). The shape to internalize: a sequence is a class with a body(), and running it is create-then-start-on-a-sequencer-with-an-objection.
Verification Perspective — one structure, many sequences
The payoff of separating stimulus from structure is swappability: the same agent runs any number of different sequences, so a whole regression of scenarios comes from one testbench.
The figure is the reuse argument made concrete. The agent — sequencer and driver — is built once and never changes. The sequences — smoke_seq, stress_seq, error_seq — are different stimulus programs, and each test runs the one it needs on that same agent's sequencer via start(). A smoke test runs a few transactions; a stress test floods the design; an error test injects faults — three completely different behaviors from one structure, achieved purely by which sequence is started. Nothing in the agent, driver, or sequencer changes between them; only the program loaded on top differs. This is why the sequence abstraction is the heart of UVM stimulus: it converts "writing a new test" from "modifying the testbench" into "writing a new sequence and starting it" — a small, local, structure-free act. It also composes with everything earlier: a sequence can build its items with constraints (randomized scenarios), use config knobs (Module 7.5) to parameterize, and substitute item types via the factory (Module 6). The sequence is the variable layer of the testbench, and the structure is the fixed layer — and almost all of test-writing happens in the variable layer, which is exactly where you want the effort to go.
Runtime / Execution Flow — start(), then body() runs
At run time, running a sequence is a launch: start() binds the sequence to a sequencer and invokes its body(), which then produces items into the structure until it returns.
The launch sequence is straightforward but has a few load-bearing details. Step 1, the test creates the sequence with type_id::create — a transient object, parameterized by the item type it produces. Step 2, start(sequencer) does two things: it binds the sequence to that sequencer (so its items know where to go) and invokes body(); importantly, start() blocks until body() returns — so the calling code (the test's run_phase) waits for the whole scenario to finish. Step 3, body() runs as a task, generating the item stream: each item goes through the handshake (Modules 8.2–8.3), is routed by the sequencer to the driver, driven on the pins, and completed — one item at a time, paced by the driver (Module 8.3's back-pressure). Step 4, when body()'s procedure finishes, it returns, start() unblocks, and the scenario is done. Wrapping all of this is the objection: because this runs in run_phase, an objection must be raised before start() and dropped after — the objection is what tells the phase "stimulus is in progress, don't end yet," and dropping it signals "done, the phase may end" (Module 5). Without it, the phase manager sees no objections and ends run_phase at time 0, killing body() before it produces anything — which is the single most common sequence mistake.
Waveform Perspective — body() producing a stream over time
A sequence's body() produces a stream of items over simulation time. The waveform shows the items emerging one after another from a single running sequence, each driven in turn.
One sequence's body() generates a stream of items over time
12 cyclesThe waveform shows a sequence as a sustained producer. seq_active is high across the entire span — from start() invoking body() to body() returning — representing the scenario running. During that span, the item trace shows the stream the body() loop generates: req0, then req1, then req2, each emerging in turn and each driven (drv_active) before the next, paced by the driver's back-pressure (Module 8.3). This is the essence of a sequence: one running body() yields many transactions over time, in the order the procedure dictates — a loop produces a regular stream, a branch produces a conditional one, randomization varies each. The objection trace stays high alongside seq_active: the test raised it at start() and holds it for the whole scenario, which is what keeps run_phase alive while items are being produced; when body() returns, seq_active falls, the objection is dropped, and run_phase is free to end. The picture to carry is the duration: a sequence isn't a single event but a span of production — its body() runs for the length of the scenario, emitting transactions throughout — and the objection must cover that whole span, or the production is cut short.
DebugLab — the sequence that produced almost nothing
A sequence that was killed at time 0 because no objection kept the phase alive
A test created a sequence meant to drive 100 transactions and started it in run_phase. But the simulation ended almost immediately — at time 0 or after just one item — with little or no stimulus reaching the DUT, and the test "passed" only because nothing happened. The sequence's body() was correct (the loop, the randomization, the handshake all fine), and start() was called on the right sequencer. Yet the scenario never ran to completion; it was cut off before it began.
The sequence was started in run_phase without raising an objection, so the phase manager saw no reason to keep run_phase alive and ended it immediately — killing the still-running sequence:
my_test::run_phase:
seq = my_seq::type_id::create("seq");
seq.start(env.agent.sequencer); // ✗ no objection raised → run_phase has NOTHING holding it open
phase manager: zero objections on run_phase → end the phase NOW (time 0)
→ the run_phase task (mid seq.start) is killed → body() never completes
result: the sequence is terminated before (or just after) it starts → no/partial stimulus
fix — raise an objection around the sequence so the phase stays alive:
phase.raise_objection(this); // ✓ "stimulus in progress — don't end run_phase"
seq.start(env.agent.sequencer); // body() runs to completion
phase.drop_objection(this); // ✓ "done — run_phase may end now"This is the canonical sequence-startup bug, and it's about phasing (Module 5), not the sequence itself. run_phase is the time-consuming phase, and it ends when all objections are dropped — which means if no one raises an objection, there's nothing to keep it open, so the phase manager ends it at time 0. When the test calls seq.start() without an objection, the phase manager sees zero objections and ends run_phase immediately; ending the phase kills the still-running run_phase task (which is blocked inside seq.start()), so body() is terminated before it can produce its transactions. The sequence code is entirely correct — it simply never gets the time to run. The fix is to raise an objection before start() and drop it after: the raised objection tells the phase "stimulus is in progress, keep run_phase alive," so body() runs to completion, and dropping the objection afterward signals the phase may finally end. (UVM also supports the sequence raising the objection itself through its starting-phase handle, but the explicit raise/drop in the test is the clearest form.) The rule: a sequence that runs in run_phase must have an objection held for its entire duration, or the phase ends before it does.
The tell is a run_phase that ends at (or near) time 0 with little stimulus. Diagnose sequence-startup bugs:
- Check for a raised objection around
start(). Confirm the test (or sequence) raises an objection before the sequence runs and drops it after. No objection is the signature of an early-ending phase. - Confirm the phase ended prematurely. A simulation that finishes at time 0 (or far too early) with a correct-looking sequence points at objections, not the sequence code.
- Verify the sequence itself runs at all. Add a message at the top of
body(); if it prints once (or not at all) and then the sim ends, the phase is being torn down under it. - Rule out an unconnected/empty path. Distinct from objections: if
start()is on a null or wrong sequencer, or the driver isn't connected, the sequence hangs rather than ending early — different symptom.
Keep the phase alive for the sequence's whole duration:
- Raise an objection before
start(), drop it after. Makeraise_objection→start()→drop_objectionthe standard shape for running a sequence inrun_phase. - Cover the entire scenario. The objection must be held for the whole sequence, not part of it — raise before it begins, drop only after
start()returns (the scenario is complete). - Consider the starting-phase mechanism for reuse. A sequence can raise/drop its starting phase's objection itself, so it manages its own lifetime — useful when the same sequence is the top sequence of many tests.
- Don't drop early. Dropping the objection before the sequence finishes ends the phase mid-scenario — drop only when the stimulus is truly done.
The one-sentence lesson: a sequence started in run_phase produces stimulus only while the phase is alive, and run_phase ends the instant all objections are dropped — so raise an objection before start() and drop it after, or the phase ends at time 0 and the sequence is killed before it generates anything.
Common Mistakes
- Starting a sequence without an objection.
run_phaseends at time 0 with no objection, killing the sequence; raise an objection beforestart()and drop it after. - Dropping the objection too early. Ending the objection before the sequence finishes cuts the scenario short; drop only after
start()returns. - Treating a sequence as a component. A sequence is a transient
uvm_objectrun viastart(), not auvm_componentin the hierarchy — don't build it into the tree. - Putting pin-level driving in the sequence. The sequence generates transactions (the "what"); driving them is the driver's job (the "how"). Keep the sequence structure-free so it stays reusable.
- Starting on the wrong (or null) sequencer.
start()binds the sequence to a sequencer; a null or wrong handle sends items nowhere or hangs. Start on the intended agent's sequencer. - Randomizing items in the wrong place. Inside
body(), randomize betweenstart_itemandfinish_item(Module 8.3) — not before the handshake.
Senior Design Review Notes
Interview Insights
A sequence is a transient stimulus program: a uvm_sequence object whose body() task generates a stream of sequence items representing a scenario. It relates to the driver and sequencer through a clean separation of concerns. The sequence is the "what" — it decides which transactions to generate and in what order, expressed procedurally in body(). The driver is the "how" — it translates each transaction into pin-level activity. The sequencer sits between them as the broker: you run a sequence on a sequencer with start(), and the sequencer routes the sequence's items to the driver, arbitrating if multiple sequences run. The key property is that the sequence is decoupled from both: it never touches pins (that's the driver) and it isn't part of the component hierarchy (that's the agent holding the sequencer and driver). It's a transient object — created, run via start(), and discarded — not a permanent component. This separation is what enables reuse: because the sequence generates only transactions, the same sequence runs against any compatible driver or timing, and because it's bound to a sequencer only at start() time, any compatible sequence runs on a given agent. So one fixed testbench — agent, sequencer, driver — runs an open-ended set of scenarios just by starting different sequences. The mental model is that the testbench is a fixed machine and a sequence is a program you load and run on it: swap the program, change the behavior, leave the machine unchanged.
Exercises
- Write a body(). Sketch a sequence that sends five write transactions with incrementing addresses, showing the per-item create/randomize/handshake inside
body(). - Run it correctly. Write the test
run_phasethat creates and starts that sequence onenv.agent.sequencer, with the objection placed correctly, and explain what each objection call does. - Diagnose the early end. A simulation ends at time 0 with a correct sequence that produced nothing. Name the cause, where it is, and the fix.
- Argue the separation. Explain why the same agent can run a smoke, a stress, and an error scenario without structural change, and what makes that possible.
Summary
- A sequence is a transient stimulus program: a
uvm_sequenceobject whosebody()task generates a stream of sequence items — the scenario — run on a sequencer withstart(sequencer). - It separates the what (the sequence) from the how (the driver) and the where (the agent): the sequence generates transactions only, the driver drives pins, the agent is the permanent structure — so the sequence is a transient object, not a component.
- That separation makes stimulus swappable: one fixed agent runs many sequences (smoke, stress, error), so a whole regression of scenarios comes from one testbench — new behavior means a new sequence, not a new testbench.
start()launchesbody()(binding the sequence to a sequencer and blocking untilbody()returns), andbody()produces a stream of items over time, paced by the driver — a sequence is a span of production, not a single event.- The durable rule of thumb: write the scenario as a sequence's
body()(transactions only, no pin detail), run it withcreate→raise_objection→start(sequencer)→drop_objection, and hold the objection for the sequence's whole duration — because a sequence started without an objection is killed at time 0 before it produces any stimulus.
Next — Sequencer Interaction: a sequence runs on a sequencer, and the relationship between them — how the sequencer arbitrates, grants, and routes — is the next layer down. The next chapter examines the sequence↔sequencer interaction: how the sequencer brokers items, arbitrates among sequences, and connects to the driver.