One vif binds one driver easily. A real hierarchy relays each vif through layers that never use it, down to the leaves that do. This lesson borrows the names of those layers from the Testbench part: an agent groups the driver and monitor for one interface, and an env holds the agents.
Constructor chaining passes every vif as an argument at every level. The dependency is visible, but each new interface edits every relay layer.
interface bus_if (input logic clk);
logic [31:0] addr;
logic valid;
endinterface
class driver;
virtual bus_if vif;
function new(virtual bus_if vif);
this.vif = vif;
endfunction
endclass
class monitor;
virtual bus_if vif;
function new(virtual bus_if vif);
this.vif = vif;
endfunction
endclass
class agent;
driver drv;
monitor mon;
function new(virtual bus_if vif);
drv = new(vif); // relay to the leaves
mon = new(vif);
endfunction
endclass
class env;
agent agt;
function new(virtual bus_if vif); // env never USES vif: it only relays it
agt = new(vif);
endfunction
endclass
module tb_chain;
logic clk = 0;
always #5 clk = ~clk;
bus_if bif (clk);
initial begin
env e;
e = new(bif); // one argument per vif, at every level
e.agt.drv.vif.addr = 32'h40; // the leaf reaches the real pins
#1;
if (bif.addr == 32'h40 && e.agt.mon.vif.addr == 32'h40)
$display("PASS: the vif relayed through env and agent reached both leaves");
else $fatal(1, "FAIL: relay lost the vif");
$finish;
end
endmodulePASS: the vif relayed through env and agent reached both leavesArity is compile-checked, but passing the wrong instance still compiles. Chaining guarantees presence, never correctness.
A central config class gathers every vif and knob into one handle. Relay layers pass that handle unchanged, so new interfaces touch only the config and the leaf.
Sharing one handle matters. The top fills it once, and every component observes the same vifs.
The config records each vif through a setter, so check() can prove every one was filled before anything is built:
interface bus_if (input logic clk);
logic [31:0] addr;
logic valid;
endinterface
interface irq_if (input logic clk);
logic irq;
endinterface
class tb_config;
virtual bus_if bus_vif; // ALL vifs for the whole bench live here
virtual irq_if irq_vif;
bit is_active = 1;
local bit bus_set, irq_set;
function void set_bus(virtual bus_if v);
bus_vif = v;
bus_set = 1;
endfunction
function void set_irq(virtual irq_if v);
irq_vif = v;
irq_set = 1;
endfunction
function void check(); // one clear failure at the top, before anything is built
if (!bus_set) $fatal(1, "tb_config: bus_vif unset");
if (!irq_set) $fatal(1, "tb_config: irq_vif unset");
endfunction
endclass
class driver;
virtual bus_if vif;
function new(virtual bus_if vif);
this.vif = vif;
endfunction
endclass
class agent;
driver drv;
function new(tb_config cfg);
if (cfg.is_active) drv = new(cfg.bus_vif); // the leaf picks what it needs
endfunction
endclass
class env;
agent agt;
function new(tb_config cfg); // ONE argument, forever:
agt = new(cfg); // adding vifs never changes this line
endfunction
endclass
module tb_cfg;
logic clk = 0;
always #5 clk = ~clk;
bus_if bif (clk);
irq_if iif (clk);
initial begin
tb_config cfg; // declare only: an initializer here would run at time 0
env e;
cfg = new();
cfg.set_bus(bif); // fill once at the top
cfg.set_irq(iif);
cfg.check(); // guard BEFORE building
e = new(cfg); // built only after the guard passed
e.agt.drv.vif.addr = 32'h80;
#1;
if (bif.addr == 32'h80) $display("PASS: one config handle carried both vifs; the driver reached bus_if through it");
else $fatal(1, "FAIL: config did not reach the driver");
$finish;
end
endmodulePASS: one config handle carried both vifs; the driver reached bus_if through itcheck() runs before building anything. One clear fatal at the top beats a null-vif crash deep in a run.
UVM's config database is the same distribution with the plumbing removed. The top publishes each vif under a string key, and each leaf looks it up by scope, with no relay layer involved.
String keys are checked at run time, not compile time. Every get() needs its return-value guard: the null-vif check reborn.
UVM is outside this course's checks, so the sketch below is partial and not run here. It shows the two calls and the mandatory guard.
// snippet
// tb_top: publish the vif under a string key and a scope pattern
uvm_config_db#(virtual bus_if)::set(null, "uvm_test_top.env.agt.*", "vif", bif);
// agent leaf, build phase: look it up and never ignore the return value
if (!uvm_config_db#(virtual bus_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "config_db get failed")The central config object. Relay layers carry the one handle, so only the config class and the leaf that uses the new vif change.
config_db::get() fails and the sim later fatals on a null vif. Where should it have failed?At the get() call itself. Ignoring the return value turns a lookup miss into a distant null-vif crash, so fatal there with the key in the message.
Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.
The config_db lines are UVM and are shown as a fragment; UVM is outside this course's checks.