The test layer answers one question: what runs today, and with which knobs. A base test owns the environment handle, sets sane defaults, and defines the run skeleton that every derived test reuses.
The test class listing below is a fragment, so this course's checks do not run it. It builds the environment from the harness lesson, extended with a report and an error-rate knob.
// not-run: fragment: needs the env class and bus_if interface from the harness listing, and has no top module
class base_test;
env e;
virtual bus_if vif; // assigned by tb_top
// Default knobs: derived tests override them in configure()
int unsigned num_txns = 200;
int unsigned err_pct = 0;
virtual function void configure();
// base test keeps defaults
endfunction
virtual task run();
e = new();
configure(); // derived-class hook
e.vif = vif;
e.num_txns = num_txns;
e.build();
e.set_err_pct(err_pct); // defined on env, forwarded to gen
e.run(); // blocks until env done criteria
e.report();
endtask
endclassThe error-rate call goes to a real method. The environment defines set_err_pct and forwards it to the generator, so the call compiles and the knob has one owner.
Each derived test is a few lines long. That brevity proves the layering worked: all protocol and plumbing knowledge lives below the test layer.
The derived tests below are not run either, for the same reason: they extend the base test fragment above.
// not-run: fragment: extends base_test from the listing above, and has no top module
class smoke_test extends base_test;
virtual function void configure();
num_txns = 20; // fast bring-up: is anything alive?
endfunction
endclass
class stress_test extends base_test;
virtual function void configure();
num_txns = 50000; // volume: hit corner timing windows
endfunction
endclass
class error_test extends base_test;
virtual function void configure();
num_txns = 1000;
err_pct = 15; // 15 percent of txns stress error paths
endfunction
endclass
// Constraint-narrowed flavor: aim stimulus at one address window.
// Generator knobs are written between build() and run(), keeping
// every plumbing line of the base run. No undeclared blueprint.
class small_addr_test extends base_test;
virtual function void configure();
num_txns = 500;
endfunction
virtual task run();
e = new();
configure();
e.vif = vif;
e.num_txns = num_txns;
e.build();
e.gen.addr_lo = 16'h0000;
e.gen.addr_hi = 16'h00FF;
e.set_err_pct(err_pct);
e.run();
e.report();
endtask
endclassTest | Knobs | Question it answers |
|---|---|---|
smoke_test | 20 transactions, defaults | Is anything alive at all. |
stress_test | 50000 transactions | What breaks under volume and backpressure. |
error_test | 1000 transactions, 15 percent error rate | Do the DUT error paths behave. |
small_addr_test | 500 transactions, low address window | Does one feature corner hold up. |
run() and skips the base plumbing. What breaks?The environment is half built: handles stay null and wiring never happens. Override configure() for knobs, and override run() only to add knob writes between build() and run() while keeping every plumbing line.
Without the UVM factory, the workable pattern is a plusarg string with a case ladder in tb_top. The base-class handle holds any derived test, and the virtual run() dispatches to it.
// snippet
// Factory-lite: map a plusarg string to an object. The ladder is
// partial here: clock, reset, interface and DUT live in the
// harness listing.
module tb_top_sel;
// ... clock, reset, interface bif, DUT as before ...
base_test t; // base-class handle holds any derived test
string testname;
initial begin
if (!$value$plusargs("TESTNAME=%s", testname))
testname = "smoke_test";
case (testname)
"smoke_test": begin smoke_test st = new(); t = st; end
"stress_test": begin stress_test st = new(); t = st; end
"error_test": begin error_test et = new(); t = et; end
"small_addr_test": begin small_addr_test sa = new(); t = sa; end
default: begin
$display("TB_TOP: unknown TESTNAME, stopping");
$finish;
end
endcase
t.vif = bif;
wait (rst_n === 1);
t.run(); // virtual: dispatches to the derived test
$finish;
end
endmodule
// Run: <simulator> +TESTNAME=stress_testEnd of test needs both sides: stimulus exhausted AND the scoreboard drained, never just one.
A drain window of idle cycles after the last stimulus lets in-flight responses emerge.
The watchdog stays: a test that never meets its done criteria must die loudly, not hang the farm.
A runnable version of the same ideas: a numeric plusarg selects smoke, stress, or targeted stimulus, and the test ends only after stimulus, drain, and a full read-back.
`timescale 1ns/1ps
module sel_dut (
input logic clk,
input logic rst_n,
input logic valid,
input logic write,
input logic [3:0] addr,
input logic [31:0] wdata,
output logic ready,
output logic [31:0] rdata
);
logic [31:0] mem [0:15];
assign ready = 1'b1;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rdata <= 32'h0;
else if (valid && write) begin
mem[addr] <= wdata;
rdata <= 32'h0;
end
else if (valid && !write) rdata <= mem[addr];
end
endmodule
TESTID=%d while the reference ladder uses TESTNAME=%s?The string case in the ladder crashes Icarus 13; Verilator 5.048 runs it. The numeric select keeps the same idea, one handle serving many tests, in a block both tools run. The string ladder stays the pattern to copy for real flows.