A virtual interface type includes the interface's parameter values. virtual bus_if #(8) and virtual bus_if #(32) are different types, and one cannot be assigned to the other. Verilator rejects such an assignment at compile time with a message that the right-hand side is a different interface specialization.
So any class that stores the handle must name a specialization. Write it generically and the class needs its own parameter, and so does every class that holds that class. The width ends up on the driver, monitor, agent, environment, config object and test.
test #(W) ─► env #(W) ─► agent #(W) ─► driver #(W) ─► virtual bus_if #(W)
└──► monitor #(W) ─► virtual bus_if #(W)
One width change = every #(W) in the chain must agree, or the build fails.Technique | What it fixes | What remains |
|---|---|---|
typedef per specialization | Widths are spelled once: typedef virtual bus_if #(.W(32)) bus32_vif; | Classes still carry #(W) |
Config object carrying the vif | One object per bus instance; agents receive a cfg, not loose handles | The cfg class is parameterized |
Max-width interface plus runtime width | One interface type, W_MAX bits, a runtime active width | Unused upper bits, and every check must mask |
Abstract BFM (previous lesson) | The environment holds no vif and no parameter | An API to design and maintain |
Parameterized classes are the right tool when the width changes data types inside the class, for example a typedef bit [W-1:0] data_t that the driver uses. The cost is that every specialization is a separate class type, so a queue cannot hold drivers of different widths.
Two memories, 8 and 32 bits. A typedef names each virtual interface specialization once. A parameterized config object carries the handle and its name, and a parameterized driver writes four patterns that light the top bit and the top byte, then reads them back.
interface bus_if #(parameter int W = 8) (input logic clk);
// Elaboration-time guard: reported while the design is built.
if (W % 8 != 0 || W > 64) begin : g_bad_width
$error("bus_if: W must be a multiple of 8 and at most 64");
end
// Run-time backstop, in case the build was told to treat errors as warnings.
initial if (W % 8 != 0 || W > 64) $fatal(1, "FAIL: bus_if built with W=%0d", W);
logic en, we;
logic [7:0] addr;
logic [W-1:0] wdata, rdata;
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
// One typedef per specialization: the only place the widths are spelled out.
typedef virtual bus_if #(.W(8)) bus8_vif;
typedef virtual bus_if #(.W(32)) bus32_vif;
// The config object carries the handle; W travels with the type.
class bus_cfg #(int W = 8);
virtual bus_if #(.W(W)) vif;
string name;
function new(string name, virtual bus_if #(.W(W)) vif);
this.name = name; this.vif = vif;
endfunction
endclass
class bus_driver #(int W = 8);
typedef bit [W-1:0] data_t;
bus_cfg #(W) cfg;
int errors;
function new(bus_cfg #(W) cfg); this.cfg = cfg; endfunction
task write(bit [7:0] a, data_t d);
@(negedge cfg.vif.clk);
cfg.vif.en <= 1; cfg.vif.we <= 1; cfg.vif.addr <= a; cfg.vif.wdata <= d;
@(negedge cfg.vif.clk);
cfg.vif.en <= 0; cfg.vif.we <= 0;
endtask
task read(bit [7:0] a, output data_t d);
@(negedge cfg.vif.clk);
cfg.vif.en <= 1; cfg.vif.we <= 0; cfg.vif.addr <= a;
@(negedge cfg.vif.clk);
cfg.vif.en <= 0;
d = cfg.vif.rdata;
endtask
// Patterns that light the top byte catch any width truncation on the path.
task run();
data_t pat [4] = '{'1, {1'b1, {(W-1){1'b0}}}, data_t'(64'hA5A5_5A5A_C3C3_3C3C), '0};
data_t got;
foreach (pat[i]) write(8'(i), pat[i]);
foreach (pat[i]) begin
read(8'(i), got);
if (got !== pat[i]) begin
errors++;
$display("%s addr %0d: got %h exp %h", cfg.name, i, got, pat[i]);
end
end
endtask
endclass
typedef bus_driver #(8) drv8_t;
typedef bus_driver #(32) drv32_t;
The patterns are chosen to expose truncation: all ones, only the top bit, and a mixed pattern. A path that drops upper bits passes a random test with small values more often than you would expect.
Mutant run. Instantiating the 32-bit memory with mem #(.W(8)), a parameter that was not propagated, still compiles: Verilator reports the port width mismatch as a WIDTHTRUNC warning, and the course runner's -Wno-fatal lets the build continue. The bench fails with 3 mismatches, for example got 0000003c exp c3c33c3c.
The $error inside the generate if is an elaboration system task. The LRM says that an elaboration-time $error lets elaboration continue so that further errors are reported, but no simulation runs. Tools differ in how strictly they apply that:
Tool (as tested here) | bus_if #(.W(12)) |
|---|---|
Icarus 13 | Elaboration error, no simulation. Accepts only a single string argument for elaboration tasks. |
Verilator 5.048, default | USERERROR warning, which stops the build because warnings are fatal by default. |
Verilator 5.048 with -Wno-fatal | The warning is printed and the simulation still runs. |
The course runner uses -Wno-fatal, which is common in real flows too. That is why the interface also has a run-time $fatal backstop. With W set to 12, Verilator prints the elaboration message and the run then ends with FAIL: bus_if built with W=12 at time 0.
Benefit: one interface type, so no parameter spreads through the classes. Risk: upper bits that the DUT does not drive are still visible, so every driver, monitor and scoreboard must mask to the active width, and a missed mask shows up as spurious mismatches or hidden bugs.
The config object groups everything that describes one bus instance (handle, name, later timeouts or modes). Agents then take one argument that can grow without changing constructor signatures, and the vif type is named in one place.