The DMA bench moves to an emulator for a long soak test. The emulation engineer reads the agents and stops at the driver: vif.psel <= 1'b1; inside a class. "Every one of these lines is a round trip between the emulator and the workstation. Your driver would run at simulator speed and drag the emulator down with it." Meanwhile a second team wants the same APB agent on a 64-bit data bus, and finds that virtual apb_if will not hold an apb_if with different parameters.
Both problems come from the same place: how class code reaches the pins. What are the options, and when is the simple one not enough?
Classes are created at run time and cannot contain wires; interfaces are static hardware instantiated in a module. Something must connect the two. A virtual interface is a variable that holds a reference to an interface instance, like a phone number: with it, a class can read and drive every signal in that interface directly. It is simple, and it is what the DMA bench uses.
An abstract BFM (bus functional model) is a remote control instead of a phone number. The class side holds a handle to an abstract class with methods such as write(addr, data) and read(addr, data). The concrete class that implements those methods is declared inside the interface, so it can touch the pins, and the class side never names a signal or an interface type at all. This is often called the polymorphic interface pattern.
// snippet: from the course DMA bench
module top;
import uvm_pkg::*;
import dma_pkg::*;
logic clk = 0;
always #5 clk = ~clk; // 100 MHz
rst_if rst (clk);
wire rst_n = rst.rst_n;
initial begin
rst.rst_n = 1'b0;
repeat (4) @(posedge clk);
rst.rst_n <= 1'b1;
end
apb_if apb (clk, rst_n);
axis_if s_ax (clk, rst_n);
axis_if m_ax (clk, rst_n);
irq_if irqi (clk);
// The output side accepts every beat unless a test adds a reactive slave
// (axis_sink_agent), whose driver then drives TREADY.
initial m_ax.tready = 1'b1;
dma_dut dut (
.clk, .rst_n,
.paddr(apb.paddr), .psel(apb.psel), .penable(apb.penable), .pwrite(apb.pwrite),
.pwdata(apb.pwdata), .prdata(apb.prdata), .pready(apb.pready), .pslverr(apb.pslverr),
.s_tdata(s_ax.tdata), .s_tvalid(s_ax.tvalid), .s_tlast(s_ax.tlast), .s_tready(s_ax.tready),
.m_tdata(m_ax.tdata), .m_tvalid(m_ax.tvalid), .m_tlast(m_ax.tlast), .m_tready(m_ax.tready),
.irq(irqi.irq)
);
initial begin
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.apb.*", "vif", apb);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.in_agt.*", "vif", s_ax);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.out_agt.*", "vif", m_ax);
uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top", "irq_vif", irqi);
uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top.env.irq.*", "vif", irqi);
uvm_config_db#(virtual rst_if)::set(null, "uvm_test_top*", "rst_vif", rst);
run_test("dma_smoke_test");
end
endmoduleEach set stores a virtual apb_if (or virtual axis_if) under the name "vif" for every component below a path. The context is null, so the paths are full: "uvm_test_top.env.apb.*" reaches env.apb.drv and env.apb.mon but, because a wildcard must match the getter's full name, not env.apb itself, which does not need it. The two stream agents share one class but get different interfaces, purely by path. On the other side, each driver and monitor calls uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif) and stops with a NOVIF fatal if nothing matches.
Two properties of this scheme matter for what follows. The type in #( ) is part of the key: a get for virtual apb_if never sees an entry stored as anything else. And the class code names the interface type: apb_driver cannot be compiled without apb_if.
Suppose the interface grows parameters so one agent can serve several bus widths:
// snippet
interface apb_if #(int AW = 8, int DW = 32) (input logic clk, input logic rst_n);
logic [AW-1:0] paddr;
logic [DW-1:0] pwdata, prdata;
// ...
endinterface
// top module: a 64-bit instance
apb_if #(.AW(12), .DW(64)) apb64 (clk, rst_n);
initial uvm_config_db#(virtual apb_if #(12, 64))::set(null, "uvm_test_top.env.apb.*", "vif", apb64);
// the driver must name the same specialization, or its get finds nothing
class apb_driver #(int AW = 8, int DW = 32) extends uvm_driver #(apb_item #(AW, DW));
`uvm_component_param_utils(apb_driver #(AW, DW))
virtual apb_if #(AW, DW) vif;
// ...
endclassEvery specialization is a different type. virtual apb_if means virtual apb_if #(8, 32), so a driver compiled with the defaults asks config_db for that type, finds nothing, and stops with NOVIF, even though an interface is sitting at the right path. To fix it, the parameters spread to the item, the driver, the monitor, the sequencer, the agent, the env, and every sequence that names the item type. Parameterized classes are registered with uvm_component_param_utils, which registers by type only: factory overrides by name do not work for them, and the factory's printouts are harder to read. Many VIPs avoid all this by using one maximum width in the interface and a run-time width in the config object.
The polymorphic pattern removes the interface type from the class side entirely. This runnable model shows all of it: an abstract class in a package, a concrete class declared inside the interface, and a driver-like class that holds only the abstract handle.
// The abstract-BFM pattern: class code holds a handle to an abstract class;
// the concrete class lives inside the interface and owns every pin.
package bfm_pkg;
virtual class apb_bfm; // all the class side ever sees
pure virtual task write(bit [7:0] addr, bit [31:0] data);
pure virtual task read(bit [7:0] addr, output bit [31:0] data);
endclass
endpackage
interface apb_bfm_if (input logic clk);
import bfm_pkg::*;
logic [7:0] paddr;
logic psel = 0, penable = 0, pwrite = 0;
logic [31:0] pwdata, prdata;
logic pready;
// The concrete BFM is declared inside the interface, so it reaches the pins
// directly: no virtual interface handle anywhere in class code.
class apb_bfm_impl extends apb_bfm;
task write(bit [7:0] addr, bit [31:0] data);
@(posedge clk);
paddr <= addr; pwrite <= 1'b1; pwdata <= data; psel <= 1'b1; penable <= 1'b0;
@(posedge clk);
penable <= 1'b1;
do @(posedge clk); while (!pready);
psel <= 1'b0; penable <= 1'b0;
endtask
task read(bit [7:0] addr, output bit [31:0] data);
@(posedge clk);
paddr <= addr; pwrite <= 1'b0; psel <= 1'b1; penable <= 1'b0;
@(posedge clk);
penable <= 1'b1;
do @(posedge clk); while (!pready);
data = prdata;
psel <= 1'b0; penable <= 1'b0;
endtask
endclass
apb_bfm_impl impl;
// Built on first use, so the handle is valid whatever order initial blocks run in.
function apb_bfm get_bfm();
if (impl == null) impl = new();
return impl;
endfunction
endinterface
// A tiny register slave with one wait state per access.
module apb_regs (input logic clk, apb_bfm_if bus);
logic [31:0] mem [4];
logic waited = 0;
assign bus.pready = waited;
assign bus.prdata = mem[bus.paddr[3:2]];
always_ff @(posedge clk) begin
waited <= bus.psel && bus.penable && !waited;
if (bus.psel && bus.penable && waited && bus.pwrite) mem[bus.paddr[3:2]] <= bus.pwdata;
end
endmodule
In a UVM bench the only changes are where the handle travels and who calls it. The top module stores the abstract handle, and the driver's run_phase calls methods instead of touching signals:
// snippet
// top module
initial uvm_config_db#(apb_bfm)::set(null, "uvm_test_top.env.apb.*", "bfm", apb.get_bfm());
// driver: no virtual interface anywhere
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
if (req.write) bfm.write(req.addr, req.data);
else bfm.read(req.addr, req.rdata);
seq_item_port.item_done();
end
endtaskThe config_db type is now the abstract class, the same for every specialization of the interface: a 64-bit interface's implementation can extend the same apb_bfm and plug into the unchanged driver. The monitor side works the same way in reverse: the interface's implementation detects a completed transfer and calls a method on a class handle it was given (for example, a function that builds the item and calls ap.write).
An emulator runs synthesizable hardware fast; the UVM classes stay on the workstation. Every time a class touches a signal, the two must synchronise, so a driver that wiggles pins from a class runs at the speed of that link. The emulation-friendly structure splits the bench into an HDL side (DUT, interfaces, and BFMs whose pin-level tasks are written in a synthesizable subset) and an HVL side (the UVM classes). Classes call BFM methods once per transaction; the cycle-by-cycle work runs inside the emulator. The abstract-BFM pattern fits that split naturally, because the class side already speaks only in transactions. Each emulation flow has its own rules for what a BFM task may contain, so treat this as the shape, not a recipe.
Virtual interface | Parameterized vif | Abstract BFM | |
|---|---|---|---|
Class code names | The interface type and its signals | The interface type and its parameters | Only an abstract class |
config_db type |
| One type per specialization | One abstract type for all |
Boilerplate | Least | Parameters on every class | Abstract class plus an implementation per interface |
Emulation | Pin access from classes: slow | Same | Transaction calls into the HDL side: fast |
Debug | Signals visible in class code | Same | Pin activity hidden behind method calls |
Tool support | Universal | Universal | Classes inside interfaces: check your simulator |
For a block bench that will only ever simulate, the virtual interface is the right default: it is what the DMA bench uses, and every engineer can read it. Reach for the abstract BFM when emulation is on the roadmap, when one agent must serve many interface specializations, or when the VIP must not expose its signal names.
The model above prints one line when every read-back matches:
PASS: 2 registers written and read back through the abstract BFM handleThe interesting part is what does not appear in reg_driver: no virtual, no apb_bfm_if, no signal name. Replace apb_bfm_if with a 64-bit interface whose implementation extends the same apb_bfm, and reg_driver compiles and runs unchanged.
The second team from the opening added parameters to apb_if, instantiated apb_if #(.AW(12), .DW(64)) in their top, and set it with uvm_config_db#(virtual apb_if #(12, 64)). The shared agent package still declared virtual apb_if vif; in its driver. Every run stopped at time 0:
UVM_FATAL dma_tb.sv(...) @ 0: uvm_test_top.env.apb.drv [NOVIF] apb_if not set for this driverThe entry was at the right path under the right name, and +UVM_CONFIG_DB_TRACE showed it. It was the wrong type: the driver asked for virtual apb_if #(8, 32). The quick fix parameterized the driver; the next week they were parameterizing the monitor, the sequencer, the agent, the env and forty sequences. They settled on an abstract apb_bfm handle in config_db, with one implementation inside each interface variant. The agent classes went back to having no parameters at all.
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.apb", "vif", apb); with no wildcard. The driver's get fails. Why?The path must match the getter's full name. The driver is uvm_test_top.env.apb.drv, and the path uvm_test_top.env.apb matches only the agent itself. Use "uvm_test_top.env.apb.*" as the bench does, or have the agent get the handle and pass it to its children.
get_bfm() instead of initialising apb_bfm_impl impl = new(); in the declaration?Static variable initialisers and initial blocks in different scopes run in an order the standard does not fix. A testbench initial that reads bus.impl at time 0 might see null. Creating the object on first use makes the handle valid whatever order things run in.
Size the interface and the item for the maximum width (64 bits) and put the active width in the agent's config object. The driver and monitor mask or check against the configured width at run time. Classes stay unparameterized, config_db sees one interface type, and factory overrides by name keep working.
Next: you now know what every agent is made of. Reusable VIP turns the same agent into something another team can use: passive one level up, parameterized, versioned. After it, error injection and the agent patterns put the parts to harder work.