Skip to main content

Getting Started with UVM

The single most common misconception for someone new to UVM: expecting to "install" it like a standalone tool. There's no UVM application, no package manager, nothing to launch. UVM is a SystemVerilog class library — a directory of .sv files defining classes like uvm_component and uvm_driver — that gets compiled directly alongside your own testbench code. If you can already compile a SystemVerilog project, you already have almost everything you need.

Where the library actually comes from​

  • It's usually already on your machine. Every major commercial simulator (Questa, VCS, Xcelium) ships a copy of the UVM source under its own install tree, since the standard requires it. Check there first before downloading anything separately.
  • Accellera — the standards body that maintains UVM — also publishes the reference implementation directly (uvm-core, on GitHub), for cases where a project needs a specific version pinned independently of whatever a given simulator bundles.
  • The library implements IEEE 1800.2, the standard that formalized UVM (see Introduction to UVM for how it got there). Different simulators may bundle slightly different point releases, which is a real, if usually minor, source of cross-project compatibility friction — worth knowing about even if it rarely bites.
  • For quick experiments with no local setup at all, browser-based tools like EDA Playground have UVM preloaded against multiple simulators.

The compile-time mechanics​

Getting the library into your build comes down to two things: telling the compiler where the source lives, and importing the package.

# Typical simulator invocation — exact flags vary by tool
vlog -incdir $UVM_HOME/src $UVM_HOME/src/uvm_pkg.sv my_testbench.sv
  • $UVM_HOME — an environment variable pointing at the UVM installation root. This is a convention, not something UVM enforces itself, but nearly every project and simulator's own examples assume it exists.
  • -incdir — makes `include directives inside the UVM source resolvable.
  • uvm_pkg.sv — the actual package file being compiled; everything else is pulled in through its own includes.
  • In your own source, this shows up as two lines that need to appear together at the top of any file using UVM:
import uvm_pkg::*;
`include "uvm_macros.svh"
A gotcha specific to environment variables

$UVM_HOME set in one terminal session vanishes the moment that shell closes, unless it's persisted in a shell profile (.bashrc, .cshrc, or your CI environment's own config). A build that works today and mysteriously "can't find UVM" tomorrow is very often just a lost environment variable, not a real compile problem. On a team, mixing UVM versions across engineers' environments is also a real source of subtle, hard-to-reproduce bugs — worth standardizing early.

A minimal UVM testbench, with no DUT at all​

Before wiring up a real interface, it's worth seeing the smallest possible thing UVM will actually run — deliberately with no DUT, no interface, no stimulus. The goal here is purely to see the class-based mechanics move, so they're not new and unfamiliar later when real signals are added.

class hello_env extends uvm_env;
`uvm_component_utils(hello_env)

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

class hello_test extends uvm_test;
`uvm_component_utils(hello_test)
hello_env env;

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

function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = hello_env::type_id::create("env", this);
endfunction

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

task run_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("HELLO", "Hello, UVM!", UVM_LOW)
phase.drop_objection(this);
endtask
endclass

module tb_top;
initial run_test("hello_test");
endmodule

A few things worth noticing here specifically because they'll recur constantly from this point on:

  • env is built via type_id::create(), never new(). This one habit, formed now, is what makes the factory (covered later in this section) work at all later — see Utility & Field Macros for why.
  • uvm_top.print_topology() in end_of_elaboration_phase dumps the entire component tree UVM actually built, as text, before simulation runs. On a real testbench with a dozen components, this is one of the fastest ways to confirm the hierarchy is what you think it is — it's worth reaching for immediately whenever a connection seems to be silently not working.
  • The objection around `uvm_info matters more than it looks — without raise_objection/drop_objection, run_phase has no reason to stay open at all, so nothing here would print reliably. This is covered in full in Phases & Objections.

Running it produces one line of output and a printed tree with exactly two components (uvm_test_top and its env child) — small enough to read at a glance, which is the point. Everything from here builds on top of this same skeleton: a test that builds an environment and starts something in run_phase.

Selecting the test and verbosity without recompiling​

initial run_test("hello_test"); hardcodes which test runs — fine for this one-test example, but a real testbench usually has many tests sharing one compiled image. Two command-line plusargs make that practical:

  • +UVM_TESTNAME=<name> — passed at simulation time (not compile time), this tells run_test() which factory-registered test to construct, overriding (or replacing entirely) a hardcoded argument. Writing initial run_test(); with no argument at all and always selecting the test via +UVM_TESTNAME on the command line is the more common pattern once a project has more than a couple of tests — it means switching tests never requires recompiling.
  • +UVM_VERBOSITY=<level> — sets the initial verbosity (UVM_NONE, UVM_LOW, UVM_MEDIUM, UVM_HIGH, UVM_FULL) for every component at simulation start, without editing any `uvm_info call. This is what actually decides whether `uvm_info("HELLO", "Hello, UVM!", UVM_LOW) above prints at all — its message only shows if the active verbosity is UVM_LOW or higher.

Both are read directly off the simulator's command line at elaboration time, before run_phase starts — no testbench code has to parse them explicitly.