A 2-port DUT instantiates two interfaces and needs two agents. A vif array turns the binding paths into one loop.
Static instance arrays are hierarchy, not variables. Collect them into vif variables element by element, then loop downstream.
interface bus_if (input logic clk);
logic [7:0] data;
logic valid;
endinterface
class agent; // one per port: here just a driver
virtual bus_if vif;
function new(virtual bus_if vif);
this.vif = vif;
endfunction
task drive(bit [7:0] d);
@(posedge vif.clk);
vif.data <= d;
vif.valid <= 1'b1;
endtask
endclass
class env;
agent agts[2];
function new(input virtual bus_if vifs[2]);
foreach (agts[i])
agts[i] = new(vifs[i]); // per-port binding, one loop
endfunction
endclass
module tb_ports;
logic clk = 0;
always #5 clk = ~clk;
bus_if bif[2] (clk); // two REAL instances for the 2-port DUT
virtual bus_if vifs[2]; // dynamic side: an array of references
initial begin
env e;
vifs[0] = bif[0]; // collect element by element
vifs[1] = bif[1];
e = new(vifs);
fork
e.agts[0].drive(8'h11);
e.agts[1].drive(8'h22);
join
#1;
if (bif[0].data == 8'h11 && bif[1].data == 8'h22)
$display("PASS: 2 agents built in one loop each drove their own port");
else $fatal(1, "FAIL: ports got %h and %h", bif[0].data, bif[1].data);
$finish;
end
endmodulePASS: 2 agents built in one loop each drove their own portThe constructor formal carries an explicit direction. Some compilers reject a bare unpacked-array formal, so input is written out.
A parameterized interface creates one type per parameter value. The matching vif types are equally distinct and never cross-assign.
The compiler is protecting the widths. A 32-bit driver attached to 64-bit pins would be silently wrong, so the mismatch is a compile error.
v32 is a virtual pbus_if #(32). Does v32 = bif64 compile, where bif64 is a pbus_if #(64) instance?No. Each parameterization is its own type, so a 32-bit vif cannot point at a 64-bit instance; the compiler protects the widths.
interface pbus_if #(int WIDTH = 32) (input logic clk);
logic [WIDTH-1:0] data;
logic valid;
endinterface
module tb_top;
timeunit 1ns;
timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
pbus_if #(32) bif32 (clk);
pbus_if #(64) bif64 (clk);
virtual pbus_if #(32) v32;
virtual pbus_if #(64) v64;
initial begin
v32 = bif32; // OK: types match exactly
v64 = bif64;
// v32 = bif64; // COMPILE ERROR: distinct types
// v64 = v32; // COMPILE ERROR: vifs never cross-assign
if ($bits(v32.data) != 32 || $bits(v64.data) != 64)
$fatal(1, "FAIL: vif widths differ from 32 and 64");
$display("PASS: each vif matches its own interface width");
$finish;
end
endmodulePASS: each vif matches its own interface widthTypedef-per-width embraces the distinct types. Each driver specialization carries its matching vif, and mismatches fail at compile time.
The listing below repeats pbus_if so it runs on its own.
interface pbus_if #(int WIDTH = 32) (input logic clk);
logic [WIDTH-1:0] data;
logic valid;
endinterface
class driver #(int WIDTH = 32);
typedef virtual pbus_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; raw drive kept short on purpose
vif.valid <= 1'b1;
endtask
endclass
module tb_top;
timeunit 1ns;
timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
pbus_if #(32) bif32 (clk);
pbus_if #(64) bif64 (clk);
initial begin
driver #(32) d32; // declare first, then construct in order
driver #(64) d64;
d32 = new(bif32);
d64 = new(bif64);
fork
d32.drive(32'hA5A5_0001);
d64.drive(64'h1_0000_0002);
join
#1;
if (bif32.data !== 32'hA5A5_0001 || bif64.data !== 64'h1_0000_0002)
$fatal(1, "FAIL: a driver specialization drove the wrong width or value");
$display("PASS: 32- and 64-bit drivers each drove their own vif");
$finish;
end
endmodulePASS: 32- and 64-bit drivers each drove their own vifMax-width-generic shares one interface at the widest port and carries the active width at run time. One agent serves every port, but width checks become manual and upper bits need masking.
Strategy | Wins | Pays |
|---|---|---|
Typedef per width | Compile-time width safety | One class specialization per width |
Max-width generic | Uniform agents and vif arrays | Run-time width checks and tie-offs |
v32 = bif64 fails to compile although both are the same interface. Why?Parameterization is part of the type. pbus_if #(32) and pbus_if #(64) are distinct types, so their vifs never cross-assign.
driver #(32) d32 = new(bif32) sits at compilation-unit scope. What breaks?At $unit scope, bif32 is not visible: it is declared inside tb_top. Declare the handle in tb_top, either as a module item with an initializer or inside initial.
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.