Compile & Run a UVM Testbench - IEEE 1800.2 Quick Start (Questa, VCS, Xcelium)
An engineer joins your team, clones the testbench, and can't run it. They find a 2016 tutorial — this very post, in its original form — that says: download uvm-1.2.tar.gz from Accellera, unpack it, add uvm_pkg.sv to your file list, and pass +define+UVM_NO_DPI if the DPI link fails. They spend the afternoon fighting compile errors between their UVM and the one the simulator quietly loaded on its own. The fix, it turns out, was one flag. The download was never needed.
That workflow made sense in 2016. It is actively harmful advice in 2026. UVM stopped being "a library you download" the day it became IEEE 1800.2 — a standard your simulator implements and ships, precompiled, in its install tree. This post is the modern quick start: what actually happens when you run a UVM test, the one-flag flow on each major simulator, the runtime switches that make UVM usable — and, at the end, a ladder into the machinery underneath, down to swapping UVM's core services at runtime.
What Actually Happens When You "Run UVM"
Before the commands, the mental model. A UVM testbench is ordinary SystemVerilog: a package of classes, a top module with an initial run_test();, and the UVM library itself. Three things happen in sequence, and knowing which stage an error comes from is half of debugging any flow problem:
flowchart LR
A["Your sources
tb_pkg.sv, tb_top.sv"] --> C["Compile"]
B["UVM library
(ships with simulator)"] --> C
C --> D["Elaborate
build hierarchy"]
D --> E["Run
run_test()"]
E --> F["Factory creates test
from +UVM_TESTNAME"]
F --> G["Phases execute
build → run → report"]
classDef src fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
classDef lib fill:#d1fae5,stroke:#059669,color:#064e3b;
classDef run fill:#fef3c7,stroke:#d97706,color:#78350f;
class A src;
class B lib;
class C,D src;
class E,F,G run;
The step people underestimate is the fourth one. run_test() takes a string, checks whether +UVM_TESTNAME overrides it, and asks the UVM factory to construct a component whose registered name matches. That's why you can add a new test without touching tb_top.sv — and why a typo in +UVM_TESTNAME fails at time zero with a factory error, not a compile error. Keep this pipeline in mind; the Advanced section walks through it with the hood open.
UVM 1.2 vs IEEE 1800.2 — Why the Version in the Title Changed
UVM 1.2 (Accellera, 2014) was the last "download it yourself" release. In 2017 the API was standardized as IEEE 1800.2, revised in 2020, and Accellera's role shifted to publishing a reference implementation that tracks the standard. Practically, three things changed for you:
| Aspect | UVM 1.2 (2014) | IEEE 1800.2 |
|---|---|---|
| What it is | A library you download | A standard your simulator implements |
| How you get it | Accellera tarball, compile yourself | Precompiled in the simulator install |
Long-deprecated API (e.g. set_config_int) | Still present, warns | Removed — use uvm_config_db |
| Core services (factory, report server) | Global singletons | Accessed via uvm_coreservice_t — swappable |
For most testbenches, migration is undramatic: recompile against the 1800.2 library and fix what the compiler flags — overwhelmingly the removed set_config_* calls and a handful of renamed accessors. The payoff is that every simulator now converges on one documented API, and the extension points you'll meet in the Advanced section are guaranteed by an IEEE standard rather than by library internals.
uvm_coreservice_t material — the equivalent 1.2 spellings are noted where it matters.The Modern Flow: Use the UVM Your Simulator Ships
All three major simulators bundle UVM. You select it with a flag; you never add uvm_pkg.sv to a file list.
Questa / ModelSim
# Recent Questa releases make the bundled UVM visible automatically:
vlog tb_pkg.sv tb_top.sv
vsim -c tb_top +UVM_TESTNAME=smoke_test -do "run -all; quit"
Questa ships precompiled UVM libraries (both 1.2 and 1800.2 trees) inside the install; import uvm_pkg::* resolves against them with no +incdir. Version selection switches vary by release — check vlog -help in your install rather than trusting a blog post (including this one).
VCS
# -ntb_opts uvm-ieee selects the bundled IEEE 1800.2 library
vcs -sverilog -ntb_opts uvm-ieee tb_pkg.sv tb_top.sv
./simv +UVM_TESTNAME=smoke_test
# Legacy projects: -ntb_opts uvm-1.2
Xcelium
# -uvm pulls in the bundled UVM; -uvmhome selects a specific tree
xrun -uvm tb_pkg.sv tb_top.sv +UVM_TESTNAME=smoke_test
The Manual Flow — and the Three Times You Actually Need It
The old version of this post treated hand-compiling UVM as the normal path. Today it's a specialist move, but it still has three legitimate uses: (1) pinning one exact UVM version across multiple simulators so cross-tool regressions can't diverge, (2) reproducing a suspected UVM library bug against the unmodified reference code, and (3) running a locally patched UVM. In those cases, fetch Accellera's reference implementation (the uvm-core releases tracking 1800.2-2020) and compile it explicitly:
# File list (tb.f) — manual UVM compile, 1800.2 reference implementation
+incdir+uvm-core/src
uvm-core/src/uvm_pkg.sv
+incdir+./tb
./tb/tb_pkg.sv
./tb/tb_top.sv
vlib work
vlog -f tb.f
vsim -c tb_top -sv_lib uvm-core/lib/uvm_dpi +UVM_TESTNAME=smoke_test -do "run -all; quit"
Note what's absent: +define+UVM_NO_DPI. The 2016 version of this post presented that flag as near-routine. It isn't. DPI is on by default and powers three things you want: regular-expression matching for factory overrides and config_db wildcards, register-model backdoor access (uvm_hdl_read/uvm_hdl_deposit), and the command-line processor behind every +uvm_set_* plusarg. Disable DPI and those silently degrade. Treat UVM_NO_DPI as a last resort for a simulator that genuinely cannot link C code — not as a convenience.
The Runtime Control Toolbox
Compiling once and steering everything from the run command is the entire point of UVM's plusarg system. These are the switches worth memorizing:
| Plusarg | What it does |
|---|---|
+UVM_TESTNAME=<test> | Selects the test class via factory lookup — one compile, many tests |
+UVM_VERBOSITY=<level> | Global message filter: UVM_LOW, UVM_MEDIUM, UVM_HIGH, UVM_DEBUG |
+uvm_set_verbosity=<comp>,<id>,<level>,<phase> | Surgical verbosity for one component — see Advanced section |
+UVM_MAX_QUIT_COUNT=<n>,NO | Stop after n UVM_ERRORs instead of simulating garbage all night |
+uvm_set_config_int=<comp>,<field>,<value> | Poke a config_db value from the command line — no recompile |
+UVM_CONFIG_DB_TRACE | Logs every config_db set/get — the #1 debug flag for "my get() returns nothing" |
+UVM_OBJECTION_TRACE | Logs objection raise/drop — the #1 debug flag for "my test ends early / never ends" |
+UVM_PHASE_TRACE | Logs phase transitions |
Seeding lives one level down, at the simulator: vsim -sv_seed random (Questa), +ntb_random_seed_automatic (VCS), -svseed random (Xcelium). The seed must come from the tool, not from your code — it's printed in the log, and re-running with that exact seed is what makes a 3 a.m. failure reproducible at 10 a.m.
Common Mistakes
- Compiling your own
uvm_pkg.svwhile the tool loads its bundled one. Symptom: pages of "type is incompatible with class uvm_object" errors that make no sense. Two differentuvm_pkgcompilations are meeting in one elaboration. Delete your copy; use the vendor flag. - Cargo-culting
+define+UVM_NO_DPIfrom old tutorials, then wondering why backdoor register access and factory wildcard overrides quietly stopped working. - Using global
+UVM_VERBOSITY=UVM_HIGHas a debugger. That's a 2 GB log with the answer buried at line 4,801,223. Use+uvm_set_verbosityon the one component you suspect. - No
+UVM_MAX_QUIT_COUNTin the regression flow. One broken driver produces 40,000 identical UVM_ERRORs and an overnight run that told you nothing after the first failure. - Forgetting
`uvm_component_utilson a new test. The factory can only construct what's registered; the failure is a time-zero "requested type not found" — always a registration problem, never a compile problem.
Interview Corner
Q: Walk me through what happens between+UVM_TESTNAME=my_test and your build_phase executing.
A: run_test() in tb_top hands control to uvm_root, which reads +UVM_TESTNAME (overriding any string argument), asks the factory to create a component of that registered name as uvm_test_top, and then starts phasing. build_phase runs top-down from uvm_test_top, constructing the environment. If the factory lookup fails, the simulation ends at time zero — which is why a test forgotten from `uvm_component_utils dies before a single clock edge.
A: Three subsystems are implemented in C for capability or speed: regex matching (factory and config_db wildcards), HDL backdoor access (uvm_hdl_read/deposit/force, which register-model backdoor operations rely on), and the command-line processor behind the +uvm_set_* family. With UVM_NO_DPI, regex degrades to simple globbing, backdoor accesses fail, and command-line overrides go dead.
A: Governance and guarantees more than day-to-day API. 1800.2 turned the API into an IEEE standard that all simulators implement natively; long-deprecated calls like set_config_int were finally removed, and core services (factory, report server) became formally replaceable through uvm_coreservice_t. A typical 1.2 testbench migrates with a recompile and mechanical fixes.
A: The seed came from the simulator's seeding mechanism and was logged; all randomization derives from it hierarchically; and the run command is reconstructable (same compile, same plusargs). Then -sv_seed 3728194522 reproduces the failure exactly. If any test seeds itself internally — $urandom in an initial block, time-based seeds — reproducibility is gone and the seed in the log is a lie.
Beyond the Quick Start: Advanced → Expert
Everything above gets a testbench running. This section is the ladder down into the machinery — each rung is optional, and each one is where some real project eventually ends up.
Level 1 — A team-grade compile flow
Solo flows recompile everything, every time. Team flows separate rarely-changing from often-changing code: the UVM library and VIP packages compile once into a shared library area; your testbench compiles incrementally against them; only the run step varies per test. On Questa that's distinct vlib/vlog -work targets and -L at elaboration; VCS and Xcelium have equivalent partition-compile flows. The structural win is that a one-line sequence edit costs seconds, not a full-library recompile — and the shared library pins one UVM version for everyone, killing "works on my machine" at the root.
Level 2 — Surgical verbosity and message control
The verbosity system is hierarchical and time-aware, which almost nobody exploits:
# UVM_HIGH for one driver only, and only in run phase:
./simv +UVM_TESTNAME=stress_test \
+uvm_set_verbosity=uvm_test_top.env.agt.drv,_ALL_,UVM_HIGH,run
# Escalate a specific message ID's action, no recompile:
+uvm_set_action=uvm_test_top.env.scb,DATA_MISMATCH,UVM_ERROR,UVM_DISPLAY|UVM_STOP
The second line turns one scoreboard message into a breakpoint for one run. For programmatic control of the same machinery — demoting expected errors during an error-injection test, counting messages — see UVM Report Catcher, which is the class-based face of this system.
Level 3 — Swapping UVM's core services (1800.2)
Here is the payoff of 1800.2's uvm_coreservice_t: the factory, report server, and root are not hardwired singletons — they're services you can replace. The classic use is a custom report server that emits machine-readable logs for a CI dashboard:
class json_report_server extends uvm_default_report_server;
virtual function string compose_report_message(
uvm_report_message report_message, string report_object_name);
return $sformatf("{\"sev\":\"%s\",\"id\":\"%s\",\"msg\":\"%s\",\"time\":%0t}",
report_message.get_severity().name(),
report_message.get_id(),
report_message.get_message(), $time);
endfunction
endclass
// In your base test's build_phase, before anything logs:
json_report_server srv = new();
uvm_report_server::set_server(srv);
Every `uvm_info/warning/error in the entire testbench — including inside VIP you don't own — now flows through your formatter. The same seam lets advanced flows substitute a custom factory (override bookkeeping, instrumentation) via uvm_coreservice_t::get().set_factory(). This is the standardized version of what used to require patching library internals.
Level 4 — The DPI boundary, from the inside
The register model's backdoor is the deep end of the DPI material: uvm_hdl_deposit("tb_top.dut.csr.status", value) writes an RTL signal directly through the simulator's C interface, letting a register test initialize or check state in zero simulation time — and uvm_hdl_force/release can hold a net against the design's own drivers. Two expert-level cautions: backdoor paths are strings resolved at runtime, so a hierarchy refactor breaks them silently until that test runs (guard them with a smoke test that touches every path); and forcing signals bypasses the very protocol logic you're paid to verify — legitimate for bring-up and fault injection, incriminating anywhere else.
Level 5 — UVM beyond the big-three simulators
Two frontiers worth watching. Verilator's UVM support is progressing and, as of early 2026, can run meaningful subsets of UVM testbenches — still experimental, but the direction (open-source UVM regressions in CI containers, no license queue) is clear enough to plan around. And UVM's architecture has escaped SystemVerilog entirely: UVM-SystemC implements the same phasing, factory, and sequence machinery in C++ for virtual-platform verification. If you want to see a UVM agent — driver, sequencer, monitor — rebuilt in SystemC with the concepts laid bare, that's Part 25 of the SystemC series: the same ideas, second language, deeper understanding of both.
Key Takeaways
- UVM is an IEEE standard your simulator ships, not a library you download. One flag (
-ntb_opts uvm-ieee,xrun -uvm, or nothing at all on Questa) replaces the entire 2016 download-and-compile ritual. - Hand-compiling the Accellera reference implementation is a specialist move for version pinning and library debugging — and if you do it, leave DPI on.
- One compile, many runs:
+UVM_TESTNAME, seeds, verbosity, and config pokes all belong on the run command, and the trace plusargs (+UVM_CONFIG_DB_TRACE,+UVM_OBJECTION_TRACE) solve the two most common "why is my testbench haunted" mysteries. - The advanced surface — partition compiles, per-component verbosity windows, swappable report servers and factories, HDL backdoor — is all reachable from the standard API. The quick start is the same machine with the panels closed.
Now that it runs: learn to drive stimulus with UVM Sequences Fundamentals, add checking with the SVA Guide, and see how agents package into reusable VIP in Understanding Verification IP.
Comments (0)
Leave a Comment