UVM Basics

Chapter 10 of 10

Capstone: Assembling a Complete UVM Environment

A second test proving chapter 1's fourth promise -- testing a new scenario needs one new class and one factory override, not a rewrite -- switching tests from the command line with +UVM_TESTNAME, organizing the whole environment into a package, and what's next for the uvm-advanced and dv-methodology tracks.

Nine chapters built one running environment, one piece at a time: a transaction (chapter 4), a config_db-handed virtual interface (chapter 5), a factory-overridable driver (chapter 6), a sequence/sequencer pair (chapter 7), a monitor packaged into an agent (chapter 8), and a scoreboard packaged into an env (chapter 9). uvm_agent and uvm_env are already familiar — nothing in this chapter introduces a new UVM concept. What's left is exactly what a capstone should be: proving chapter 1's fourth promise with a second test, and organizing everything into something that looks like a real project.

What's already built

PieceChapterRole
mux_transaction4, extended in 8stimulus + observed output
config_db handoff3, 5gets vif from the top module into every component that needs it
my_driver4, 6, 7drives vif from items pulled off seq_item_port
my_monitor8samples vif, broadcasts a filled-in mux_transaction
my_sequence7produces two randomized items
my_agent8packages sequencer + driver + monitor, active/passive
my_scoreboard9checks each observed transaction, no state needed
my_env9packages agent + scoreboard, connects monitor to scoreboard
my_test3, simplified every chapter sincebuilds env, starts a sequence

A second sequence: exhaustive instead of random

mux2 has exactly 3 stimulus bits — small enough to test every combination directly, not just a couple of random ones:

class my_sequence_exhaustive extends my_sequence;
  `uvm_object_utils(my_sequence_exhaustive)
 
  function new(string name = "my_sequence_exhaustive");
    super.new(name);
  endfunction
 
  task body();
    for (int i = 0; i < 8; i++) begin
      req = mux_transaction::type_id::create("req");
      start_item(req);
      req.sel = i[0];
      req.a   = i[1];
      req.b   = i[2];
      finish_item(req);
    end
  endtask
endclass

Not every sequence needs randomize() — with only 8 possible input combinations, directly assigning each bit of the loop counter i to a field covers all of them exactly once, more precisely than random draws would. my_sequence_exhaustive extends my_sequence, not uvm_sequence #(mux_transaction) directly — that's not a stylistic choice, it's required for what comes next.

A second test — one new class, one new line

class exhaustive_test extends my_test;
  `uvm_component_utils(exhaustive_test)
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    my_sequence::type_id::set_type_override(my_sequence_exhaustive::get_type());
  endfunction
endclass

exhaustive_test doesn't override run_phase at all — it inherits my_test's exactly as chapter 9 wrote it, including the line seq = my_sequence::type_id::create("seq");. The one line exhaustive_test's own build_phase adds is chapter 6's factory override: from this point on, anything that asks the factory for a my_sequence gets a my_sequence_exhaustive instead. seq is still declared as my_sequence — which is exactly why my_sequence_exhaustive had to extend my_sequence and not just share its parent class: type_id::create()'s return value has to be assignment-compatible with how seq is declared. seq.start(sqr) then calls body() polymorphically (SV ch11) — and since my_sequence_exhaustive overrides body(), the exhaustive version runs, even though the calling code never changed.

This is chapter 1's fourth promise, concretely: testing a new scenario took one new sequence class and one new test class with a single line in it — my_test, my_env, my_agent, the driver, the monitor, and the scoreboard are all completely unmodified.

Switching tests without touching source: +UVM_TESTNAME

Change the top module's run_test("my_test") to a plain run_test(), with no argument:

`include "uvm_macros.svh"
import uvm_pkg::*;
import mux_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();
  end
endmodule

With no argument, run_test() reads which test to construct from the +UVM_TESTNAME command-line plusarg instead of a hardcoded string. On the simulator command line (or EDA Playground's run-options field): +UVM_TESTNAME=my_test runs the original two-item random test, +UVM_TESTNAME=exhaustive_test runs all eight combinations — same compiled testbench, same source files, different test picked at run time. That's the payoff chapter 1 named and this whole track has been building toward: testing a different scenario is a command-line flag, not an edit.

Organizing it all: one package

SV ch16 covered organizing code across files with package. A real UVM environment follows the same idea — one class per file, all pulled together into a single package the top module imports once:

package mux_uvm_pkg;
  import uvm_pkg::*;
  `include "uvm_macros.svh"
 
  `include "mux_transaction.sv"
  `include "my_sequence.sv"
  `include "my_sequence_exhaustive.sv"
  `include "my_driver.sv"
  `include "my_driver_alt.sv"
  `include "my_monitor.sv"
  `include "my_agent.sv"
  `include "my_scoreboard.sv"
  `include "my_env.sv"
  `include "my_test.sv"
  `include "exhaustive_test.sv"
endpackage

`include inside a package (rather than plain files compiled loose) is what makes every class in the list visible to every other one, in dependency order, and available to the top module through one import mux_uvm_pkg::*; — the same organizing principle SV ch16 taught, applied to an environment with more than one class in it for the first time.

What this track didn't cover

This environment checks correctness — it doesn't measure whether testing was thorough, and it only ever drove a single agent. Coordinating more than one agent (a real DUT usually has more than one interface), layering sequences on top of each other, and giving a testbench a shared register model to work against are all real problems a bigger environment runs into — and they're exactly what the next track, uvm-advanced, covers: virtual sequences, multi-agent coordination, and the UVM register abstraction layer (RAL). Nothing here tracks which of the 8 input combinations a given run actually exercised, whether both tests together cover everything worth covering, or what's still untested, either — measuring that, and the judgment calls around when there's been "enough" testing, is dv-methodology's job instead. Same handoff shape as chapter 1 of this track: everything here gets reused, nothing gets thrown away, and the next tracks standardize what's still being done ad hoc or not done at all.

Summary

  • my_sequence_exhaustive extends my_sequence (not uvm_sequence #(mux_transaction) directly) drives all 8 possible mux2 input combinations by direct assignment instead of randomize() — appropriate when the input space is small enough to cover exhaustively.
  • A factory override (chapter 6) set inside a new test's build_phasemy_sequence::type_id::set_type_override(my_sequence_exhaustive::get_type()) — swaps which sequence a completely unmodified, inherited run_phase ends up running.
  • run_test() with no argument reads which test to build from the +UVM_TESTNAME command-line plusarg, so switching tests is a run-time flag, not a source edit.
  • A real UVM environment's classes live one-per-file, `included into a single package the top module imports — the same organizing principle SV ch16 taught.
  • This track checked correctness with a single agent; it never coordinated more than one agent, layered sequences, or measured thoroughness. Multi-agent coordination and a shared register model are uvm-advanced's job; measuring thoroughness is dv-methodology's — the same way this whole track was UVM's answer to what SV Basics' capstone couldn't standardize on its own.

Why does my_sequence_exhaustive have to extend my_sequence specifically, rather than uvm_sequence #(mux_transaction) directly?

exhaustive_test doesn't override run_phase at all. How does it end up running eight transactions instead of my_test's original two?

What's the practical difference between run_test('my_test') and a bare run_test() combined with +UVM_TESTNAME=my_test on the command line?

What does this track's environment check, and what does it not measure -- which is exactly dv-methodology's focus?