You've seen the UVM class hierarchy and you have UVM installed. Now let's actually run something. The goal here is small on purpose: build the smallest possible UVM testbench that prints "Hello UVM!" to the simulation log, so you can see how a uvm_test and a uvm_env fit together - with no DUT, no interface, and no configuration database involved yet. Those pieces matter a lot once you're actually verifying a design, but they're not needed just to get UVM's class hierarchy and phasing running for the first time, so we'll leave them for the chapter where you build your first real testbench.
We'll keep this testbench to two components:
- A test, which sets things up and decides what runs
- An environment, which does the actual printing
5 min read | Intermediate Level
What You'll Learn
- How a minimal
uvm_testanduvm_envconnect to each other - How
run_test()in a top-level module kicks off the entire UVM phasing mechanism - What happens inside
build_phaseandrun_phasefor a basic component - How to read UVM's topology printout and report summary
Testbench Setup
The whole testbench is just the environment instantiated inside the test, with the test started from a top-level module. There's no design under test here at all - this is purely about seeing UVM's class structure and phases run.
Test
The test is an object of uvm_test and owns the environment. In a real testbench, the test is also where you'd choose which sequence runs and where you'd pull in configuration - useful later on, since it lets you swap test scenarios without touching the rest of the testbench. Here, its only job is to construct the environment. Let's walk through the class piece by piece.
class base_test extends uvm_test;
This defines a class called base_test that inherits all the properties and methods of uvm_test. Next, we register the component with the UVM factory using the macro `uvm_component_utils, since a test is a kind of uvm_component.
`uvm_component_utils (base_test)
Now we declare a handle to the environment:
my_env m_top_env;
The constructor new() takes the standard name, parent arguments every uvm_component needs:
function new (string name, uvm_component parent = null);
super.new (name, parent);
endfunction : new
The build_phase constructs the environment. Note that we call the parent class's build_phase first using super, before doing our own work.
The line using type_id is the preferred way to construct UVM objects. It routes the object creation through the factory, which means the type can be substituted later - for example, swapping in an extended environment for a different test - without editing this code. You could call new() directly instead, but that bypasses the factory and gives up that flexibility, so it isn't recommended.
virtual function void build_phase (uvm_phase phase);
super.build_phase (phase);
m_top_env = my_env::type_id::create ("m_top_env", this);
endfunction : build_phase
Real testbenches almost always need to pass information - like a virtual interface handle - from outside UVM into the test using uvm_config_db. That mechanism is covered in its own chapter once you have an actual interface and DUT to connect.
You can also print the testbench's component hierarchy after the build and connect phases finish, which is a handy sanity check that everything got constructed where you expect:
virtual function void end_of_elaboration_phase (uvm_phase phase);
uvm_top.print_topology ();
endfunction
Environment
The environment's only job here is to print "Hello UVM!" during the run phase.
class my_env extends uvm_env ;
`uvm_component_utils (my_env)
function new (string name, uvm_component parent);
super.new (name, parent);
endfunction : new
function void build_phase (uvm_phase phase);
super.build_phase (phase);
endfunction : build_phase
task run_phase (uvm_phase phase);
`uvm_info (get_name(), $sformatf ("Hello UVM ! Simulation has started."), UVM_LOW)
endtask : run_phase
endclass : my_env
`uvm_info is the macro almost every testbench uses for informational messages. It wraps the underlying uvm_report_info function, automatically stamping in the file name and line number, and - importantly - it skips the message-formatting work entirely if the verbosity level would filter the message out anyway. That's why `uvm_info/`uvm_warning/`uvm_error/`uvm_fatal are the preferred way to report, rather than calling uvm_report_info and friends directly.
UVM_LOW is the verbosity level of this message, not a delay or a severity. Every component has a verbosity threshold (UVM_MEDIUM by default), and a message only prints if its level is at or below that threshold. UVM_LOW messages print under almost any threshold, which is why it's a safe default for a message you always want to see.
Top-Level Module
Everything above this point is pure UVM class code - it doesn't run by itself. Something has to actually kick off the simulation and start UVM's phasing, and that's the job of a small top-level module. The global run_test() task does this: it looks up the test you name, constructs it, and drives it through every UVM phase from build_phase to run_phase and beyond.
module tb_top;
import uvm_pkg::*;
`include "uvm_macros.svh"
initial begin
run_test ("base_test");
end
endmodule
That's the entire simulation environment: one module, one line that matters (run_test("base_test")), and the two classes you just wrote.
Passing the test name directly to run_test() works fine for a single fixed test like this. Once you have a library of tests, the more common approach is to leave the argument blank - run_test(); - and select the test from the simulator command line instead, using +UVM_TESTNAME=base_test. That way you don't need to recompile to switch tests.
---------------------------------------------------------------- CDNS-UVM-1.1d (14.10-s004) (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 - @2601 m_top_env my_env - @201 ------------------------------------ UVM_INFO ./tb/my_pkg.sv(28) @ 0: uvm_test_top.m_top_env [m_top_env] Hello UVM ! Simulation has started. --- UVM Report catcher Summary --- Number of demoted UVM_FATAL reports : 0 Number of demoted UVM_ERROR reports : 0 Number of demoted UVM_WARNING reports: 0 Number of caught UVM_FATAL reports : 0 Number of caught UVM_ERROR reports : 0 Number of caught UVM_WARNING reports : 0 --- UVM Report Summary --- ** Report counts by severity UVM_INFO : 3 UVM_WARNING : 0 UVM_ERROR : 0 UVM_FATAL : 0 ** Report counts by id [RNTST] 1 [UVMTOP] 1 [m_top_env] 1 Simulation complete via $finish(1) at time 0 FS + 179
This log is adapted from a real simulation run of the original two-message version of this testbench, trimmed down to match the single `uvm_info call shown above. The exact line number in ./tb/my_pkg.sv(28) will differ in your own file depending on where the call actually lands - what matters is the message text, the [m_top_env] tag identifying which component logged it, and the UVM_INFO severity.
Two things worth noticing in this log: the testbench topology printout confirms m_top_env was built correctly under uvm_test_top, and the report summary at the bottom shows 3 UVM_INFO messages with zero warnings, errors, or fatals - the simplest possible "clean run." Two of those three messages ([RNTST] and [UVMTOP]) are printed automatically by UVM itself; only the third, tagged [m_top_env], is the message you actually wrote.
The version banner above (CDNS-UVM-1.1d) reflects whichever UVM version and simulator produced this particular log. Your own output will show your simulator's name and the UVM version you've got configured - the structure of the topology table and report summary will look the same either way.
Common Mistakes
Mistake #1: Forgetting to call super.build_phase(phase)
Wrong: function void build_phase(uvm_phase phase); m_top_env = ...
Right: super.build_phase(phase); m_top_env = ...
Why? Skipping the parent class's build_phase call can silently break configuration inheritance that the base class sets up, even though your own code might appear to run fine.
Mistake #2: Using new() Instead of the Factory
Calling m_top_env = new("m_top_env", this) works, but it bypasses the UVM factory entirely. If you later want to substitute a derived environment for a specific test, factory-based creation through type_id::create() lets you do that without editing this file; a plain new() call doesn't.
Mistake #3: Forgetting the Test Name in run_test()
If you call run_test(); with no argument and don't pass +UVM_TESTNAME on the simulator command line either, UVM has no test to run and the simulation will end immediately or report an error, depending on your simulator. While building your first example, it's simplest to just hardcode the name as shown above.
Where Does This Pattern Show Up in Real Projects?
The test-plus-environment split you just built is the skeleton of every UVM testbench, no matter how complex. Production testbenches add agents, scoreboards, and coverage collectors inside the environment, and multiple test classes that each configure a different sequence - but the core relationship between a thin test class, a heavier environment class, and a one-line top module stays the same.
Practice Exercise
Challenge: Personalize the Message
Modify my_env's run_phase so the message prints your own name instead of the generic "Hello UVM!" text. Then, in base_test's build_phase, change the instance name passed to create() from "m_top_env" to something else, and re-run. Watch how the tag in front of your message in the log changes to match.
Hint
get_name() returns whatever instance name the component was created with. The tag UVM prints before your message - like [m_top_env] - comes directly from that name, not from the class name my_env.
Solution
// In my_env:
task run_phase (uvm_phase phase);
`uvm_info (get_name(), $sformatf ("Hello from your_name_here ! UVM is running."), UVM_LOW)
endtask : run_phase
// In base_test's build_phase:
m_top_env = my_env::type_id::create ("my_renamed_env", this);
After this change, the log tag becomes [my_renamed_env] instead of [m_top_env], even though the class is still called my_env. This is worth internalizing early: the name UVM shows you in messages and in the topology printout is the instance name you chose, not the class type.
Summary
In this tutorial, you learned:
- A minimal UVM testbench needs just a
uvm_testand auvm_envto run end to end - no DUT required - A top-level module's
run_test()call is what actually starts UVM's phasing mechanism - Object creation should go through the factory (
type_id::create()) rather than a directnew()call `uvm_infoand the other severity macros are thin, efficient wrappers around theuvm_report_*functions- The topology printout and report summary are your first checkpoints for confirming a testbench built and ran correctly