The subsystem bench, two DMAs in a chain, reuses every class of the block bench without a single edit. The one file it does not reuse is the top module: it defines DMA_NO_TOP, includes dma_tb.sv, and supplies its own. Forty lines decide whether a bench can be reused at the next level. What has to be in them, and what must never be?
SystemVerilog has two worlds. Modules, interfaces and wires are static: they exist from time 0 and are connected by elaboration. Classes are dynamic: they are created at run time and can only reach signals through a virtual interface, a handle to a static interface instance. The top module is the only place that lives in both worlds: it instantiates the static parts, then publishes handles to them in uvm_config_db and hands control to UVM with run_test.
Think of an airport jet bridge. The terminal (the testbench classes) and the aircraft (the DUT and its pins) are built separately and know nothing about each other. The bridge connects one door to one door. It does not fly the plane or check tickets; it only has to be in the right place before boarding starts.
// snippet: from the course DMA bench
module top;
import uvm_pkg::*;
import dma_pkg::*;
logic clk = 0;
always #5 clk = ~clk; // 100 MHz
rst_if rst (clk);
wire rst_n = rst.rst_n;
initial begin
rst.rst_n = 1'b0;
repeat (4) @(posedge clk);
rst.rst_n <= 1'b1;
end
apb_if apb (clk, rst_n);
axis_if s_ax (clk, rst_n);
axis_if m_ax (clk, rst_n);
irq_if irqi (clk);
// The output side accepts every beat unless a test adds a reactive slave
// (axis_sink_agent), whose driver then drives TREADY.
initial m_ax.tready = 1'b1;
dma_dut dut (
.clk, .rst_n,
.paddr(apb.paddr), .psel(apb.psel), .penable(apb.penable), .pwrite(apb.pwrite),
.pwdata(apb.pwdata), .prdata(apb.prdata), .pready(apb.pready), .pslverr(apb.pslverr),
.s_tdata(s_ax.tdata), .s_tvalid(s_ax.tvalid), .s_tlast(s_ax.tlast), .s_tready(s_ax.tready),
.m_tdata(m_ax.tdata), .m_tvalid(m_ax.tvalid), .m_tlast(m_ax.tlast), .m_tready(m_ax.tready),
.irq(irqi.irq)
);
initial begin
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.apb.*", "vif", apb);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.in_agt.*", "vif", s_ax);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.out_agt.*", "vif", m_ax);
uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top", "irq_vif", irqi);
uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top.env.irq.*", "vif", irqi);
uvm_config_db#(virtual rst_if)::set(null, "uvm_test_top*", "rst_vif", rst);
run_test("dma_smoke_test");
end
endmoduleThe clock is an ordinary always toggle. Reset is not an ordinary reg: it lives in a tiny interface, rst_if, and the top only drives it for the first four clocks. Because it is an interface, a handle to it can be published, and dma_reset_test can drive rst_n low in the middle of a packet. A reset generated by a fixed initial block in the top could never be repeated by a test. Reset on the fly builds on that.
Each bus is one interface instance, connected to the DUT port by port. The line initial m_ax.tready = 1'b1; makes the output side accept every beat by default. When dma_backpressure_test swaps in the reactive slave, the sink driver starts driving TREADY from the class side and takes over. The default belongs in the top because it is a property of the harness, not of any test.
`null` context means the path is absolute and the set has the highest precedence (1000), so no component can accidentally override a virtual interface. Every path therefore starts with uvm_test_top, the instance name run_test gives the test.
*`"uvm_test_top.env.apb."**: the agent's children, the driver and the monitor, are the ones that get the handle. A wildcard must match the getter's full name, so env.apb.* matches env.apb.drv but not env.apb` itself.
`"uvm_test_top"`, field `irq_vif`: an exact path, because only the test (in dma_base_test) needs the raw interrupt line.
*`"uvm_test_top", field rst_vif`: no dot before the star, so it matches the test itself and** everything under it. The reset test and the reset-aware scoreboard both fetch it.
The sets come first and run_test comes last, in one initial block. Components call get in build_phase, which runs inside run_test; putting both in one block guarantees the order. run_test here is the global task in uvm_pkg, a thin wrapper around uvm_root::get().run_test(). Its argument is only the default test, because +UVM_TESTNAME wins.
The whole top sits inside ` ifndef DMA_NO_TOP `. The subsystem bench defines the macro, includes the file for its package and interfaces, and writes a top with two DUTs that sets the same field names one level deeper, uvm_test_top.env.dma0.apb.*` and so on. Vertical Reuse shows that top in full.
No scenario decisions: no register values, no packet counts, no choice of test beyond the default. No #delay before run_test. No class logic beyond the config_db sets. A top that knows about the scenario cannot be shared by the next test, let alone by the next level of integration.
The first lines of every run come from run_test, before any component exists:
UVM_INFO @ 0: reporter [RNTST] Running test dma_smoke_test...
UVM_INFO uvm_root.svh(531) @ 0: reporter [UVMTOP] UVM testbench topology:
...
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatchedNo NOVIF fatal means every driver, monitor and the test found their handles: all the config_db paths matched. The scoreboard's full name, uvm_test_top.env.sb, shows the hierarchy the paths were written for.
A cleanup commit "tidied" the top's sets, removing what looked like redundant wildcards: "uvm_test_top.env.apb.*" became "uvm_test_top.env.apb". Every test died at time 0 with NOVIF from the APB agent's children (apb_if not set for this driver, or the monitor's version). The handle was set on the agent, and the agent never asks for it; the driver asks, under the name uvm_test_top.env.apb.drv, which the new path does not match.
The fix was to restore the wildcard. The lesson is a rule worth knowing by heart: a config_db path is matched against the full name of the component that calls get. When a get fails, print the getter's get_full_name() and compare it with the set path character by character; +UVM_CONFIG_DB_TRACE shows every set and get for one run.
initial block that starts with #1. What happens?run_test starts at time 0 and the build phase runs in zero time, so every get happens before the delayed sets. Each driver and monitor reports NOVIF at time 0. The sets must happen before run_test creates the components, which is why they share one block, ahead of the call.
rst_vif use "uvm_test_top*" while irq_vif uses the exact path "uvm_test_top"?Two different consumers. The interrupt line is fetched only by the test, so an exact path is enough and keeps the handle out of everyone else's reach. The reset interface is fetched by the reset test and by dma_scoreboard_ra at uvm_test_top.env.sb, so the pattern must match both the test and its descendants; with no dot before the star, it does.
Next: you now have every piece of a test. Designing a Test Library turns nine tests into a library that a team can grow for a year without copying.