A driver that holds virtual bus_if #(32) vif knows three things it should not: which interface type the pins use, its parameter values, and the signal names. Every class that holds or passes that handle inherits the same knowledge. Change the width and the edit ripples through the driver, the monitor, the agent, the config object and the test.
The abstract BFM pattern, sometimes called a polymorphic interface, removes that knowledge. The environment calls methods on an abstract class. The concrete class that implements those methods is declared inside the interface, where the pins and the parameters are already in scope.
package / $unit interface bus_if #(W)
┌──────────────────────┐ ┌──────────────────────────────┐
│ virtual class bus_bfm │◄─extends┤ class bfm_impl │
│ pure virtual write │ │ write(): drives en/addr/... │
│ pure virtual read │ │ width(): returns W │
└──────────▲───────────┘ │ function bus_bfm get_bfm() │
│ handle └──────────────────────────────┘
┌──────────┴───────────┐
│ class env │ no vif, no #(W), one copy for every width
└──────────────────────┘The API: a virtual class with pure virtual tasks and functions, declared outside any interface (a package in real code). Its signatures use the widest data type the family needs, here 64 bits.
The implementation: a class declared inside the interface that extends the API. It reads and drives the interface signals directly and can use the interface parameter W.
The hand-off: a function in the interface returns the implementation as a base-class handle. The top module calls it hierarchically and gives the handle to the environment.
The concrete class is a different type in every interface specialization, but nothing outside the interface ever names it. The environment only sees bus_bfm, so one environment class serves an 8-bit and a 32-bit bus without a parameter.
Two memories, 8 and 32 bits wide, each behind a bus_if of matching width. The same env class writes 20 random words through each BFM, masks the expected data to the width the BFM reports, then reads every address back.
// The environment sees only this API: no virtual interface, no width parameter.
virtual class bus_bfm;
pure virtual function int unsigned width();
pure virtual task write(bit [7:0] addr, bit [63:0] data);
pure virtual task read(bit [7:0] addr, output bit [63:0] data);
endclass
interface bus_if #(parameter int W = 8) (input logic clk);
logic we, en;
logic [7:0] addr;
logic [W-1:0] wdata, rdata;
// Concrete BFM lives inside the interface: it sees the pins and W directly.
class bfm_impl extends bus_bfm;
function int unsigned width(); return W; endfunction
task write(bit [7:0] a, bit [63:0] d);
@(negedge clk);
en <= 1; we <= 1; addr <= a; wdata <= d[W-1:0];
@(negedge clk);
en <= 0; we <= 0;
endtask
task read(bit [7:0] a, output bit [63:0] d);
@(negedge clk);
en <= 1; we <= 0; addr <= a;
@(negedge clk);
en <= 0;
d = 64'(rdata);
endtask
endclass
bfm_impl bfm;
// Built on first request, so no initial-block ordering is involved.
function bus_bfm get_bfm();
if (bfm == null) bfm = new();
return bfm;
endfunction
initial begin en = 0; we = 0; end
endinterface
module mem #(parameter int W = 8) (
input logic clk, en, we,
input logic [7:0] addr,
input logic [W-1:0] wdata,
output logic [W-1:0] rdata
);
logic [W-1:0] m [256];
always_ff @(posedge clk)
if (en) begin
if (we) m[addr] <= wdata;
else rdata <= m[addr];
end
endmodule
// Written once, used for every width: no parameter, no virtual interface.
class env;
bus_bfm bfm;
string tag;
int errors;
function new(string tag, bus_bfm bfm); this.tag = tag; this.bfm = bfm; endfunction
task run(int n);
bit [63:0] mask, exp [bit [7:0]], got, d;
bit [7:0] a;
mask = (bfm.width() == 64) ? '1 : (64'd1 << bfm.width()) - 1;
for (int i = 0; i < n; i++) begin
a = 8'(i * 7);
d = {$urandom, $urandom};
bfm.write(a, d);
exp[a] = d & mask;
end
foreach (exp[k]) begin
bfm.read(k, got);
if (got !== exp[k]) begin
errors++;
$display("%s addr %0h: got %h exp %h", tag, k, got, exp[k]);
end
end
endtask
endclass
Two timing choices keep the bench free of races. The BFM drives on the falling edge with nonblocking assignments, so the memory samples settled values at the rising edge. The read task samples rdata at the falling edge after the read cycle, half a period after the memory updated it.
get_bfm() builds the object on first use. That removes any dependence on the order in which initial blocks in the interface and in the top module start.
Mutant run. Hard-coding an 8-bit slice in the BFM's read (d = 64'(rdata[7:0])) still passes on the 8-bit bus and fails all 20 read-backs on the 32-bit bus. Width assumptions are exactly what this pattern moves into the interface, where W is known.
Suppose a third project needs the same protocol with a 64-bit bus and a different wait-state rule. With this pattern the work is local: a new interface (or a new parameter value), a new implementation class inside it, and one line in the top module that hands its BFM to the environment. The environment, the scoreboard and every test stay untouched, because none of them names the interface type.
Compare that with a parameterized environment: the new width is a new specialization of every class in the chain, and any code that stores agents of different widths in one array stops compiling. The abstract BFM trades that ripple for a one-time cost, which is designing an API general enough for every variant.
Concern | What to do |
|---|---|
API must cover every variant | Size data arguments for the widest variant and expose width() so callers can mask. |
Monitors | Same pattern: an abstract monitor class whose implementation inside the interface samples the pins and returns transactions. |
Clocking blocks | Declare them in the interface; the concrete class uses them like any interface item. |
Interfaces bound into the DUT | Works well with bind: the bound interface hands out its BFM handle, and the environment never needs its hierarchical path. |
Debug | Stack traces show the class inside the interface instance, which helps locate which bus a failure came from. |
A class declared inside an interface is in the interface's scope, so it sees the interface parameters and signals directly, much like a task declared in that interface. Each interface instance has its own copy of that scope.
From the API, through a method such as width() implemented in the interface. The environment should never read a parameter of a type it does not name.