Executing a sequence with the `uvm_do sequence macros takes one line instead of several: the macro creates the sequence or item, randomizes it, and starts it. The price is control. A child sequence started this way always skips its pre_body() and post_body(), and the macro decides when randomization happens.
This article opens up the macro to show exactly what it calls and in what order, and then follows a three-level example through the log.
7 min read | Intermediate Level
What You'll Learn
- Describe what
`uvm_doexpands into for an item and for a sequence - Predict the order of
pre_do(),mid_do()andpost_do()in nested sequences - Explain why
pre_body()andpost_body()never run in a macro-started sequence - Recognise late randomization and why it matters
What the Macro Expands Into
Every member of the family, from `uvm_do to `uvm_do_pri_with, is a thin wrapper around `uvm_do_on_pri_with, as described in How to use `uvm_do sequence macros. Its definition in the UVM library is short enough to read in full:
`define uvm_do_on_pri_with(SEQ_OR_ITEM, SEQR, PRIORITY, CONSTRAINTS)
begin
uvm_sequence_base __seq;
`uvm_create_on(SEQ_OR_ITEM, SEQR) // always builds a new object
if (!$cast(__seq,SEQ_OR_ITEM)) start_item(SEQ_OR_ITEM, PRIORITY); // item: wait for grant
if ((__seq == null || !__seq.do_not_randomize) && !SEQ_OR_ITEM.randomize() with CONSTRAINTS ) begin
`uvm_warning("RNDFLD", "Randomization failed in uvm_do_with action")
end
if (!$cast(__seq,SEQ_OR_ITEM)) finish_item(SEQ_OR_ITEM, PRIORITY); // item: send to driver
else __seq.start(SEQR, this, PRIORITY, 0); // sequence: call_pre_post = 0
end
The $cast to uvm_sequence_base is how the macro tells the two cases apart. If the cast fails, the argument is a sequence item and goes through start_item() and finish_item(). If it succeeds, the argument is a sequence and its start() task is called with this as the parent.
Three details in this code explain most of the macro's behaviour:
`uvm_create_onruns unconditionally, so a new object is built on every call, whatever the handle held before.- For an item, randomization happens after
start_item()returns, which is after the sequencer has granted access. This is called late randomization. - For a sequence, the last argument of
start()is 0. That iscall_pre_post, so the child'spre_body()andpost_body()are skipped.
Call Order for Items and Sequences
The orange boxes are hooks of the sequence that contains the macro call, not of the item or child. That is why a parent can use mid_do() to inspect or tweak each item just before it is sent, and post_do() to see it after the driver has finished with it.
Late randomization is useful when the item's values depend on the state of the testbench at the moment the driver is ready, such as a free buffer slot or the current register value. Values chosen at grant time are more up to date than values chosen when the sequence first asked for the sequencer.
Quick Check
Q: A parent sequence's mid_do() sets item.addr = 0. Will the driver see address 0, or a random address?
Show Answer
Address 0. For an item, mid_do() runs after randomize() and before the item is handed to the driver, so the assignment overwrites the random value.
Example: Three Nested Sequences
The example uses three sequences that each start the next one. They share a base class whose pre_do(), mid_do() and post_do() print a message, so the parent hooks show up in the log.
class base_sequence extends uvm_sequence #(my_data);
`uvm_object_utils (base_sequence)
`uvm_declare_p_sequencer (my_sequencer)
function new (string name = "base_sequence");
super.new (name);
endfunction
virtual function void mid_do (uvm_sequence_item this_item);
`uvm_info (get_type_name (), "Executing mid_do", UVM_MEDIUM)
endfunction
virtual task pre_do (bit is_item);
`uvm_info (get_type_name (), "Executing pre_do", UVM_MEDIUM)
endtask
virtual function void post_do (uvm_sequence_item this_item);
`uvm_info (get_type_name (), "Executing post_do", UVM_MEDIUM)
endfunction
endclass
The test starts seq1 with start() and handles the objection. seq1 defines pre_start() and uses a macro to run seq2. The macro creates m_seq2 itself and names it after the handle, so there is no need to call type_id::create() first.
class seq1 extends base_sequence;
`uvm_object_utils (seq1)
seq2 m_seq2;
function new (string name = "seq1");
super.new (name);
endfunction
virtual task pre_start ();
`uvm_info (get_type_name (), "Executing pre_start()", UVM_MEDIUM)
endtask
virtual task body ();
`uvm_info ("SEQ1", "Starting seq1", UVM_MEDIUM)
#10;
`uvm_do (m_seq2) // create, randomize, start with seq1 as parent
`uvm_info ("SEQ1", "Ending seq1", UVM_MEDIUM)
endtask
endclass
seq2 does the same with seq3, and also defines pre_body() and post_body() to check whether they run.
class seq2 extends base_sequence;
`uvm_object_utils (seq2)
seq3 m_seq3;
function new (string name = "seq2");
super.new (name);
endfunction
virtual task pre_body ();
`uvm_info (get_type_name(), "Executing pre_body", UVM_MEDIUM)
endtask
virtual task body ();
`uvm_info ("SEQ2", "Starting seq2", UVM_MEDIUM)
#10;
`uvm_do (m_seq3)
`uvm_info ("SEQ2", "Ending seq2", UVM_MEDIUM)
endtask
virtual task post_body ();
`uvm_info (get_type_name(), "Executing post_body", UVM_MEDIUM)
endtask
endclass
seq3 is the innermost sequence. It waits a little and defines pre_body(), post_body() and post_start().
class seq3 extends base_sequence;
`uvm_object_utils (seq3)
function new (string name = "seq3");
super.new (name);
endfunction
virtual task pre_body ();
`uvm_info (get_type_name(), "Executing pre_body", UVM_MEDIUM)
endtask
virtual task body ();
`uvm_info ("SEQ3", "Starting seq3", UVM_MEDIUM)
#10;
`uvm_info ("SEQ3", "Ending seq3", UVM_MEDIUM)
endtask
virtual task post_body ();
`uvm_info (get_type_name(), "Executing post_body", UVM_MEDIUM)
endtask
virtual task post_start ();
`uvm_info (get_type_name (), "Executing post_start", UVM_MEDIUM)
endtask
endclass
CDNS-UVM-1.1d (14.10-s013)
(C) 2007-2013 Mentor Graphics Corporation
(C) 2007-2013 Cadence Design Systems, Inc.
(C) 2006-2013 Synopsys, Inc.
(C) 2011-2013 Cypress Semiconductor Corp.
----------------------------------------------------------------
UVM_INFO @ 0: reporter [RNTST] Running test base_test...
UVM_INFO @ 0: reporter [UVMTOP] UVM testbench topology:
------------------------------------------------------------
Name Type Size Value
------------------------------------------------------------
uvm_test_top base_test - @2644
m_top_env my_env - @214
m_drv0 my_driver - @208
rsp_port uvm_analysis_port - @2843
seq_item_port uvm_seq_item_pull_port - @2792
m_seqr0 my_sequencer - @2743
rsp_export uvm_analysis_export - @2933
seq_item_export uvm_seq_item_pull_imp - @3481
arbitration_queue array 0 -
lock_queue array 0 -
num_last_reqs integral 32 'd1
num_last_rsps integral 32 'd1
------------------------------------------------------------
UVM_INFO ./tb/my_pkg.sv(92) @ 0: uvm_test_top.m_top_env.m_drv0 [my_driver] Applying initial reset
UVM_INFO ./tb/my_pkg.sv(96) @ 390000: uvm_test_top.m_top_env.m_drv0 [my_driver] DUT is now out of reset
UVM_INFO ./tb/my_pkg.sv(103) @ 390000: uvm_test_top.m_top_env.m_drv0 [my_driver] Waiting for data from sequencer
UVM_INFO ./tb/my_pkg.sv(170) @ 390000: uvm_test_top.m_top_env.m_seqr0@@seq1 [seq1] Executing pre_start()
UVM_INFO ./tb/my_pkg.sv(180) @ 390000: uvm_test_top.m_top_env.m_seqr0@@seq1 [SEQ1] Starting seq1
UVM_INFO ./tb/my_pkg.sv(143) @ 400000: uvm_test_top.m_top_env.m_seqr0@@seq1 [seq1] Executing pre_do
UVM_INFO ./tb/my_pkg.sv(139) @ 400000: uvm_test_top.m_top_env.m_seqr0@@seq1 [seq1] Executing mid_do
UVM_INFO ./tb/my_pkg.sv(207) @ 400000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2 [SEQ2] Starting seq2
UVM_INFO ./tb/my_pkg.sv(143) @ 410000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2 [seq2] Executing pre_do
UVM_INFO ./tb/my_pkg.sv(139) @ 410000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2 [seq2] Executing mid_do
UVM_INFO ./tb/my_pkg.sv(234) @ 410000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2.m_seq3 [SEQ3] Starting seq3
UVM_INFO ./tb/my_pkg.sv(236) @ 420000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2.m_seq3 [SEQ3] Ending seq3
UVM_INFO ./tb/my_pkg.sv(147) @ 420000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2 [seq2] Executing post_do
UVM_INFO ./tb/my_pkg.sv(244) @ 420000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2.m_seq3 [seq3] Executing post_start
UVM_INFO ./tb/my_pkg.sv(210) @ 420000: uvm_test_top.m_top_env.m_seqr0@@seq1.m_seq2 [SEQ2] Ending seq2
UVM_INFO ./tb/my_pkg.sv(147) @ 420000: uvm_test_top.m_top_env.m_seqr0@@seq1 [seq1] Executing post_do
UVM_INFO ./tb/my_pkg.sv(183) @ 420000: uvm_test_top.m_top_env.m_seqr0@@seq1 [SEQ1] Ending seq1
UVM_INFO ./tb/my_pkg.sv(122) @ 420000: uvm_test_top.m_top_env.m_drv0 [my_driver] Finished DUT simulation
--- UVM Report catcher Summary ---
Walking through the log
- The test starts seq1, so its
pre_start()prints first, followed by "Starting seq1". - After the
#10, the macro starts m_seq2. seq1'spre_doandmid_doprint, tagged[seq1]because they belong to the parent. seq2'spre_body()is silent. - "Starting seq2" appears with the full name
seq1.m_seq2, confirming the parent link. After its own#10, the same pattern repeats one level down with[seq2]hooks around m_seq3. - When seq3's body ends, its parent's
post_doruns first, and only then seq3's ownpost_start(). Itspre_body()andpost_body()never print. - Control unwinds: "Ending seq2", then seq1's
post_do, then "Ending seq1".
Every step matches the right-hand column of the diagram. Note that the parent's pre_do() and mid_do() run before the child's body(), and the parent's post_do() runs before the child's post_start().
If a child sequence needs setup or cleanup code that must run even when it is started by a macro, put it in pre_start() and post_start(). Those are called on every start, whatever call_pre_post is set to.
Macros or Explicit Calls?
Because the expansion is so short, writing it out by hand is easy. For an item, the explicit version is start_item(), then randomize() with a check on its result, then finish_item(). For a child sequence, it is a call to start() with the sequencer and this as the parent.
The explicit form lets you choose call_pre_post, reuse an object, react properly to a randomization failure, and set a breakpoint on each step. The macro form is shorter and perfectly fine for simple, fully random stimulus. Many teams allow the macros in quick tests and prefer explicit calls in reusable sequence libraries.
Common Mistakes
Mistake #1: Relying on pre_body() in a macro-started sequence
The macro passes 0 for call_pre_post, so code in a child's pre_body() or post_body() silently never runs. The same child started from a test with start() would run it, which makes the bug easy to miss.
Mistake #2: Creating the object before calling the macro
Calling type_id::create() and then `uvm_do on the same handle wastes the first object, because the macro always builds a new one. Any fields set on the first object are lost. Use `uvm_create with `uvm_send or `uvm_rand_send if you need to prepare the object first.
Mistake #3: Ignoring the RNDFLD warning
If randomization fails, the macro only prints a RNDFLD warning and still sends the item with whatever values it had. Conflicting inline constraints therefore produce bad stimulus rather than a hard error. Consider promoting this warning to an error in your test.
Summary
In this article, you learned:
- All
`uvm_dovariants expand into`uvm_do_on_pri_with, which creates, randomizes and starts - Items go through
start_item()andfinish_item()with randomization in between, after the grant - Sequences are started with
start(SEQR, this, PRIORITY, 0), sopre_body()andpost_body()are skipped - The calling sequence's
pre_do(),mid_do()andpost_do()wrap every item or child it runs - Must-run setup and cleanup in a child sequence belong in
pre_start()andpost_start()