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
| Piece | Chapter | Role |
|---|---|---|
mux_transaction | 4, extended in 8 | stimulus + observed output |
config_db handoff | 3, 5 | gets vif from the top module into every component that needs it |
my_driver | 4, 6, 7 | drives vif from items pulled off seq_item_port |
my_monitor | 8 | samples vif, broadcasts a filled-in mux_transaction |
my_sequence | 7 | produces two randomized items |
my_agent | 8 | packages sequencer + driver + monitor, active/passive |
my_scoreboard | 9 | checks each observed transaction, no state needed |
my_env | 9 | packages agent + scoreboard, connects monitor to scoreboard |
my_test | 3, simplified every chapter since | builds 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
endclassNot 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
endclassexhaustive_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
endmoduleWith 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(notuvm_sequence #(mux_transaction)directly) drives all 8 possiblemux2input combinations by direct assignment instead ofrandomize()— appropriate when the input space is small enough to cover exhaustively.- A factory override (chapter 6) set inside a new test's
build_phase—my_sequence::type_id::set_type_override(my_sequence_exhaustive::get_type())— swaps which sequence a completely unmodified, inheritedrun_phaseends up running. run_test()with no argument reads which test to build from the+UVM_TESTNAMEcommand-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 singlepackagethe 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 isdv-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?