From a layered bench to UVM
Map the generator, driver, monitor, scoreboard and environment you already know onto UVM classes, and see what the library adds around them.
- Lesson
- U1.1
- Time
- 25 min
- Runs on
- Verilator with UVM
- Check
- 6 questions
A layered FIFO bench builds its driver inside the environment with drv = new(vif);. To test error handling, a colleague changes that line to build an error-injecting driver instead. That night every test in the regression runs the error driver, and dozens of clean tests fail with protocol errors nobody asked for.
| Test | Driver it needs | Driver it got after the edit |
|---|---|---|
fifo_smoke | fifo_driver | err_driver |
fifo_fill_drain | fifo_driver | err_driver |
fifo_wrap_stress | fifo_driver | err_driver |
fifo_bad_parity | err_driver | err_driver |
One edit to the environment changed every test, because the environment decided which driver class to build.
The jobs stay the same
In the SystemVerilog modules you split a testbench into jobs. A generator makes transactions, a driver turns them into pin activity, a monitor turns pin activity back into transactions, and a scoreboard checks them. An environment builds and connects those pieces, and a test decides what to run.
UVM, the Universal Verification Methodology, keeps every one of those jobs. It is a class library, standardised as IEEE 1800.2, that gives each job a base class. You extend the base class and fill in the same code you wrote before.
| Layered bench | UVM class | What changes |
|---|---|---|
| Transaction class | uvm_sequence_item | Registered with the factory; gains print, copy and compare hooks |
| Generator | uvm_sequence, run on a uvm_sequencer | The sequence makes items; the sequencer hands them to the driver one at a time |
| Mailbox to the driver | seq_item_port connected to seq_item_export | The driver asks for the next item and says when it is done |
| Driver | uvm_driver | Same job: turn an item into pin activity |
| Monitor | uvm_monitor with a uvm_analysis_port | Publishes each observed item to any number of listeners |
| Scoreboard | uvm_scoreboard with a uvm_analysis_imp | Receives each item through a write() function |
| Environment | uvm_agent inside a uvm_env | The agent groups one interface's sequencer, driver and monitor; the env holds agents and checkers |
| Test program | uvm_test, started by run_test("name") | Chosen by class name when the simulation starts |
Where each piece of the layered bench lands in UVM.
The biggest change is the generator. A UVM sequence is an object that produces items, and a sequencer is the component that passes them to the driver. Splitting the two lets several sequences take turns on one driver, which later lessons use.
Components form a tree
Every UVM component is created with two arguments: a name and a parent. The parent is the component that builds it, so it passes this. Names and parents link the components into a tree.
run_test("incr_test") creates the test, names it uvm_test_top, and places it under the library's hidden top component, uvm_root. uvm_root has an empty name, so every full name starts at uvm_test_top, and the null in the example's uvm_config_db set refers to it. Every other component hangs below the test, and its full name is its path from there. The example's monitor is uvm_test_top.env.agent.mon.
uvm_test_top (incr_test)
env (incr_env)
agent (incr_agent)
drv (incr_driver)
mon (incr_monitor)
sqr (uvm_sequencer)
sb (incr_scoreboard)Components exist for the whole simulation. Items and sequences are different: they are objects derived from uvm_object. You create them while the test runs and drop them when you are done, and they have no parent component, so they never appear in the tree.
The full name matters more than it looks. Configuration and factory overrides can target one path, such as uvm_test_top.env.agent.drv, and every report message prints the path of the component that sent it.
How a run starts
The top module is still an ordinary SystemVerilog module. It makes the clock, instantiates the interface and the design, and then hands control to UVM in an initial block.
In the example, that block does two things. It stores the virtual interface in the config database, then calls run_test("incr_test"). The factory creates an incr_test object as uvm_test_top, and the phases begin. From there, the test builds the environment, the environment builds the agent and scoreboard, and the agent builds its sequencer, driver and monitor.
Nothing in the top module names the driver or the scoreboard. It knows only the test, and even the test name could come from the command line instead, through +UVM_TESTNAME. That is what lets one compiled bench run many different tests.
Where the classes meet the pins
Only two classes touch the design's signals: the driver and the monitor. Both reach the pins through the virtual interface. Everything above them, from sequence to scoreboard, works with items.
The driver changes valid and x on falling edges; the monitor samples on rising edges where valid is 1.
The driver changes valid and x on the falling clock edge, and the monitor samples on the rising edge. Half a cycle apart, the two never race. The monitor publishes three items, and the scoreboard checks that each y equals x + 1 in eight bits, so 255 must give 0.
What UVM adds around the jobs
The factory. The example never calls new for a component. It calls incr_driver::type_id::create("drv", this), which asks the factory for an object of that type. A test can register an override first, saying "whenever anyone asks for fifo_driver, build err_driver". The environment code does not change, and only the error test sees the error driver. That is the fix for the opening story.
Configuration. uvm_config_db is a lookup table keyed by component path and field name. In the example, the top module stores the virtual interface for the path uvm_test_top.env.agent, and the agent fetches it in its build_phase. In the layered bench you passed the interface through every constructor instead.
Phasing. A layered bench usually runs from one initial block that calls build, connect and run in turn. UVM gives every component the same named steps, called phases, such as build_phase, connect_phase, run_phase and report_phase. The library calls each phase on every component in a fixed order, so no one writes that sequencing code by hand.
Reporting. The report macros uvm_info, uvm_warning, uvm_error and uvm_fatal, each written with a leading backtick, print a message tagged with its severity, source line, time, component path and an ID. The library counts the messages and prints a summary at the end. The next section shows how to read them.
Transaction-level ports. Components talk through ports instead of handles to each other. A monitor calls ap.write(item) on its analysis port, and every connected subscriber gets the item. With nothing connected, the call simply returns, so the monitor works the same with zero, one or five listeners.
Reports, verbosity and the verdict
Every later lesson prints reports, so learn to read one now. Each report has a severity, and each severity has a default action: what the library does with it.
| Severity | Default action | What it means for the run |
|---|---|---|
UVM_INFO | Display | Printed only if its verbosity passes the filter below |
UVM_WARNING | Display | Always printed; not counted as a failure |
UVM_ERROR | Display and count | The run continues and the summary counts it; +UVM_MAX_QUIT_COUNT=n stops the run at the nth error |
UVM_FATAL | Display and exit | The simulation stops at once |
Defaults from the report handler. A test can change an action, for example to stop on the first error.
An info report also carries a verbosity: UVM_NONE (0), UVM_LOW (100), UVM_MEDIUM (200), UVM_HIGH (300), UVM_FULL (400) or UVM_DEBUG (500). A component prints the report only when that number is at or below its verbosity level, which is UVM_MEDIUM by default. So UVM_LOW means "show this nearly always", and UVM_HIGH means "only when I ask for detail".
You ask for detail when the run starts. +UVM_VERBOSITY=UVM_HIGH raises the level of every component to 300, without recompiling. The Try it run below passes this argument, so the driver's UVM_HIGH reports appear; without it, the same program prints only the UVM_LOW and UVM_NONE reports.
UVM itself never prints PASS or FAIL. It counts reports, and the summary at the end lists the count per severity. By convention a test fails when UVM_ERROR or UVM_FATAL is above zero. Measured: a test with three errors runs to the end, prints UVM_ERROR : 3, and the simulator still exits with status 0. A regression script must read the summary; the exit status alone hides the failure.
Zero errors is not the same as a passing test. A test that checked nothing also reports zero errors, as [U1.3](U1-3-uvm-objection-lifetime.html) shows. A good test counts what it checked and reports an error when that count is wrong.
How a UVM project is filed
The example keeps every class in one file so you can read it top to bottom. A real project splits it: one class per .svh file, and one package per agent, per environment and per test library. A package is a SystemVerilog namespace that is compiled once and then imported.
.svh files listed in each box are included inside that package only.- The library and the interface first
uvm_pkg and the interface compile first. Neither depends on anything in this project.
More context
Nothing moves yet: these two only provide names. Every arrow in the figure points at what a box depends on.
- Then the agent package
The agent package imports uvm_pkg, and its driver and monitor declare
virtual incr_if. A package can import only what has already been compiled.More context
The arrows point from the importer to what it needs. The names travel the other way, from the compiled code into the package that uses them.
Follow the calls, values and waits
- data uvm_driver, uvm_monitor, ...: uvm_pkg → incr_agent_pkg
- data type incr_if: incr_if → incr_agent_pkg
- Then the environment package
The environment package imports the agent package, so
incr_envcan hold anincr_agentand the scoreboard can takeincr_item.More context
There is exactly one compiled
incr_item, inside the agent package, so the driver, the monitor and the scoreboard all name the same type.Follow the calls, values and waits
- data incr_item, incr_agent: incr_agent_pkg → incr_env_pkg
- Then the test package
The test package imports the environment package, and each test builds an
incr_env.More context
An import is not passed on. A test that names
incr_itemmust import the agent package as well; the environment's import does not reach it.Follow the calls, values and waits
- data incr_env: incr_env_pkg → incr_test_pkg
- The top module comes last
It instantiates the interface and the design, imports the test package and calls run_test. It never names a driver or a scoreboard.
More context
An interface cannot be declared inside a package, so the top module owns the one real instance. The agent reaches it later through a
virtual incr_ifhandle.Follow the calls, values and waits
- data base_test and its tests: incr_test_pkg → tb_top module
- data one incr_if instance: tb_top module → incr_if
Read the diagram as text
- uvm_pkg: The class library
- incr_agent_pkg: incr_item, incr_driver, incr_monitor, incr_agent
- incr_env_pkg: incr_scoreboard, incr_env
- incr_test_pkg: base_test and the tests derived from it
- tb_top module: Clock, interface, DUT, run_test
- incr_if: Interfaces stay outside packages
- incr_agent_pkg → uvm_pkg: imports
- incr_env_pkg → incr_agent_pkg: imports
- incr_test_pkg → incr_env_pkg: imports
- tb_top module → incr_test_pkg: imports
- tb_top module → incr_if: instantiates
- incr_agent_pkg → incr_if: refers to (virtual incr_if)
To reuse the agent, import one package and compile its interface. Nothing in it depends on this environment.
package incr_agent_pkg;
import uvm_pkg::*;
`include "uvm_macros.svh"
`include "incr_item.svh"
`include "incr_driver.svh"
`include "incr_monitor.svh"
`include "incr_agent.svh"
endpackageIncluding a class file is a text paste. Paste the same incr_item.svh into two packages and you get two classes with one name. They are different types, so a $cast from one to the other fails. A sequence compiled in the test package then makes items the agent's driver cannot accept, and both print as incr_item in every message.
Keep each class in exactly one package, and reach it from elsewhere through import. An import is not passed on: a test package that names incr_item must import the agent package itself, even though the environment package already did. Tests then live in their own package, and [U1.4](U1-4-uvm-test-family.html) shows how a family of them shares one environment.
Try it
This is the complete layered bench in UVM: an item, a sequence, a driver, a monitor, an agent, a scoreboard, an environment and a test. The test prints the component tree, and the scoreboard reports each check with the uvm_info macro.
The Run button starts this simulation with +UVM_VERBOSITY=UVM_HIGH, so the driver's UVM_HIGH DRIVE reports appear between the checks. Without that argument they would be filtered out. Change UVM_HIGH in the driver to UVM_FULL and run again: the DRIVE lines disappear, because 400 is above the level of 300.
Read one scoreboard line from left to right: severity, source line, time in ns, the full name of the reporting component, the ID in brackets, then the message. Change the design to assign bus.y = bus.x + 2; and run again: you should see three UVM_ERROR lines and a summary count of 3.
uvm_test_top (incr_test) env (incr_env) agent (incr_agent) drv (incr_driver) mon (incr_monitor) sqr (uvm_sequencer) sb (incr_scoreboard) UVM_INFO main.sv(52) @ 10: uvm_test_top.env.agent.drv [DRIVE] x=16 UVM_INFO main.sv(120) @ 15: uvm_test_top.env.sb [MATCH] x=16 y=17 UVM_INFO main.sv(52) @ 30: uvm_test_top.env.agent.drv [DRIVE] x=127 UVM_INFO main.sv(120) @ 35: uvm_test_top.env.sb [MATCH] x=127 y=128 UVM_INFO main.sv(52) @ 50: uvm_test_top.env.agent.drv [DRIVE] x=255 UVM_INFO main.sv(120) @ 55: uvm_test_top.env.sb [MATCH] x=255 y=0 UVM_INFO main.sv(123) @ 60: uvm_test_top.env.sb [SUMMARY] 3 checked, 0 errors UVM_ERROR : 0Check yourself
Remember
- UVM keeps the layered bench's jobs: a sequence makes items, a driver drives them, a monitor observes and a scoreboard checks.
- Components form a tree of names and parents rooted at
uvm_test_top; items and sequences are objects outside the tree. - Create components with
type_id::create(name, this), so a test can swap a class through the factory without editing the environment. - The config database delivers per-instance settings, such as virtual interfaces, by component path.
- UVM counts reports and never prints PASS: read the error and fatal counts in the summary, since the simulator still exits 0. An info report prints only when its verbosity is at or below the component's level (
UVM_MEDIUMby default).