Every stimulus pattern in a UVM testbench, from a single register write to a full traffic scenario, runs because something called start() on a sequence. Knowing exactly what start() does, and which hooks it calls, is what lets you build layered sequences, add setup and cleanup code, and control priority when several sequences compete for one driver.
A UVM sequence is executed by calling its start() task with the sequencer it should run on. start() then calls a fixed series of hooks: pre_start(), optionally pre_body(), the sequence's own body(), optionally post_body(), and finally post_start(). If a parent sequence is named, the parent's pre_do(), mid_do() and post_do() are called around the child's body() as well.
8 min read | Intermediate Level
What You'll Learn
- Call
start()with the right sequencer, parent, priority andcall_pre_postarguments - Predict the order in which
start()calls each hook - Explain what
call_pre_postcontrols - Use the parent argument when one sequence starts another
Syntax
virtual task start (uvm_sequencer_base sequencer,
uvm_sequence_base parent_sequence = null,
int this_priority = -1,
bit call_pre_post = 1);
sequencer- the sequencer the sequence runs on. A sequence that sends items must be given a sequencer. A virtual sequence, which only starts other sequences, may be started withnull.parent_sequence- the sequence that is starting this one, if any. When given, the parent'spre_do(),mid_do()andpost_do()hooks are called during this sequence's execution.this_priority- the arbitration priority. The default of -1 means "use the parent's priority", or 100 for a sequence with no parent. Higher numbers mean higher priority.call_pre_post- when 1 (the default),pre_body()andpost_body()are called aroundbody().
start() is a task and returns only when the sequence has finished. A typical call from a test looks like this:
seq = my_seq::type_id::create("seq");
if (!seq.randomize()) `uvm_error("TEST", "Randomization failed") // optional
seq.start(m_env.m_agent.m_sequencer);
The Order of Hooks
pre_start() and post_start() always run. pre_body() and post_body() run only when call_pre_post is 1. The parent's pre_do(), mid_do() and post_do() run only when a parent sequence was passed in, and they are called on the parent, not on the sequence being started.
Example: call_pre_post
This base sequence prints a message from each hook so the execution order shows up in the log.
class base_seq extends uvm_sequence;
// Rest of the code (factory registration and constructor)
virtual task pre_body();
`uvm_info(get_type_name(), "pre_body()", UVM_LOW)
endtask
virtual task pre_do(bit is_item);
`uvm_info(get_type_name(), "pre_do()", UVM_LOW)
endtask
virtual function void mid_do(uvm_sequence_item this_item);
`uvm_info(get_type_name(), "mid_do()", UVM_LOW)
endfunction
virtual task body();
`uvm_info(get_type_name(), "body()", UVM_LOW)
endtask
virtual function void post_do(uvm_sequence_item this_item);
`uvm_info(get_type_name(), "post_do()", UVM_LOW)
endfunction
virtual task post_body();
`uvm_info(get_type_name(), "post_body()", UVM_LOW)
endtask
endclass
The test starts it with the default arguments. No sequencer is needed because this sequence sends no items.
class base_test extends uvm_test;
// Rest of the code
virtual task run_phase(uvm_phase phase);
base_seq bs = base_seq::type_id::create("bs", this);
bs.start(null); // call_pre_post defaults to 1
endtask
endclass
UVM_INFO @ 0: reporter [RNTST] Running test base_test... UVM_INFO ./tb/tb_top.sv(19) @ 0: reporter@@bs [base_seq] pre_body() UVM_INFO ./tb/tb_top.sv(31) @ 0: reporter@@bs [base_seq] body() UVM_INFO ./tb/tb_top.sv(39) @ 0: reporter@@bs [base_seq] post_body() --- UVM Report catcher Summary ---
The prefix reporter@@bs shows the sequence ran with no sequencer. pre_do(), mid_do() and post_do() do not appear, because there is no parent. Starting it with call_pre_post set to 0 removes pre_body() and post_body() as well:
bs.start(null, null, 0, 0); // no parent, priority 0, call_pre_post = 0
UVM_INFO @ 0: reporter [RNTST] Running test base_test... UVM_INFO ./tb/tb_top.sv(31) @ 0: reporter@@bs [base_seq] body() --- UVM Report catcher Summary ---
Inheritance Is Not the Parent Argument
The word "parent" is easy to confuse with class inheritance. If child1_seq extends base_seq and the test starts it with cs.start(null), there is still no parent sequence, so its pre_do(), mid_do() and post_do() are never called. The parent argument refers only to the sequence that starts another one at run time.
Example: One Sequence Starting Another
Now child1_seq starts child2_seq from its body(). Both extend base_seq and print from every hook. First, the child is started without a parent:
class child1_seq extends base_seq;
// Rest of the code (registration, constructor, other hooks as in base_seq)
virtual task body();
child2_seq cs2 = child2_seq::type_id::create("cs2");
`uvm_info(get_type_name(), "body()", UVM_LOW)
cs2.start(null); // parent argument left at its default, null
endtask
endclass
UVM_INFO @ 0: reporter [RNTST] Running test child1_test... UVM_INFO ./tb/tb_top.sv(52) @ 0: reporter@@cs [child1_seq] pre_body() UVM_INFO ./tb/tb_top.sv(65) @ 0: reporter@@cs [child1_seq] body() UVM_INFO ./tb/tb_top.sv(87) @ 0: reporter@@cs2 [child2_seq] pre_body() UVM_INFO ./tb/tb_top.sv(99) @ 0: reporter@@cs2 [child2_seq] body() UVM_INFO ./tb/tb_top.sv(107) @ 0: reporter@@cs2 [child2_seq] post_body() UVM_INFO ./tb/tb_top.sv(76) @ 0: reporter@@cs [child1_seq] post_body() --- UVM Report catcher Summary ---
cs2 runs completely inside cs's body(), and cs resumes afterwards. Its name in the log is just cs2, because without a parent it is a root sequence. Passing this as the parent changes both of those things:
cs2.start(null, this); // child1_seq is now cs2's parent sequence
UVM_INFO @ 0: reporter [RNTST] Running test child1_test... UVM_INFO ./tb/tb_top.sv(52) @ 0: reporter@@cs [child1_seq] pre_body() UVM_INFO ./tb/tb_top.sv(65) @ 0: reporter@@cs [child1_seq] body() UVM_INFO ./tb/tb_top.sv(87) @ 0: reporter@@cs.cs2 [child2_seq] pre_body() UVM_INFO ./tb/tb_top.sv(56) @ 0: reporter@@cs [child1_seq] pre_do() UVM_INFO ./tb/tb_top.sv(60) @ 0: reporter@@cs [child1_seq] mid_do() UVM_INFO ./tb/tb_top.sv(99) @ 0: reporter@@cs.cs2 [child2_seq] body() UVM_INFO ./tb/tb_top.sv(72) @ 0: reporter@@cs [child1_seq] post_do() UVM_INFO ./tb/tb_top.sv(107) @ 0: reporter@@cs.cs2 [child2_seq] post_body() UVM_INFO ./tb/tb_top.sv(76) @ 0: reporter@@cs [child1_seq] post_body() --- UVM Report catcher Summary ---
Three things changed. The child's name is now cs.cs2, showing its place under the parent. The parent's pre_do() and mid_do() run after the child's pre_body() and before its body(), and the parent's post_do() runs right after the child's body(). That matches the order in the diagram exactly.
Quick Check
Q: A parent sequence wants to count how many child sequences it has started. Which of its own hooks is a good place for the counter?
Show Answer
mid_do() or post_do(). Both run on the parent once per child started with the parent passed in, and they receive the child as an argument. They do not run for children started without a parent, so the parent argument must be given.
Priority and Arbitration
The this_priority argument matters only when two or more sequences run on the same sequencer at the same time and both want to send an item. The sequencer then has to pick one, and how it picks is set by its arbitration mode, chosen with set_arbitration().
The default mode, UVM_SEQ_ARB_FIFO, grants requests in the order they arrived and ignores priority completely. To make priority count, switch to a mode such as UVM_SEQ_ARB_STRICT_FIFO, which always serves the highest-priority request first, or UVM_SEQ_ARB_WEIGHTED, which picks randomly with priority as the weight.
m_env.m_agent.m_sequencer.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
fork
bg_seq.start(m_env.m_agent.m_sequencer, null, 100); // background traffic
irq_seq.start(m_env.m_agent.m_sequencer, null, 500); // wins whenever both wait
join
Here irq_seq gets the driver whenever both sequences are waiting, while bg_seq fills the gaps. Without the set_arbitration() call, the two priorities would have no effect.
Where to Use Each Hook
pre_start()andpost_start()always run, which makes them the place for code that must happen whichever way the sequence is started.pre_body()andpost_body()suit setup and cleanup that should run only for top-level starts, since the`uvm_domacros start child sequences withcall_pre_postset to 0.pre_do(),mid_do()andpost_do()let a parent observe or adjust each item or child sequence it runs, for example to record statistics.
Common Mistakes
Mistake #1: Starting an item-sending sequence with a null sequencer
A sequence whose body() calls start_item() needs a sequencer to send items through. Starting it with null works only for virtual sequences that start other sequences on real sequencers.
Mistake #2: Expecting pre_body() to run under `uvm_do
Sequences started with the `uvm_do family run with call_pre_post set to 0, so their pre_body() and post_body() are silently skipped. Put code that must always run in pre_start() and post_start().
Mistake #3: Calling start() without an objection
Starting a sequence from run_phase does not keep the phase alive by itself. Raise an objection before start() and drop it after, as described in UVM Objection.
Summary
In this article, you learned:
start()runs a sequence on a sequencer and returns whenbody()and all hooks have finishedpre_start()andpost_start()always run, whilepre_body()andpost_body()depend oncall_pre_post- Passing a parent sequence makes the parent's
pre_do(),mid_do()andpost_do()run around the child'sbody() - The default priority is the parent's, or 100 for a root sequence
- The parent argument is about run-time nesting, not class inheritance