reg_env is fully built, but nothing has ever actually instantiated it. This article writes the component that finally does: a test that builds reg_env, starts reg_sequence against it, and gives this entire tutorial's testbench its first real run.

9 min read | Intermediate Level

What You'll Learn

  • What a test does, and why the same environment gets reused across many of them
  • How to build reg_test and start reg_sequence inside run_phase
  • How to write a derivative test that reuses reg_test but runs a different sequence
  • How to set a real default sequence, now that every name it depends on actually exists

What Is a Test?

A test verifies a specific feature or behavior of a design. Real verification plans typically need many tests - one for basic writes and reads, one for boundary addresses, one for back-to-back operations with no idle cycles, and so on. Rewriting the entire environment for each one would be wasteful, so instead, the same reg_env gets reused across all of them, with each test only changing what it actually needs to: which sequence runs, and which knobs are tweaked beforehand.

Building reg_test

1. Declare the Class


class reg_test extends uvm_test;

  `uvm_component_utils(reg_test)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  // Code for rest of the steps come here
endclass

2. Declare and Build the Environment


reg_env m_top_env;

virtual function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  m_top_env = reg_env::type_id::create("m_top_env", this);
endfunction

The source this article is built on also shows a configuration object built and passed down here, useful once an environment has knobs worth bundling into one object - the pattern covered in this chapter's deep dive on multi-agent environments. reg_env doesn't need one: it always builds the same agent and the same scoreboard, so there's nothing for reg_test to configure before building it.

3. Print the Topology


virtual function void end_of_elaboration_phase(uvm_phase phase);
  uvm_top.print_topology();
endfunction

By end_of_elaboration_phase, every component this tutorial has built is guaranteed to already exist - reg_test, reg_env, reg_agent and everything inside it, reg_scoreboard. print_topology() prints the entire hierarchy as built, which is a quick way to confirm nothing was left out or built in the wrong place before the simulation actually starts running anything.

4. Start a Sequence in run_phase


virtual task run_phase(uvm_phase phase);
  reg_sequence m_seq = reg_sequence::type_id::create("m_seq");

  phase.raise_objection(this);
  m_seq.start(m_top_env.m_agent.m_sequencer);
  phase.drop_objection(this);
endtask

Every piece of this line was established and explained from its own side, in its own article: the objection-raising pattern from How to Create and Use a Sequence, reg_sequence itself, and the path m_top_env.m_agent.m_sequencer, which only resolves to something real because reg_env owns m_agent and reg_agent owns m_sequencer. Nothing about any of those three classes needed to change to make this call work.

Quick Check

Q: Would m_seq.start(m_top_env.m_agent.m_sequencer) still work if reg_agent were configured as passive?

Show Answer

No - a passive reg_agent never builds m_sequencer in the first place, so m_top_env.m_agent.m_sequencer would be a null handle by the time run_phase tries to call start() on it. Starting a sequence against an agent only makes sense when that agent is active.

Setting a Real Default Sequence

How to Create and Use a Sequence introduced default sequences with a placeholder path, since reg_sequencer didn't have a fixed position in any hierarchy yet. It does now:


function void start_of_simulation_phase(uvm_phase phase);
  super.start_of_simulation_phase(phase);

  uvm_config_db #(uvm_object_wrapper)::set(this, "m_top_env.m_agent.m_sequencer.run_phase",
                                            "default_sequence", reg_sequence::type_id::get());
endfunction

"m_top_env.m_agent.m_sequencer.run_phase" is no longer a stand-in for a path that doesn't exist - it's the actual, resolvable location of reg_sequencer inside reg_test's own hierarchy. Setting this means reg_sequencer starts reg_sequence automatically once its run phase begins, with no explicit start() call needed anywhere - an alternative to the run_phase code shown in the previous step, not something used alongside it.

A Derivative Test

reg_test only ever starts reg_sequence - the same five-transaction, random read/write sequence every time. A second test that needs different stimulus doesn't have to rebuild reg_env from scratch. The cleanest way to give it different stimulus is a derivative sequence, extending reg_sequence with one extra constraint inside body(), the same inline with pattern already used in that article's own practice exercise:


class reg_write_only_sequence extends reg_sequence;

  `uvm_object_utils(reg_write_only_sequence)

  function new(string name = "reg_write_only_sequence");
    super.new(name);
  endfunction

  virtual task body();
    repeat (5) begin
      req = reg_transaction::type_id::create("req");
      start_item(req);
      assert(req.randomize() with { wr_en == 1; });
      finish_item(req);
    end
  endtask

