UVM Sequence got reg_sequence running end to end against reg_sequencer and reg_driver - but two things were deliberately left for later: how a sequence keeps the simulation alive while it runs, and how to start a sequence without writing a call to start() by hand every time. This article closes both gaps.
9 min read | Intermediate Level
What You'll Learn
- Why a sequence is a container, and the two ways to start one
- The two places an objection usually gets raised around a sequence's run
- How to set reg_sequence as a sequencer's default sequence
- What actually happens, end to end, when a sequence runs
A Sequence Is a Container
A single reg_transaction is one packet of data - one operation, either a write or a read. A sequence is a container for an organized collection of those, or even other sequences. The same way reg_sequence generates five transactions in a loop, a real verification plan typically ends up with several different sequences, each built for a different scenario: one for resets, one for directed register reads and writes, one for a stress test that hammers random addresses.
Those sequences don't have to be used in isolation. They can be run individually, composed together into a larger sequence - a reset sequence followed by a block of reads and writes, for instance - or selected at random from a library to widen test coverage without writing a new sequence for every combination by hand.
Composing sequences out of other sequences, and randomly selecting from a sequence library, both depend on machinery - virtual sequences and sequence libraries - covered in UVM Virtual Sequence and UVM Sequence Library. The rest of this article stays focused on a single sequence's own lifecycle.
Two Ways to Start a Sequence
Every example so far has used reg_sequence's body() task, but never actually started reg_sequence itself in more than one way. There are two: calling start() directly, or using the `uvm_do macro. Both end up running body() to completion, but they fit different situations.
Calling start() directly is what a test typically does to kick off a top-level sequence against a specific sequencer, naming that sequencer explicitly:
reg_sequence seq;
seq = reg_sequence::type_id::create("seq");
seq.start(m_sequencer);
The `uvm_do macro is most often reached for inside another sequence's own body(), to kick off reg_sequence as a sub-sequence on whatever sequencer that parent sequence is already running on:
virtual task body();
`uvm_do(reg_seq)
endtask
Where start() takes the target sequencer as an explicit argument, `uvm_do uses the parent sequence's own sequencer implicitly - there's no second argument naming one. That's the real difference, not just brevity: `uvm_do only makes sense when there's already a parent sequence to inherit a sequencer from, which is exactly the composition scenario - sequences running other sequences - that this article keeps deferring to its own dedicated chapter. Calling reg_sequence directly from a test, as shown above, always uses start(), since there's no parent sequence to borrow a sequencer from.
Both start() and `uvm_do have their own articles, How to Execute Sequences via start() and How to Use uvm_do Sequence Macros, covering the full mechanics - priorities, locking, and what exactly the macro expands to. This section only covers enough to tell the two apart.
Quick Check
Q: Could a test's run_phase use `uvm_do(seq) instead of seq.start(m_sequencer) to start reg_sequence?
Show Answer
Not directly. `uvm_do relies on a parent sequence's own sequencer to run on, and a test's run_phase isn't a sequence - it has no sequencer of its own to borrow. start(), which takes the sequencer as an explicit argument, is the only option when starting a sequence from outside another sequence.
Keeping the Simulation Alive While reg_sequence Runs
The body() task in UVM Sequence generated transactions but never addressed one real problem: nothing was keeping the simulation alive while reg_sequence ran. UVM's run phase ends as soon as nothing is objecting to it ending, and a sequence running on its own doesn't object to anything by default. There are two places this objection usually gets raised, depending on how the sequence is started.
The more common case is a component - usually a test - raising the objection itself, wrapped around the same start() call just shown:
task run_phase(uvm_phase phase);
reg_sequence seq;
seq = reg_sequence::type_id::create("seq");
phase.raise_objection(this);
seq.start(m_sequencer);
phase.drop_objection(this);
endtask
Here, the objection belongs to the component calling start(), not to reg_sequence itself. start() doesn't return until body() has finished, so the objection is held for exactly as long as reg_sequence is running, then dropped right after. reg_sequence doesn't need to know anything about objections at all in this case - the responsibility sits entirely with whatever called start().
The second case is narrower: a sequence started as a default sequence, covered next, has no run_phase code wrapping it - nothing calls start() by hand, so there's no surrounding raise_objection/drop_objection pair either. For that specific case, pre_body() and post_body() give the sequence a way to raise and drop its own objection instead:
class reg_sequence extends uvm_sequence #(reg_transaction);
`uvm_object_utils(reg_sequence)
function new(string name = "reg_sequence");
super.new(name);
endfunction
virtual task pre_body();
uvm_phase starting_phase = get_starting_phase();
if (starting_phase != null)
starting_phase.raise_objection(this);
endtask
virtual task body();
repeat (5) begin
req = reg_transaction::type_id::create("req");
start_item(req);
assert(req.randomize());
finish_item(req);
end
endtask
virtual task post_body();
uvm_phase starting_phase = get_starting_phase();
if (starting_phase != null)
starting_phase.drop_objection(this);
endtask
endclass
get_starting_phase() only returns a non-null phase when reg_sequence is started as a default sequence. That's exactly why both callbacks check for null before doing anything: if reg_sequence is started directly with start() instead, as in the run_phase example above, there's no starting phase to raise an objection against, and the check quietly does nothing rather than failing.
Quick Check
Q: If reg_sequence is started with the run_phase pattern shown above, does its pre_body() still do anything?
Show Answer
It runs, but get_starting_phase() returns null in that case, so the if check skips the raise_objection call entirely. The objection is already being held by whatever called start(), so there's nothing for pre_body() to do - which is exactly the behavior the null check is there to produce.
Setting a Default Sequence
Every example so far has started reg_sequence explicitly, with a direct call to start(). UVM also supports starting a sequence implicitly - as a sequencer's default sequence - using uvm_config_db, the same mechanism reg_driver used to retrieve its virtual interface:
uvm_config_db #(uvm_object_wrapper)::set(this, ".run_phase",
"default_sequence", reg_sequence::type_id::get());
The path string here is left as a placeholder because reg_sequencer doesn't have a fixed hierarchical position yet - that only happens once it's built into an agent and environment, covered in UVM Agent and UVM Environment. Once it does, the placeholder becomes a real path, the same way "vif" in reg_driver's uvm_config_db::get() call became a real interface handle once it was actually set somewhere in the hierarchy.
Setting a default sequence this way means reg_sequencer starts it automatically when its run phase begins, with no run_phase code anywhere calling start() by hand. This is the one case where reg_sequence is on its own for objections - which is exactly why pre_body()/post_body() exist: get_starting_phase() returns a valid phase precisely when a sequence is started this way, giving it something to raise an objection against that no other component is already holding.
What Actually Happens When reg_sequence Runs
Putting everything from this chapter together, one run of reg_sequence works like this:
Five transactions run through in total, each one created and randomized inside body(), routed through reg_sequencer, driven onto reg_if by reg_driver, and acted on by reg_block. The objection raised in pre_body() is what keeps the simulation alive long enough for all five to complete, and dropping it in post_body() is what finally lets the test end.
This tutorial deliberately skips the older uvm_sequence_utils/uvm_sequencer_utils macro pair some existing UVM code still uses for registering sequences and sequencers. That alternative is being phased out of the standard and offers no benefit over uvm_object_utils/uvm_component_utils, which this tutorial uses throughout - there's no reason to learn the older form unless you encounter it in legacy code.
Common Mistakes
Mistake #1: Raising the objection inside body() instead of pre_body()
This applies specifically to a sequence relying on its own pre_body()/post_body() - that is, one started as a default sequence. Raising the objection inside body() itself, after some setup work has already run, leaves a brief window where the simulation could end before the objection is even in place. pre_body() runs before body() starts, closing that gap entirely.
Mistake #2: Forgetting the null check on starting_phase
Wrong:
virtual task pre_body();
get_starting_phase().raise_objection(this);
endtask
Right:
virtual task pre_body();
uvm_phase starting_phase = get_starting_phase();
if (starting_phase != null)
starting_phase.raise_objection(this);
endtask
Why? get_starting_phase() returns null whenever a sequence is started directly with start() rather than as a default sequence. Calling raise_objection() on a null handle crashes the simulation instead of just skipping the objection.
Practice Exercise
Challenge: Log the Transaction Count
Add a line to reg_sequence's post_body() that logs how many transactions were sent, using uvm_info.
Hint
The number of transactions is already known - it's the same value used in body()'s repeat loop. You don't need to count anything at runtime if you store that number in a class property instead of a literal.
Solution
int unsigned n_times = 5;
virtual task body();
repeat (n_times) begin
req = reg_transaction::type_id::create("req");
start_item(req);
assert(req.randomize());
finish_item(req);
end
endtask
virtual task post_body();
uvm_phase starting_phase = get_starting_phase();
`uvm_info(get_type_name(), $sformatf("Sent %0d transactions", n_times), UVM_LOW)
if (starting_phase != null)
starting_phase.drop_objection(this);
endtask
Moving the transaction count into n_times instead of a hardcoded repeat (5) also makes the count something a test could configure later, rather than something fixed inside the sequence itself.
Summary
In this article, you learned:
- A sequence is a container that can hold multiple transactions, and sequences themselves can be composed or selected from a library
- Most commonly, the component calling
start()raises and drops the objection itself, around that call pre_body()andpost_body()raise and drop an objection from inside the sequence, but only do anything when the sequence is started as a default sequence- Setting a default sequence with
uvm_config_dbstarts a sequence automatically, without an explicitstart()call
Going Deeper
- Objections in full: UVM Objection
- Coordinating several sequences: UVM Virtual Sequence
- Picking sequences from a library: UVM Sequence Library