Part 2 · OOP for Verification · Intermediate
Declaring & Binding a vif
Virtual interface declaration in classes, assignment from tb_top, and guarding against the classic null-vif crash.
Declaration and the binding direction
Declaring a vif in a class is one line: virtual bus_if vif;. The interesting part is binding — getting a real instance into that variable. Only the static world knows the instances, so binding always flows top-down : the top module, which instantiated the interface, passes it into the class world, typically as a constructor argument. The class never reaches up to find it; it is handed the reference at construction.
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);
if (vif == null)
$fatal(1, "driver::new called with null vif");
this.vif = vif; // bind at construction — never null afterward
endfunction
task run();
forever begin
@(posedge vif.clk);
vif.addr <= $urandom;
vif.valid <= 1'b1;
end
endtask
endclass
module tb_top;
logic clk = 0;
always #5 clk = ~clk;
bus_if bif (clk); // the REAL instance, elaborated here
// dut u_dut (.bus(bif)); // same instance wired to the DUT
initial begin
driver drv = new(bif); // STATIC → DYNAMIC handoff
drv.run();
end
endmoduleWhy constructor binding is the strong default
The object is never in a half-built state — if it exists, its vif is valid.
The null check lives in exactly one place (new), not scattered before every pin access.
Reviewers see the dependency in the signature: a driver visibly requires a bus_if to exist.
The classic null-vif crash
The most common beginner fatal in class-based testbenches: a process touches vif.clk before anything assigned the vif. Because the vif defaults to null, the first dereference produces a null-handle fatal — usually at time zero, usually from a forever @(posedge vif.clk) loop that started before the binding code ran. The root cause is always an ordering bug: construction/start happened before binding.
NULL-VIF CRASH TIMELINE
buggy ordering correct ordering
────────────── ────────────────
t=0: drv = new(); vif = null t=0: drv = new(bif); vif = bif ✓
t=0: fork drv.run() ─┐ t=0: drv.check(); guard passes
@(posedge vif.clk) t=0: fork drv.run()
│ @(posedge vif.clk)
▼ │
NULL DEREFERENCE ▼
** Fatal: null handle ** waits on real clock ✓
rule: BIND, then GUARD, then STARTGuarding patterns
class monitor;
virtual bus_if vif;
// Pattern 1: guard in new() when binding via constructor
function new(virtual bus_if vif);
if (vif == null) $fatal(1, "monitor: null vif at construction");
this.vif = vif;
endfunction
endclass
class env;
driver drv;
monitor mon;
// Pattern 2: late binding via a connect step — guard there,
// and again at start-of-run as a final safety net
function void connect(virtual bus_if bif);
drv.vif = bif;
mon = new(bif);
endfunction
task run();
if (drv.vif == null)
$fatal(1, "env::run — drv.vif never bound; check connect order");
fork
drv.run();
join_none
endtask
endclassInterview angle: 'your sim dies with a null handle at time 0 in the driver — walk me through your debug.' The expected answer names the vif, identifies that run() started before binding, and proposes guards at construction or connect plus correct start ordering. Mentioning that UVM solves the distribution half of this with uvm_config_db (covered in the configuration-patterns lesson) is a bonus.
Key takeaways
Binding flows top-down: tb_top owns the instance and hands the reference into the class world.
Bind in the constructor when possible — the object is then valid for its whole lifetime.
Guard with an explicit null check in new() or connect(), and fatal early with a clear message.
Start time-consuming processes only after binding — bind, guard, then start.
Common pitfalls
Forking run() before assigning the vif — the canonical time-zero null-handle fatal.
Guarding nowhere and letting the simulator's generic fatal hide which vif was unbound.
Binding one component but forgetting another — driver works, monitor crashes mid-sim.
Assigning the vif inside the class from a hierarchical path — it binds, but the class is no longer reusable.