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_do expands into for an item and for a sequence
  • Predict the order of pre_do(), mid_do() and post_do() in nested sequences
  • Explain why pre_body() and post_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_on runs 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 is call_pre_post, so the child's pre_body() and post_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
Output
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's pre_do and mid_do print, tagged [seq1] because they belong to the parent. seq2's pre_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_do runs first, and only then seq3's own post_start(). Its pre_body() and post_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_do variants expand into `uvm_do_on_pri_with, which creates, randomizes and starts
  • Items go through start_item() and finish_item() with randomization in between, after the grant
  • Sequences are started with start(SEQR, this, PRIORITY, 0), so pre_body() and post_body() are skipped
  • The calling sequence's pre_do(), mid_do() and post_do() wrap every item or child it runs
  • Must-run setup and cleanup in a child sequence belong in pre_start() and post_start()
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5