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.

systemverilog
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
endmodule

Why 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.

diagram
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 START

Guarding patterns

systemverilog
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
endclass

Interview 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.