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_test and uvm_env connect to each other
  • How run_test() in a top-level module kicks off the entire UVM phasing mechanism
  • What happens inside build_phase and run_phase for 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.

Output
----------------------------------------------------------------
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_test and a uvm_env to 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 direct new() call
  • `uvm_info and the other severity macros are thin, efficient wrappers around the uvm_report_* functions
  • The topology printout and report summary are your first checkpoints for confirming a testbench built and ran correctly
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5