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_phaseand inheritbuild_phaseunchanged, 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
- Replacing components and transactions from a test: UVM Factory Override
- Every phase and its execution order: UVM Phases
- Objections in full: UVM Objection