Skip to content

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 does start() 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 a uvm_component that 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 its main(), a procedure that says "generate this read, then that write, then ten random transfers." You load and run the program on the machine with start(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.

A transient sequence generates items that flow through the permanent sequencer and driver to the DUTstart() — generates itemsstart() —generates…routes itemsdrives pins (how)Sequence (transient program)body() generates the stream of items —the 'what'Sequencer (permanent)routes/arbitrates items to thedriverDriver (permanent)translates items to pins — the'how'DUTthe design under test12
Figure 1 — the sequence is a transient stimulus program above the permanent structure. The sequence's body() generates sequence items (the 'what'). It runs on the sequencer, which routes items to the driver. The driver translates them to pin activity (the 'how'). The agent is the permanent structure holding the sequencer and driver. The sequence is created and run, then discarded — it is not part of the hierarchy. Swapping the sequence changes the scenario without touching the structure below.

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().

a sequence, its body(), and how a test runs it
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endtask

The 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.

Multiple sequences (smoke, stress, error) each run on the same fixed agent's sequencerstart() (smoke test)start() (stresstest)start() (error test)smoke_seqa few transactionsstress_seqa flood of trafficerror_seqfault injectionAgent (fixed): sequencer +driverbuilt once — runs any compatiblesequence12
Figure 2 — one agent runs many sequences. The agent (with its sequencer and driver) is built once and fixed. Different tests run different sequences on its sequencer: a smoke sequence (a few transactions), a stress sequence (a flood), an error sequence (fault injection). Each is a different stimulus program on the same machine. Swapping the sequence changes the scenario with no structural change — which is how one testbench yields a whole regression of tests.

The figure is the reuse argument made concrete. The agent — sequencer and driver — is built once and never changes. The sequencessmoke_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.

Running a sequence: create it, start it on a sequencer which invokes body, body generates items until it returns and start unblockscreate → start(sequencer) → body() generates items → returnscreate → start(sequencer) → body() generates items → returns1Test creates the sequencetype_id::create — a transient object, parameterized by the itemtype.2start(sequencer) binds + invokes body()binds the sequence to the sequencer and calls body(); start()blocks until body returns.3body() generates the item streameach item: create → randomize → start_item/finish_item → driven(Modules 8.2–8.3).4body() returns → start() unblocksthe scenario is complete; drop the objection so run_phase can end.
Figure 3 — running a sequence: start() launches body(). A test creates the sequence and calls start(sequencer), which binds the sequence to that sequencer and invokes body(). body() runs as a task, generating items through the handshake — each item routed to the driver, driven, and completed — until the loop finishes. start() blocks until body() returns. An objection raised around start() keeps run_phase alive for the duration; dropping it lets the phase end once the sequence is done.

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 cycles
One sequence's body() generates a stream of items over timestart(): body() begins running, objection raisedstart(): body() begins…item req0 generated and drivenitem req0 generated an…body() loops → req1; then req2 — a streambody() loops → req1; t…body() returns → seq_active falls, objection dropped, run_phase can endbody() returns → seq_a…clkseq_activeobjectionitemreq0req0--req1req1--req2req2--------drv_activet0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — a running sequence produces a stream. seq_active marks the body() running across the whole span (start() to return). Each item it generates is driven in turn — item shows req0, req1, req2 emerging one after another, paced by the driver (Module 8.3). The sequence is a sustained producer: its body() loops, and each iteration yields one driven transaction. When the loop ends, seq_active falls and start() unblocks. The objection is held high across seq_active so run_phase stays alive.

The 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

Symptom

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.

Root cause

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:

why a correct sequence produced no stimulus
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Diagnosis

The tell is a run_phase that ends at (or near) time 0 with little stimulus. Diagnose sequence-startup bugs:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
Prevention

Keep the phase alive for the sequence's whole duration:

  1. Raise an objection before start(), drop it after. Make raise_objectionstart()drop_objection the standard shape for running a sequence in run_phase.
  2. 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).
  3. 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.
  4. 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_phase ends at time 0 with no objection, killing the sequence; raise an objection before start() 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_object run via start(), not a uvm_component in 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 between start_item and finish_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

  1. Write a body(). Sketch a sequence that sends five write transactions with incrementing addresses, showing the per-item create/randomize/handshake inside body().
  2. Run it correctly. Write the test run_phase that creates and starts that sequence on env.agent.sequencer, with the objection placed correctly, and explain what each objection call does.
  3. 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.
  4. 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_sequence object whose body() task generates a stream of sequence items — the scenario — run on a sequencer with start(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() launches body() (binding the sequence to a sequencer and blocking until body() returns), and body() 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 with createraise_objectionstart(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.