endclass

With that sequence written, the derivative test itself only needs to override which sequence it starts:


class reg_write_only_test extends reg_test;

  `uvm_component_utils(reg_write_only_test)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    reg_write_only_sequence m_seq = reg_write_only_sequence::type_id::create("m_seq");

    phase.raise_objection(this);
    m_seq.start(m_top_env.m_agent.m_sequencer);
    phase.drop_objection(this);
  endtask

endclass

Because reg_write_only_test doesn't define its own build_phase at all, it inherits reg_test's exactly as written - m_top_env still gets built the same way, with no duplicated code. Only the sequence being started changes, and that change lives entirely in run_phase.

Running a Test

Actually launching reg_test or reg_write_only_test happens through a global task, run_test(), typically called from the top-level module - covered in detail in UVM Testbench Top, along with how a test gets its virtual interface handle in the first place.

Common Mistakes

Mistake #1: Forgetting to raise an objection in run_phase

Without phase.raise_objection(this), the run phase has nothing telling it to stay alive, and the simulation can end before m_seq.start(...) ever finishes - or in some simulators, before it even begins.

Mistake #2: Defining build_phase in a derivative test when nothing about it actually changes

Copying reg_test's build_phase into reg_write_only_test unchanged works, but it means any later change to how reg_env gets built has to be repeated in every derivative test that copied it. Leaving build_phase out entirely, as reg_write_only_test does, means it's inherited automatically and only ever needs updating in one place.

Practice Exercise

Challenge: A Read-Only Derivative Test

Following the same shape as reg_write_only_sequence and reg_write_only_test, write a reg_read_only_sequence and a reg_read_only_test that bias every generated transaction toward reads instead of writes.

Hint

This is structurally identical to the write-only pair - same two classes, same inherited build_phase on the test side, only the constraint condition itself changes on the sequence side.

Solution
class reg_read_only_sequence extends reg_sequence;

  `uvm_object_utils(reg_read_only_sequence)

  function new(string name = "reg_read_only_sequence");
    super.new(name);
  endfunction

  virtual task body();
    repeat (5) begin
      req = reg_transaction::type_id::create("req");
      start_item(req);
      assert(req.randomize() with { rd_en == 1; });
      finish_item(req);
    end
  endtask

endclass

class reg_read_only_test extends reg_test;

  `uvm_component_utils(reg_read_only_test)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    reg_read_only_sequence m_seq = reg_read_only_sequence::type_id::create("m_seq");

    phase.raise_objection(this);
    m_seq.start(m_top_env.m_agent.m_sequencer);
    phase.drop_objection(this);
  endtask

endclass
  

Only the constrained field changed, from wr_en == 1 to rd_en == 1, on the sequence side - the test side is structurally identical to reg_write_only_test, down to the inherited build_phase.

Summary

In this article, you learned:

  • A test builds an environment and starts a sequence against it, reusing the same environment across many different tests
  • reg_test starts reg_sequence on m_top_env.m_agent.m_sequencer, a path that only resolves because every component along it is built correctly
  • A derivative test can override just run_phase and inherit build_phase unchanged, avoiding duplicated environment-building code
  • A default sequence's path is no longer a placeholder once the components it names actually exist in the hierarchy

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5