Chapter 3 of 10
Build and Run Your First UVM Test
Reuse the SV Basics capstone's mux2 DUT, wrap it in a minimal UVM skeleton, and actually run it — a near-empty driver, a test, and the config_db handoff that gets a virtual interface from the module domain into the UVM domain.
Chapter 2 covered uvm_component, phasing, objections, and reporting — all concepts, nothing running yet. This chapter fixes that: the same mux2 DUT you drove by hand in the SV Basics capstone (chapter 16), now driven by an actual UVM skeleton, actually running. Mirrors the role SV ch3 (first-simulation) played in that track — get something running before going deep.
Before anything else: pick a UVM-capable simulator
SV ch3 told you any free EDA Playground simulator, including Icarus Verilog, covers that whole track. That stops being true here — Icarus Verilog doesn't support the UVM class library, so running this chapter's example on it fails with an opaque "uvm_pkg not found"-style error, not a helpful one.
On EDA Playground's Tools & Simulators panel, pick a simulator that lists UVM support (for example, Aldec Riviera-PRO, which EDA Playground has historically offered without requiring your own commercial license — check the current list, since simulator offerings there do change over time) and, if the simulator's options expose a UVM library version, select one (UVM 1.2 is a safe default). Every example from here on assumes this.
The DUT and interface: unchanged from the capstone
Same mux2 and mux2_if as chapter 16's capstone — nothing new here, just a reminder of what they look like:
interface mux2_if;
logic sel, a, b, y;
modport dut_mp (input sel, a, b, output y);
modport tb_mp (output sel, a, b, input y);
endinterface
module mux2 (mux2_if.dut_mp bus);
always_comb begin
bus.y = bus.sel ? bus.b : bus.a;
end
endmoduleThe driver: a near-empty uvm_component
The capstone's driver class held a virtual mux2_if.tb_mp vif, assigned through its constructor. This driver holds the same handle, but gets it a different way — through uvm_config_db, UVM's standard way to hand a virtual interface from the plain-SystemVerilog module domain into the UVM class domain. The full explanation of why this works waits until chapter 5; for now, treat it as the standard pattern to copy:
class my_driver extends uvm_component;
`uvm_component_utils(my_driver)
virtual mux2_if.tb_mp vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual mux2_if.tb_mp)::get(this, "", "vif", vif))
`uvm_fatal("DRV", "no virtual interface set for vif -- check the config_db::set() call in the top module")
endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
vif.sel = 0; vif.a = 1; vif.b = 0;
#1;
`uvm_info("DRV", $sformatf("sel=%0b a=%0b b=%0b -> y=%0b", vif.sel, vif.a, vif.b, vif.y), UVM_MEDIUM)
vif.sel = 1; vif.a = 1; vif.b = 0;
#1;
`uvm_info("DRV", $sformatf("sel=%0b a=%0b b=%0b -> y=%0b", vif.sel, vif.a, vif.b, vif.y), UVM_MEDIUM)
phase.drop_objection(this);
endtask
endclassTwo familiar things and one new pattern:
super.build_phase(phase)first, then theconfig_db::get()call — exactly the habit chapter 2 covered.get()returns abit:1on success,0if nothing was everset()for this path, which is why it's wrapped inif (!...)with a`uvm_fatal— a driver that silently drove garbage because its virtual interface was never assigned would be a much worse failure mode than stopping immediately with a clear message.run_phasedoes exactly two things, wrapped inraise_objection/drop_objection(chapter 2 again): drive asel/a/bcombination, wait a time unit for the combinational logic to settle (same#1simplification the capstone used), and`uvm_infothe result.
The test: constructs the driver, nothing else
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_driver drv;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = my_driver::type_id::create("drv", this);
endfunction
endclassuvm_test is a uvm_component subclass UVM provides as a conventional marker for the top of the hierarchy — nothing about it behaves differently from any uvm_component you already know from chapter 2. my_driver::type_id::create("drv", this) builds the driver instead of a plain new(...) call — this is the UVM factory at work, and why it's spelled that way instead of new waits until chapter 6. For now, the rule to copy is simple: inside build_phase, construct child components with SomeClass::type_id::create("instance_name", this), not new(...).
Wiring it up: the top-level module
`include "uvm_macros.svh"
import uvm_pkg::*;
module tb_top;
mux2_if bus_if();
mux2 dut (bus_if);
initial begin
uvm_config_db#(virtual mux2_if.tb_mp)::set(null, "*", "vif", bus_if);
run_test("my_test");
end
endmoduleEvery file that uses UVM needs `include "uvm_macros.svh" and import uvm_pkg::*; before anything else — that's what makes `uvm_info, uvm_component, uvm_config_db, and everything else UVM-provided available at all.
The initial block does two things, in order: uvm_config_db#(virtual mux2_if.tb_mp)::set(null, "*", "vif", bus_if) publishes the real bus_if instance under the field name "vif", visible to any component that asks (the "*" means "any instance path"); then run_test("my_test") starts the phase engine, constructs exactly one my_test as the root of the component tree (conventionally called uvm_test_top), and runs every phase in order. set() has to happen before run_test() — chapter 2's "build_phase runs top-down" material is exactly why: by the time my_test's build_phase constructs drv, and drv's own build_phase calls get(), the value is already there waiting.
What this leaves unexplained (exactly two things)
`uvm_component_utils/type_id::create()— this is the UVM factory; chapter 6 covers what it actually does and why it matters.uvm_config_db#(...)::set()/get()— chapter 5 covers the full mechanics: what the context/path/field-name arguments mean, and whybuild_phase's ordering guarantees it works.
Everything else — `uvm_info, run_phase, raise_objection/drop_objection, super.build_phase — is already-covered chapter 2 material put to use.
Run it, and the log should show build_phase/connect_phase/run_phase activity followed by two `uvm_info lines from DRV, each reporting a sel/a/b/y combination — the same result the capstone's hand-written driver produced, now coming from an actual UVM component tree.
Summary
- Icarus Verilog doesn't support UVM — pick a UVM-capable simulator on EDA Playground (e.g. Riviera-PRO) before running any UVM example.
- The DUT and interface are unchanged from the SV Basics capstone; what's new is a
uvm_component-based driver that gets itsvirtual interfacethroughuvm_config_db::get()instead of a constructor argument. config_db::get()returns0on failure — check it and`uvm_fatalrather than silently continuing with an unassigned handle.my_driver::type_id::create("drv", this)builds child components insidebuild_phase, instead ofnew(...)— the factory mechanism behind why is chapter 6's material.uvm_config_db#(...)::set(...)(in the top-level module, beforerun_test()) publishes a value that any component's matchingget()can retrieve — the full mechanics are chapter 5's material.run_test("my_test")starts the phase engine and constructs exactly one instance of the named class as the root of the component tree.- Every UVM file starts with
`include "uvm_macros.svh"andimport uvm_pkg::*;.
In tb_top's initial block, why does uvm_config_db#(...)::set(...) have to happen before run_test(...)?
If uvm_config_db#(virtual mux2_if.tb_mp)::get(...) fails inside the driver's build_phase (returns 0) and the `uvm_fatal check were removed, what would actually happen?
Why does my_test construct the driver with my_driver::type_id::create('drv', this) instead of my_driver drv = new('drv', this)?