Part 2 · OOP for Verification · Intermediate

Multi-Instance & Parameterized vifs

Arrays of vifs for multi-port DUTs, why parameterized interfaces make vif types distinct, and width strategies.

Arrays of vifs for multi-port DUTs

A 4-port switch needs four interface instances and four agents — but you do not write four binding paths. Because a vif is just a variable, you can build an array of vifs , fill each element from the corresponding static instance, and loop over agents. One wrinkle: the static instances themselves (bus_if bif[4](clk) via generate or an instance array) are hierarchy, not variables — you cannot assign the whole array in one statement. The standard idiom collects them element-by-element (or via a small generate block) into the dynamic array, then everything downstream is a plain loop.

systemverilog
module tb_top;
  logic clk = 0;
  always #5 clk = ~clk;

  // Four REAL instances wired to the 4-port DUT
  bus_if bif[4] (clk);

  // Dynamic side: an array of references
  virtual bus_if vifs[4];

  initial begin
    env e;
    // collect static instances into the vif array
    vifs[0] = bif[0];  vifs[1] = bif[1];
    vifs[2] = bif[2];  vifs[3] = bif[3];

    e = new(vifs);
    e.run();
  end
endmodule

class env;
  agent agts[4];

  function new(virtual bus_if vifs[4]);
    foreach (agts[i])
      agts[i] = new(vifs[i]);   // per-port binding, one loop
  endfunction
endclass
diagram
MULTI-PORT BINDING

  STATIC                          DYNAMIC
  bif[0] ──────────────────────► vifs[0] ──► agts[0] (drv+mon)
  bif[1] ──────────────────────► vifs[1] ──► agts[1]
  bif[2] ──────────────────────► vifs[2] ──► agts[2]
  bif[3] ──────────────────────► vifs[3] ──► agts[3]
     │                               │
  hierarchy (generate/array        variables (assignable,
  instances, fixed at elab)        loopable, passable)

Parameterized interfaces make vif types distinct

Interfaces take parameters just like modules — bus_if #(.WIDTH(64)). The consequence mirrors parameterized classes: each parameterization is a distinct type , and so is the matching vif. A virtual bus_if #(32) cannot hold a reference to a bus_if #(64) instance, the two vifs cannot cross-assign, and they cannot share one array. The compiler is protecting you — a 32-bit driver blindly attached to 64-bit pins would be wrong — but it means 'one driver class for all widths' does not happen by accident; you must choose a strategy.

systemverilog
interface bus_if #(int WIDTH = 32) (input logic clk);
  logic [WIDTH-1:0] data;
  logic             valid;
endinterface

module tb_top;
  logic clk;
  bus_if #(32) bif32 (clk);
  bus_if #(64) bif64 (clk);

  virtual bus_if #(32) v32;
  virtual bus_if #(64) v64;

  initial begin
    v32 = bif32;       // OK — types match exactly
    // v32 = bif64;    // COMPILE ERROR: distinct types
    // v64 = v32;      // COMPILE ERROR: vifs don't cross-assign
  end
endmodule

Width strategies

Strategy 1: typedef per width + parameterized components

Embrace the distinct types: typedef virtual bus_if #(32) vif32_t; in a package, and make the driver/monitor/agent classes parameterized with the same WIDTH so each specialization carries the matching vif type. Fully type-safe — a width mismatch is a compile error — at the cost of one class specialization per width (and remember from parameterized classes: separate statics per specialization).

systemverilog
class driver #(int WIDTH = 32);
  typedef virtual bus_if #(WIDTH) vif_t;
  vif_t vif;

  function new(vif_t vif);
    this.vif = vif;
  endfunction

  task drive(bit [WIDTH-1:0] d);
    @(posedge vif.clk);
    vif.data  <= d;          // width checked at compile time
    vif.valid <= 1'b1;
  endtask
endclass

driver #(32) d32 = new(bif32);
driver #(64) d64 = new(bif64);

Strategy 2: generic max-width interface

Declare the interface at the maximum width the project needs and carry the active width as a runtime field (in the interface or config). Every port then shares one interface type, one vif type, and one unparameterized agent — arrays of vifs across mixed-width ports work again. The cost: width checking moves from compile time to your own runtime checks, and unused upper bits must be tied off and masked. Interview angle: 'your DUT has 32- and 64-bit ports — one agent or two?' has no single right answer; strong candidates present exactly this trade-off (type safety per width vs uniform reusable infrastructure) and pick based on how many widths and how much sharing the project needs.

Key takeaways

  • Arrays of vifs plus a foreach loop scale binding to N-port DUTs cleanly.

  • Static instance arrays are hierarchy — collect them into vif variables element-wise, then loop.

  • Each interface parameterization is a distinct type; its vif type is equally distinct — no cross-assignment.

  • Choose deliberately: typedef-per-width for compile-time safety, generic max-width for uniform infrastructure.

Common pitfalls

  • Assuming an instance array assigns to a vif array in one statement — collect element-by-element.

  • Mixing vifs of different parameterizations in one array — compile error; they are unrelated types.

  • Choosing max-width-generic and forgetting runtime width checks — silent truncation on narrow ports.

  • Forgetting that parameterized component classes get separate static members per specialization.