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.

TestDriver it needsDriver it got after the edit
fifo_smokefifo_drivererr_driver
fifo_fill_drainfifo_drivererr_driver
fifo_wrap_stressfifo_drivererr_driver
fifo_bad_parityerr_drivererr_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 benchUVM classWhat changes
Transaction classuvm_sequence_itemRegistered with the factory; gains print, copy and compare hooks
Generatoruvm_sequence, run on a uvm_sequencerThe sequence makes items; the sequencer hands them to the driver one at a time
Mailbox to the driverseq_item_port connected to seq_item_exportThe driver asks for the next item and says when it is done
Driveruvm_driverSame job: turn an item into pin activity
Monitoruvm_monitor with a uvm_analysis_portPublishes each observed item to any number of listeners
Scoreboarduvm_scoreboard with a uvm_analysis_impReceives each item through a write() function
Environmentuvm_agent inside a uvm_envThe agent groups one interface's sequencer, driver and monitor; the env holds agents and checkers
Test programuvm_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.

The component tree of this lesson's example, printed by its test at the end of elaboration.
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 example's bus at each rising clock edge
012345validx00107fffy01118000monitor samples x=16, y=17x=255 wraps to y=0

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.

SeverityDefault actionWhat it means for the run
UVM_INFODisplayPrinted only if its verbosity passes the filter below
UVM_WARNINGDisplayAlways printed; not counted as a failure
UVM_ERRORDisplay and countThe run continues and the summary counts it; +UVM_MAX_QUIT_COUNT=n stops the run at the nth error
UVM_FATALDisplay and exitThe 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.

Each package imports the layer below itBoxes are packages, the interface or the top module. Arrows labelled imports are package imports; the others say what they mean. The .svh files listed in each box are included inside that package only.
Each package imports the layer below itBoxes are packages, the interface or the top module. Arrows labelled imports are package imports; the others say what they mean. The `.svh` files listed in each box are included inside that package only. library uvm_pkg The class library package incr_agent_pkg incr_item,incr_driver,incr_monitor,incr_agent package incr_env_pkg incr_scoreboard,incr_env package incr_test_pkg base_test and thetests derived from it module tb_top module Clock, interface,DUT, run_test interface incr_if Interfaces stayoutside packages importsimportsimportsimportsinstantiatesrefers to (virtualincr_if)
  1. 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.

  2. 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
    1. data uvm_driver, uvm_monitor, ...: uvm_pkg → incr_agent_pkg
    2. data type incr_if: incr_if → incr_agent_pkg
  3. Then the environment package

    The environment package imports the agent package, so incr_env can hold an incr_agent and the scoreboard can take incr_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
    1. data incr_item, incr_agent: incr_agent_pkg → incr_env_pkg
  4. 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_item must import the agent package as well; the environment's import does not reach it.

    Follow the calls, values and waits
    1. data incr_env: incr_env_pkg → incr_test_pkg
  5. 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_if handle.

    Follow the calls, values and waits
    1. data base_test and its tests: incr_test_pkg → tb_top module
    2. 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.

One package file per layer. Each class file is included here and nowhere else.
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"
endpackage

Including 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0

Check yourself

Predict the outputWhat line does the $display print? UVM's own messages are not part of the answer.
`include "uvm_macros.svh"
import uvm_pkg::*;
class mon_c extends uvm_monitor;
  `uvm_component_utils(mon_c)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclass
class agent_c extends uvm_agent;
  `uvm_component_utils(agent_c)
  mon_c mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    mon = mon_c::type_id::create("mon", this);
  endfunction
endclass
class env_c extends uvm_env;
  `uvm_component_utils(env_c)
  agent_c rx;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    rx = agent_c::type_id::create("rx", this);
  endfunction
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  env_c env;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env = env_c::type_id::create("env", this);
  endfunction
  function void end_of_elaboration_phase(uvm_phase phase);
    $display("%s", env.rx.mon.get_full_name());
  endfunction
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugThe test overrides base_driver with err_driver, then builds the agent. Each option is the agent's build_phase body. Which one still builds a plain base_driver?
Predict the outputBoth packages include the same incr_item.svh, which declares class incr_item extends uvm_sequence_item with uvm_object_utils(incr_item). What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
package agent_pkg;
  import uvm_pkg::*;
  `include "incr_item.svh"
endpackage
package test_pkg;
  import uvm_pkg::*;
  `include "incr_item.svh"   // the same file, pasted a second time
endpackage
module tb;
  import uvm_pkg::*;
  class empty_test extends uvm_test;
    `uvm_component_utils(empty_test)
    function new(string name, uvm_component parent); super.new(name, parent); endfunction
  endclass
  initial begin
    agent_pkg::incr_item a = new("a");
    test_pkg::incr_item  t;
    uvm_object o = a;
    $display("same name: %0b, cast ok: %0b", a.get_type_name() == "incr_item", $cast(t, o));
    run_test("empty_test");
  end
endmodule
Read included file: incr_item.svh
class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  rand bit [7:0] x;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass
ConceptWhich of these is a component in the UVM tree?
ConceptIn the example, why does the agent fetch its virtual interface from uvm_config_db instead of taking it as a constructor argument?
ConceptA regression script marks a run passing when the simulator exits with status 0. A run's summary ends with UVM_ERROR : 3 and UVM_FATAL : 0, and the simulator exits with status 0. No report action was changed. What is the verdict?

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_MEDIUM by default).