One environment, a family of tests
Reuse one environment for smoke, boundary and error tests, and select the intended test and item before their work begins.
- Lesson
- U1.4
- Time
- 25 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
Your new boundary test finishes with a correct result, 10 → 11, but never tries ff → 00. It installs a boundary_item override after its sequence has finished. The shared check reports TEST_GOAL: the design worked, but the intended case never reached its pins.
boundary_test; the last three begin a separate late_boundary_test run.- Install the rulebuild: 0 ns (illustrative)
boundary_testselectsboundary_itembefore the sequence starts.More context
The override is installed in the test's
build_phase, beforesuper.build_phase()builds the environment. No item exists yet; the factory only stores the rule.Follow the calls, values and waits
- call override family_item → boundary_item: boundary_test.build → Factory
- Create the selected itemrun: 0 ns (illustrative)
The unchanged sequence requests
family_item. The factory returns aboundary_item; itsprepare()chooses ff.More context
The lookup happens inside this create call. The example prints
ITEM boundary_item x=ff parity=0: the class was chosen here, once, for the life of the object.Follow the calls, values and waits
- call create family_item: one_seq.body → Factory
- return boundary_item object: Factory → Created item
- Check the limit15 ns (illustrative)
The monitor observes x=ff, parity=0, y=00 and error=0. The DUT check and the boundary test’s goal both pass.
More context
The driver applied ff at the 10 ns falling edge; the monitor samples at the 15 ns rising edge and prints
15 ns CHECK x=ff parity=0 y=00 error=0.Follow the calls, values and waits
- data x=ff → y=00: Created item → Observed pins
- Start a fresh late runfault run: 0 ns (illustrative)
This separate run has no override yet. The sequence receives a
family_item;prepare()chooses 10.More context
late_boundary_testis a different simulation, not a continuation of the first three steps. Its override line comes aftersuper.exercise(), which is still running the sequence.Follow the calls, values and waits
- call create family_item: one_seq.body → Factory
- return family_item object: Factory → Created item
- Waiting: late test: after start — inside super.exercise() until 20 ns
- The wrong case still worksfault run: 15 ns (illustrative)
x=10 produces y=11 with error=0. That is a correct DUT result, but the test intended to exercise ff.
More context
The scoreboard's DUT check passes: 10 + 1 is 11. Only the test goal, which requires x=ff, can tell that the wrong case ran.
Follow the calls, values and waits
- data x=10 → y=11: Created item → Observed pins
- A later rule cannot change itfault run: 20 ns (illustrative)
start()returns at 20 ns, then the late test installs its override. The observed input remains 10, socheck_phasereports TEST_GOAL.More context
The factory now holds the same rule as in the first run, but no create call follows it. The existing
family_itemkeeps its class, and the run ends with 1 UVM_ERROR.Follow the calls, values and waits
- call override family_item → boundary_item: late test: after start → Factory
- Released: late test: after start
Read the diagram as text
- boundary_test.build: Install the item override
- one_seq.body: Asks for family_item
- late test: after start: Install the same rule after the work
- Factory: Looks up the rule at create()
- Created item: Its class is fixed at creation
- Observed pins: Monitor samples at 15 ns
- boundary_test.build → Factory: set override
- one_seq.body → Factory: create("item")
- Factory → Created item: new item
- Created item → Observed pins: prepare, drive, observe
- late test: after start → Factory: set override
Both runs use the same environment and sequence. Only the test’s override timing changes.
Give each test one intention
A smoke test checks a simple, ordinary case. A boundary test reaches a limit, such as eight-bit wraparound. An error test supplies an invalid input and checks the design’s specified response.
The incrementer now receives an even-parity bit. Correct parity equals the XOR of x’s eight bits. Wrong parity must produce error=1 and y=00; otherwise y is x+1 in eight bits.
| Test | Item selected | Observed input | Required result |
|---|---|---|---|
| smoke_test | family_item | x=10, parity=1 | y=11, error=0 |
| boundary_test | boundary_item | x=ff, parity=0 | y=00, error=0 |
| error_test | bad_parity_item | x=10, parity=0 | y=00, error=1 |
Hexadecimal data; parity and error are single bits. Each test sends one input.
boundary_test as uvm_test_top, which builds the shared environment.- Compile the family
Each derived class inherits the common setup and checks. Compiling three tests does not run all three.
More context
The extends arrows are class relations fixed at compile time. Nothing travels along them, and no test object exists yet.
- Select this run
The selected name is
boundary_test. The factory creates its instance asuvm_test_top.More context
No +UVM_TESTNAME argument is given, so the
run_testdefault decides. The example's first line isTEST boundary_test; with +UVM_TESTNAME=smoke_test the same compiled program printsTEST smoke_test.Follow the calls, values and waits
- call run_test("boundary_test"): boundary_test → uvm_test_top
- Reuse the environment
boundary_testcallssuper.build_phase(). The inherited method buildsenv, and its normal driver and checker remain in place.More context
Before that call,
boundary_testinstalls its item override. The environment it builds is the same onesmoke_testanderror_testwould build.Follow the calls, values and waits
- call create("env"): uvm_test_top → env
Read the diagram as text
- base_test: Build env; run; check the chosen case
- smoke_test: Ordinary x=10; no override
- boundary_test: boundary_item; require x=ff
- error_test: bad_parity_item; require rejection
- uvm_test_top: One boundary_test instance
- env: Same driver, monitor and checker
- smoke_test → base_test: extends
- boundary_test → base_test: extends
- error_test → base_test: extends
- boundary_test → uvm_test_top: run_test creates
- uvm_test_top → env: base build creates
The other test classes remain available for another simulation. They are not children of the selected test.
base_test holds the common setup, run and checks. Its derived tests change only their stimulus choice and the observation they require. A derived test is a subclass of base_test; it is still the one test instance named uvm_test_top.
class boundary_test extends base_test;
`uvm_component_utils(boundary_test)
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function void build_phase(uvm_phase phase);
family_item::type_id::set_type_override(boundary_item::get_type());
super.build_phase(phase); // keep the shared environment
endfunction
virtual function bit [7:0] goal_x(); return 8'hff; endfunction
endclassFor these fixed cases, each item’s virtual prepare() chooses its payload. The sequence asks for family_item and calls prepare() through that base-class handle. A boundary_item chooses ff; a bad_parity_item chooses 10 and flips its parity. In a real bench the derived item usually adds a constraint, such as constraint at_max { x == 8'hff; }, and the sequence randomizes it; prepare() keeps this example’s values fixed and visible.
Choose the test when the run starts
run_test("boundary_test");The name passed to run_test is the default. A simulator argument +UVM_TESTNAME=smoke_test takes precedence and selects smoke_test instead. All the family’s classes must already be compiled and registered; the argument chooses among them.
| Default in main.sv | Simulator argument | Test created |
|---|---|---|
| boundary_test | None | boundary_test |
| boundary_test | +UVM_TESTNAME=smoke_test | smoke_test |
| boundary_test | +UVM_TESTNAME=error_test | error_test |
One selected test per simulation. An unknown test name ends with INVTST; it does not fall back to the default.
Edit the test name in the Try it code to smoke_test, error_test or late_boundary_test to try those cases. With your own simulator, the argument lets a script choose a test without editing and recompiling the source.
If a command contains several test-name arguments
uvm-core 2020.3.1 uses the first +UVM_TESTNAME value and reports MULTTST as a warning. Give each simulation one test-name argument so the run command states its choice clearly.
Place the rule before the relevant create
A factory override is a rule for future create calls. It chooses the class to construct; it does not replace objects that already exist. The late test changes the rule after its sequence has sent the only item.
After the item has been used
virtual task exercise();
super.exercise(); // sequence creates and sends family_item
family_item::type_id::set_type_override(boundary_item::get_type());
endtask
late_boundary_test observes x=10. Its goal requires ff, so TEST_GOAL reports the missed case.
Before the sequence creates it
virtual function void build_phase(uvm_phase phase);
family_item::type_id::set_type_override(boundary_item::get_type());
super.build_phase(phase);
endfunction
boundary_test inherits the same run. Its later item creation returns boundary_item, which sends ff.
Find the actual create call before deciding that an override is too late. Here, base_test.build_phase creates env, while one_seq.body creates its item during the run. Returning from the base build has not used this item’s override yet.
An item override made during run_phase can also work if it precedes the item’s create. Even creating the sequence object first is fine: this sequence creates its item later, when body() runs. [U2.2](U2-2-uvm-factory-create.html) follows the factory’s lookup rules in detail. A type override changes every family_item in the run; to change one agent’s items only, use an instance override ([U11.3](U11-3-uvm-instance-override.html)).
Keep the common checks active
The environment checks the result from the observed input and parity. The test separately checks that exactly one observation matched its intended case. That second check catches the opening failure, where an ordinary input passed the DUT check.
// Shared base_test.check_phase
if (env.sb.checked != 1)
`uvm_error("COUNT","Expected one observed input")
else if (env.sb.last_x != goal_x() || env.sb.last_error != goal_error())
`uvm_error("TEST_GOAL","The intended case was not observed")error_test expects the design’s error output to be 1. That is a passing result when parity is wrong and y is 00. A UVM_ERROR means a bench check failed; an expected DUT error indication does not require suppressing those checks.
Selected settled milestones, not consecutive rising edges. Driver changes occur on falling edges; the monitor checks at 15 ns. The sequence returns at 20 ns.
To extend the family, add a near_max_test whose item sends fe and whose goal requires fe. It should observe y=ff with error=0 using the same environment. Decide where its override must run if the sequence later starts creating items in its constructor instead of body().
Try it
ITEM names the class returned to the sequence. CHECK comes from the monitor’s observation at 15 ns. DONE includes the checks run after stimulus ends, so it also catches an input that worked but missed the selected test’s goal.
Keep family_bench.svh unchanged and switch the name in main.sv. smoke_test observes 10 → 11; error_test observes bad parity and the required error=1. late_boundary_test reproduces TEST_GOAL with x=10 instead of ff.
TEST boundary_test ITEM boundary_item x=ff parity=0 15 ns CHECK x=ff parity=0 y=00 error=0 DONE boundary_test checked=1 errors=0 UVM_ERROR : 0Read included file: family_bench.svh
`include "uvm_macros.svh"
import uvm_pkg::*;
// An 8-bit incrementer with an even-parity input check.
// Bad parity produces error=1 and y=0; correct parity produces x+1 modulo 256.
interface family_if(input logic clk);
logic valid = 0;
logic [7:0] x = 0;
logic parity = 0;
logic [7:0] y;
logic error;
endinterface
module parity_incrementer(family_if bus);
assign bus.error = (bus.parity != ^bus.x);
assign bus.y = bus.error ? 8'h00 : bus.x + 8'd1;
endmodule
class family_item extends uvm_sequence_item;
`uvm_object_utils(family_item)
bit [7:0] x;
bit parity;
function new(string name="family_item"); super.new(name); endfunction
virtual function void prepare();
x = 8'h10;
parity = ^x;
endfunction
endclass
class boundary_item extends family_item;
`uvm_object_utils(boundary_item)
function new(string name="boundary_item"); super.new(name); endfunction
virtual function void prepare();
x = 8'hff;
parity = ^x;
endfunction
endclass
class bad_parity_item extends family_item;
`uvm_object_utils(bad_parity_item)
function new(string name="bad_parity_item"); super.new(name); endfunction
virtual function void prepare();
super.prepare();
parity = !parity;
endfunction
endclass
// The same sequence asks for the base item type in every test.
class one_seq extends uvm_sequence #(family_item);
`uvm_object_utils(one_seq)
function new(string name="one_seq"); super.new(name); endfunction
virtual task body();
family_item item = family_item::type_id::create("item");
start_item(item);
item.prepare();
`ifndef FAMILY_QUIET
$display("ITEM %s x=%02h parity=%0b", item.get_type_name(), item.x, item.parity);
`endif
finish_item(item);
endtask
endclass
// Observations have their own type, so a stimulus override does not change them.
class family_sample extends uvm_object;
`uvm_object_utils(family_sample)
bit [7:0] x,y;
bit parity,error;
function new(string name="family_sample"); super.new(name); endfunction
endclass
class family_driver extends uvm_driver #(family_item);
`uvm_component_utils(family_driver)
virtual family_if vif;
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
@(negedge vif.clk);
vif.x <= req.x;
vif.parity <= req.parity;
vif.valid <= 1;
@(negedge vif.clk);
vif.valid <= 0;
seq_item_port.item_done();
end
endtask
endclass
class family_monitor extends uvm_monitor;
`uvm_component_utils(family_monitor)
virtual family_if vif;
uvm_analysis_port #(family_sample) ap;
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function void build_phase(uvm_phase phase); ap=new("ap",this); endfunction
virtual task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
if (vif.valid) begin
family_sample seen=family_sample::type_id::create("seen");
seen.x=vif.x; seen.parity=vif.parity;
seen.y=vif.y; seen.error=vif.error;
ap.write(seen);
end
end
endtask
endclass
class family_scoreboard extends uvm_scoreboard;
`uvm_component_utils(family_scoreboard)
uvm_analysis_imp #(family_sample,family_scoreboard) imp;
int checked = 0;
bit [7:0] last_x;
bit last_error;
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function void build_phase(uvm_phase phase); imp=new("imp",this); endfunction
function void write(family_sample seen);
bit want_error = seen.parity != (^seen.x);
bit [7:0] want_y = want_error ? 8'h00 : seen.x + 8'd1;
checked++;
last_x=seen.x; last_error=seen.error;
if (seen.error != want_error || seen.y != want_y)
`uvm_error("DUT_RESULT", $sformatf("x=%02h: y=%02h error=%0b; expected y=%02h error=%0b",
seen.x,seen.y,seen.error,want_y,want_error))
`ifndef FAMILY_QUIET
$display("%0d ns CHECK x=%02h parity=%0b y=%02h error=%0b", $time,
seen.x,seen.parity,seen.y,seen.error);
`endif
endfunction
endclass
class family_env extends uvm_env;
`uvm_component_utils(family_env)
uvm_sequencer #(family_item) sqr;
family_driver drv;
family_monitor mon;
family_scoreboard sb;
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function void build_phase(uvm_phase phase);
virtual family_if vif;
if (!uvm_config_db #(virtual family_if)::get(this,"","vif",vif))
`uvm_fatal("NOVIF","No family interface")
sqr=uvm_sequencer #(family_item)::type_id::create("sqr",this);
drv=family_driver::type_id::create("drv",this);
mon=family_monitor::type_id::create("mon",this);
sb=family_scoreboard::type_id::create("sb",this);
drv.vif=vif; mon.vif=vif;
endfunction
virtual function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
mon.ap.connect(sb.imp);
endfunction
endclass
class base_test extends uvm_test;
`uvm_component_utils(base_test)
family_env env;
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
env=family_env::type_id::create("env",this);
endfunction
virtual function bit [7:0] goal_x(); return 8'h10; endfunction
virtual function bit goal_error(); return 0; endfunction
virtual task exercise();
one_seq seq=one_seq::type_id::create("seq");
seq.start(env.sqr);
endtask
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
`ifndef FAMILY_QUIET
$display("TEST %s",get_type_name());
`endif
exercise();
phase.drop_objection(this);
endtask
virtual function void check_phase(uvm_phase phase);
if (env.sb.checked != 1)
`uvm_error("COUNT",$sformatf("Expected one observed input, saw %0d",env.sb.checked))
else if (env.sb.last_x != goal_x() || env.sb.last_error != goal_error())
`uvm_error("TEST_GOAL",$sformatf("Expected x=%02h error=%0b; observed x=%02h error=%0b",
goal_x(),goal_error(),env.sb.last_x,env.sb.last_error))
endfunction
virtual function void report_phase(uvm_phase phase);
`ifndef FAMILY_QUIET
uvm_report_server reports=uvm_report_server::get_server();
$display("DONE %s checked=%0d errors=%0d",get_type_name(),env.sb.checked,
reports.get_severity_count(UVM_ERROR));
`endif
endfunction
endclass
class smoke_test extends base_test;
`uvm_component_utils(smoke_test)
function new(string name,uvm_component parent); super.new(name,parent); endfunction
endclass
class boundary_test extends base_test;
`uvm_component_utils(boundary_test)
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function void build_phase(uvm_phase phase);
family_item::type_id::set_type_override(boundary_item::get_type());
super.build_phase(phase);
endfunction
virtual function bit [7:0] goal_x(); return 8'hff; endfunction
endclass
class error_test extends base_test;
`uvm_component_utils(error_test)
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function void build_phase(uvm_phase phase);
family_item::type_id::set_type_override(bad_parity_item::get_type());
super.build_phase(phase);
endfunction
virtual function bit goal_error(); return 1; endfunction
endclass
// Select this test to reproduce the late-override failure.
class late_boundary_test extends base_test;
`uvm_component_utils(late_boundary_test)
function new(string name,uvm_component parent); super.new(name,parent); endfunction
virtual function bit [7:0] goal_x(); return 8'hff; endfunction
virtual task exercise();
super.exercise();
family_item::type_id::set_type_override(boundary_item::get_type());
endtask
endclass
Check yourself
Remember
- Put common environment setup, execution and checks in base_test; derived tests choose the scenario.
- +UVM_TESTNAME, when present, replaces the run_test argument; one simulation runs one selected test.
- An override affects future create calls. Check when the relevant component or item is actually created.
- A derived item must change what reaches the pins; keep the shared checks and verify that the intended case occurred.
- An expected DUT error indication can pass the test. A UVM_ERROR reports a failed bench check.
Practice
- The error test that never broke a checksum Fix · Easy