Part 2 · OOP for Verification · Intermediate
vif Configuration Patterns
Passing vifs down a hand-built env via constructors vs config objects, a central config class, and what UVM config_db does conceptually.
The distribution problem
Binding one vif into one driver is easy. A real environment has a hierarchy — env contains agents, agents contain driver and monitor — and the vif must travel from tb_top through every layer to the leaves that actually touch pins. The middle layers (env, agent) do not use the vif themselves; they only relay it. How you relay it determines how painful it is to add the next interface.
Pattern 1: constructor chaining
Each layer takes the vif as a constructor argument and passes it down. Explicit and impossible to forget — the code does not compile with a missing argument. The cost: every intermediate constructor signature mentions every vif, so adding one interface to a 3-level hierarchy edits every level even though only the leaf cares.
class agent;
driver drv;
monitor mon;
function new(virtual bus_if vif);
drv = new(vif); // relay to leaves
mon = new(vif);
endfunction
endclass
class env;
agent agt;
function new(virtual bus_if vif); // env never USES vif —
agt = new(vif); // it only relays it
endfunction
endclass
module tb_top;
bus_if bif (clk);
initial begin
env e = new(bif); // one argument per vif, every level
end
endmodulePattern 2: a central config class
Scale fix: gather all vifs (and knobs like agent active/passive) into one configuration class, and pass a single config handle down the hierarchy. Adding an interface now touches the config class and the leaf that uses it — the relay layers are untouched, because they pass the same one handle regardless of what is inside it. Note the reference semantics doing the work: every component holds a handle to the same config object, so tb_top fills it once and everyone sees the same vifs.
class tb_config;
// ALL vifs for the whole bench live here
virtual bus_if bus_vif;
virtual irq_if irq_vif;
// plus knobs:
bit is_active = 1;
function void check();
if (bus_vif == null) $fatal(1, "tb_config: bus_vif unset");
if (irq_vif == null) $fatal(1, "tb_config: irq_vif unset");
endfunction
endclass
class agent;
driver drv;
function new(tb_config cfg);
if (cfg.is_active) drv = new(cfg.bus_vif); // 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_top;
bus_if bif (clk);
irq_if iif (clk);
initial begin
tb_config cfg = new();
cfg.bus_vif = bif; // fill once at the top
cfg.irq_vif = iif;
cfg.check(); // guard BEFORE building
begin
env e = new(cfg);
end
end
endmoduleCONSTRUCTOR CHAINING CENTRAL CONFIG OBJECT
tb_top ─(vif1,vif2,...)─► env tb_top ──(cfg)──► env
│ every level fill cfg │ one handle,
▼ repeats the list once ▼ every level
agent ─(vif1,vif2)─► agent ──(cfg)──►
│ │
▼ ▼
driver(vif1) monitor(vif2) drv=new(cfg.bus_vif) ...
add a vif: edit EVERY level add a vif: edit cfg + leaf onlyWhat UVM config_db does conceptually
UVM's uvm_config_db#(virtual bus_if)::set/get is the same idea with the plumbing removed: a global, hierarchy-aware lookup table . The top module set()s the vif under a string key with a scope pattern ("uvm_test_top.env.agt.*"); the leaf get()s it in its build phase. No constructor or relay layer mentions the vif at all — the table replaces the hand-carried config handle. The trade: keys are strings checked at runtime, so a typo or scope mismatch becomes a 'get failed' at run time rather than a compile error. That is why every UVM tutorial insists you check the get() return value and fatal on failure — it is the null-vif guard reborn. Interview angle: 'how would you pass a vif without UVM?' expects exactly these two hand-built patterns, and 'what does config_db buy you?' expects 'decoupled distribution, paid for with runtime string lookup'.
Key takeaways
Middle layers only relay vifs — choose a pattern that keeps relaying cheap.
Constructor chaining is compile-time safe but edits every level per added interface.
A central config class scales: one handle down the tree, leaves pick what they need.
UVM config_db is the same distribution decoupled through a string-keyed table — guard every get().
Common pitfalls
Adding vif arguments through five constructor layers — churn at every level for one new interface.
Forgetting to fill one config field — null-vif crash far from the cause; add a cfg.check() before building.
Copying config contents per component instead of sharing the handle — late top-level fixes never reach copies.
In UVM, ignoring the config_db get() return value — silent null vif, fatal at first pin access.