Modules and interfaces are elaborated before time zero. Every instance owns a fixed hierarchical path.
Class objects are created later by new(). They live on the heap with no hierarchical path and no pins.
Workaround | Why it fails |
|---|---|
Hierarchical paths in class code ( | Welds the class to one testbench, so a second instance cannot reuse it. |
Passing every signal as a task argument | Hundreds of ports with no grouping, no modports, and no clocking blocks. |
Instantiating an interface inside the class | Illegal: static instances need an elaboration-time position, which classes lack. |
A virtual bus_if variable stores a reference to one elaborated interface instance. Assigning it copies only the handle, never the signals.
Two objects holding the same vif observe the same pins. Reassigning a vif retargets the object without touching class code.
Before any assignment the vif is null, and every pin access through it is a dereference. The raw vif.addr drive in the bench uses <=, so DUT flops on that edge still capture the old value. Driving & Sampling via Clocking Blocks moves every drive onto vif.cb, which makes the timing explicit and the same for every component.
Nothing. Reassign its vif from the first instance to the second. The class holds a reference, so retargeting needs no source change.
addr. What does the DUT see?One contested signal. Both objects drive the same physical pins through the shared reference, so the testbench has a multiple-driver bug, not two independent streams.
The listing below binds one driver, retargets it, and checks that only the second instance is driven. Two words appear before their lessons: $urandom returns a random 32-bit number, and fork ... join_none starts d.run() in the background and carries on at once.
bif0 and then to bif1 before run() starts. After 100 ns, which instance's valid is 1?Only bif1's. The vif holds one reference, and the second assignment replaced the first before any drive happened.
interface bus_if (input logic clk);
logic [31:0] addr;
logic [31:0] data;
logic valid;
endinterface
class driver;
virtual bus_if vif; // a REFERENCE -- null until assigned
task run();
forever begin
@(posedge vif.clk); // follows the reference to the real instance
vif.addr <= $urandom; // raw NBA drive; the clocking lesson moves this onto vif.cb
vif.valid <= 1'b1;
end
endtask
endclass
module tb_top;
timeunit 1ns;
timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
bus_if bif0 (clk);
bus_if bif1 (clk);
initial begin
driver d; // declare first: an initializer here would run at time 0
d = new();
d.vif = bif0; // d now drives bif0's signals
d.vif = bif1; // retargeted -- zero class changes
fork d.run(); join_none
#100;
if (bif1.valid !== 1'b1 || bif0.valid === 1'b1)
$fatal(1, "FAIL: retargeted vif did not move the drive to bif1");
$display("PASS: retargeted vif drives bif1 and leaves bif0 alone");
$finish;
end
endmodulePASS: retargeted vif drives bif1 and leaves bif0 aloneOne way to write it. The drives are raw nonblocking assignments for now; two lessons on they move onto a clocking block.
interface pkt_if (input logic clk, input logic rst_n);
logic valid, ready, last;
logic [7:0] data;
endinterface
// A sink that is always ready and records what it accepts.
module pkt_sink (pkt_if p);
logic [7:0] got [$];
bit saw_last = 0;
assign p.ready = 1'b1;
always @(posedge p.clk)
if (p.rst_n && p.valid && p.ready) begin
got.push_back(p.data);
if (p.last) saw_last <= 1;
end
endmodule
// PacketFIFO's first driver: a class that reaches the pins through a virtual interface.
class pkt_driver;
virtual pkt_if vif;
function new(virtual pkt_if vif);
this.vif = vif;
endfunction
task send(bit [7:0] bytes [$]);
foreach (bytes[i]) begin
@(posedge vif.clk);
vif.valid <= 1'b1; // nonblocking: the sink sees it next edge
vif.data <= bytes[i];
vif.last <= (i == bytes.size() - 1);
do @(posedge vif.clk); while (!vif.ready);
vif.valid <= 1'b0;
vif.last <= 1'b0;
end
endtask
endclass
module tb_pkt_driver;
logic clk = 0, rst_n = 0;
always #5 clk = ~clk;
pkt_if bus (.clk(clk), .rst_n(rst_n));
pkt_sink u_snk (.p(bus));
initial begin
pkt_driver drv;
bus.valid = 0; bus.last = 0; bus.data = 0;
drv = new(bus); // the static instance, handed to the class
@(negedge clk) rst_n = 1;
drv.send('{8'h22, 8'hA5, 8'h3C, 8'hBB});
@(posedge clk);
$display("sink got %p", u_snk.got);
if (u_snk.got.size() == 4 && u_snk.got[3] == 8'hBB && u_snk.saw_last)
$display("PASS: pkt_driver sent the 4-byte packet through its virtual pkt_if");
else $fatal(1, "FAIL: sink got %0d bytes", u_snk.got.size());
$finish;
end
endmodulesink got '{'h22, 'ha5, 'h3c, 'hbb}
PASS: pkt_driver sent the 4-byte packet through its virtual pkt_ifRun 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.