UVM Tutorial: Structure, Hierarchy and First Steps
If you have written a few SystemVerilog testbenches and found that every new project means re-inventing drivers, checkers and stimulus from scratch, this uvm tutorial is for you. The Universal Verification Methodology (UVM) is the industry-standard way to build verification environments that are reusable, scalable and readable by any engineer who knows the framework. This guide explains what UVM is, why it exists on top of plain SystemVerilog, how its testbench hierarchy fits together, and a practical roadmap to learn it.
A quick, honest note before we start: the UVM examples below are written for a UVM-capable simulator such as industry-standard simulation tools or industry-standard simulation tools (or the free EDA Playground, which hosts them online). They depend on the UVM base-class library, so they will not compile on free tools like Icarus Verilog or Verilator. Treat them as canonical structure you read and adapt, not as code to run locally as-is. The single plain-SystemVerilog example near the end is self-contained and runs anywhere.
What is UVM?
UVM is a class library plus a set of conventions, written in SystemVerilog and standardised as IEEE 1800.2. It gives you ready-made base classes — uvm_component, uvm_object, uvm_driver, uvm_monitor and more — together with rules for how objects are built, connected, run and reported. You do not verify a chip “in UVM” the way you code in C; instead you extend these base classes to model your own design’s stimulus and checking.
Three ideas sit at its core: components (long-lived structural blocks such as drivers and monitors), transactions (short-lived data objects describing one unit of activity, like a single bus read), and sequences (the programs that generate streams of transactions). Everything else is machinery to connect these cleanly.
Why UVM over plain SystemVerilog testbenches?
Plain SystemVerilog is perfectly capable of verifying a small block. The trouble appears as designs and teams grow:
- Reuse: a hand-rolled testbench for a FIFO rarely transplants to the next project. UVM components are parameterised and factory-created, so an agent written for an SPI master can be reused across several SoCs.
- Separation of concerns: UVM pushes you to keep stimulus (sequences) apart from testbench structure (components). You can run a hundred different tests against the same environment by swapping sequences, without touching the wiring.
- Shared vocabulary: every verification engineer recognises a “driver”, “sequencer” and “scoreboard”. A new teammate reads an unfamiliar UVM environment far faster than a bespoke one.
- Built-in services: a configuration database, severity-based reporting, ordered phasing, and a factory for run-time type overrides all come for free.
The cost is a learning curve and some boilerplate. For a one-off college assignment, plain SystemVerilog may still be the right tool; for anything you expect to maintain or reuse, UVM pays back quickly.
The UVM testbench hierarchy
A UVM environment is a tree of components. From top to bottom the usual chain is test → env → agent → driver / monitor / sequencer. Understanding who owns whom is the single most important thing in this uvm tutorial.
| Component | Role |
|---|---|
uvm_test | Top-level controller; chooses the configuration and the sequence to run. |
uvm_env | Container that groups one or more agents plus the scoreboard. |
uvm_agent | Bundles the components for one interface or protocol. |
uvm_sequencer | Arbitrates and forwards transactions from sequences to the driver. |
uvm_driver | Converts transactions into pin-level activity on the DUT interface. |
uvm_monitor | Observes DUT pins and reconstructs transactions for checking. |
uvm_scoreboard | Compares observed results against expected behaviour. |
Data flows like this: a sequence runs on the sequencer and produces transactions; the sequencer hands each one to the driver, which wiggles the DUT pins through a virtual interface; the monitor independently watches those same pins and sends reconstructed transactions to the scoreboard for checking. The driver never talks to the monitor — that independence is exactly what makes the check trustworthy.
The transaction
Start at the bottom: the data object that flows through the environment. It extends uvm_sequence_item and declares the fields one operation needs.
class alu_item extends uvm_sequence_item;
rand bit [7:0] a, b;
rand bit [2:0] op;
bit [8:0] result;
`uvm_object_utils_begin(alu_item)
`uvm_field_int(a, UVM_DEFAULT)
`uvm_field_int(b, UVM_DEFAULT)
`uvm_field_int(op, UVM_DEFAULT)
`uvm_object_utils_end
function new(string name = "alu_item");
super.new(name);
endfunction
endclassThe driver
The driver pulls a transaction from the sequencer and converts it into signal activity on a virtual interface. Note how it fetches that interface from the config database during build_phase.
class alu_driver extends uvm_driver #(alu_item);
`uvm_component_utils(alu_driver)
virtual alu_if 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 alu_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "virtual interface not set for driver")
endfunction
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
vif.a <= req.a;
vif.b <= req.b;
vif.op <= req.op;
@(posedge vif.clk);
seq_item_port.item_done();
end
endtask
endclassAgent, env and test
The agent groups the driver, monitor and sequencer for one interface and connects them. The env holds agents plus the scoreboard, and the test sits on top. Every child is created with type_id::create — the factory call that lets you override types later without editing this code.
class alu_agent extends uvm_agent;
`uvm_component_utils(alu_agent)
alu_driver drv;
alu_monitor mon;
alu_sequencer seqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = alu_driver::type_id::create("drv", this);
mon = alu_monitor::type_id::create("mon", this);
seqr = alu_sequencer::type_id::create("seqr", this);
endfunction
function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(seqr.seq_item_export);
endfunction
endclass
class alu_env extends uvm_env;
`uvm_component_utils(alu_env)
alu_agent agt;
alu_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt = alu_agent::type_id::create("agt", this);
sb = alu_scoreboard::type_id::create("sb", this);
endfunction
endclass
class alu_base_test extends uvm_test;
`uvm_component_utils(alu_base_test)
alu_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 = alu_env::type_id::create("env", this);
endfunction
endclassA runnable illustration: UVM phasing
One concept you can feel without the UVM library is phasing. UVM runs every component through the same ordered phases — build, then connect, then run, then report — and each phase completes everywhere before the next begins. The plain-SystemVerilog sketch below imitates that ordering with simple tasks, so it compiles and runs on any simulator and prints the sequence. It uses no UVM classes at all.
module tb;
// Illustrates UVM's phased execution: phases run in a fixed order,
// and each one finishes before the next one starts.
task automatic build_phase;
$display("[build] construct components, read configuration");
endtask
task automatic connect_phase;
$display("[connect] wire driver/monitor to sequencer and scoreboard");
endtask
task automatic run_phase;
$display("[run] drive stimulus and sample the DUT");
endtask
task automatic report_phase;
$display("[report] print the pass/fail summary");
endtask
initial begin
build_phase;
connect_phase;
run_phase;
report_phase;
$display("All phases complete.");
$finish;
end
endmodule[build] construct components, read configuration [connect] wire driver/monitor to sequencer and scoreboard [run] drive stimulus and sample the DUT [report] print the pass/fail summary All phases complete. /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpltit571l/tb.sv:27: $finish called at 0 (1s)
Real UVM phasing adds much more — run_phase is time-consuming and uses objections to decide when the test ends — but the ordering principle is exactly what you see above.
A roadmap to learn UVM
- Solidify SystemVerilog OOP — classes, inheritance, virtual methods, parameterised classes and interfaces. UVM is only as clear as your grasp of these.
- Build one agent by hand — transaction, sequencer, driver and monitor for a simple interface such as an ALU or a counter. Resist copy-paste; type it out.
- Add the env and scoreboard — connect the monitor to a scoreboard through analysis ports and get a self-checking test passing.
- Learn the factory and config_db — use
type_id::createeverywhere, pass the virtual interface throughuvm_config_db, then practise overriding one component type from the test. - Write layered sequences — directed first, then randomised, then constrained-random with functional coverage. This is where real verification value lives.
- Study phasing and objections — understand how
run_phaseends via raised and dropped objections, and when to use the other phases.
Work through these on a real DUT, however small, and read other engineers’ environments as you go. UVM rewards hands-on repetition far more than passive reading.
Quick check
Why use UVM instead of a plain SystemVerilog testbench?
For reuse of parameterised, factory-built components; separation of stimulus (sequences) from testbench structure (components); a shared vocabulary every engineer recognises; and built-in services such as the config database, reporting, phasing and the factory.
Which component turns transactions into pin activity, and which reconstructs transactions from pins?
The driver converts transactions into pin-level signals on the DUT interface. The monitor independently observes those pins and reconstructs transactions, sending them to the scoreboard to check.
What is the ordering of the UVM phases shown in the runnable example?
Build, then connect, then run, then report. Each phase completes across all components before the next phase begins.
Why doesn’t the driver communicate with the monitor directly?
Keeping them independent means the monitor observes the DUT without being influenced by what the driver intended, so the scoreboard’s comparison is a genuine, trustworthy check rather than a self-fulfilling one.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses