SystemVerilog · Module 8
Virtual Interfaces
The bridge between module-world interfaces and class-world UVM components; config_db handoff, modport-qualified handles, and 5 debugging labs.
Module 8 · Page 8.4
Why class-based testbench components cannot hold interface instances directly, how virtual interface handles solve this, and the complete pattern for passing interfaces into class-based drivers, monitors, and UVM environments.
The Problem — Classes Cannot Hold Interface Instances
By page 8.1 you know that a class-based driver is the standard way to write reusable, object-oriented testbench components. But here is the collision: an interface instance is a static, elaboration-time construct. Its memory is allocated when the simulator builds the design hierarchy — before simulation even starts. A class instance, on the other hand, is a dynamic, run-time construct allocated on the heap at any point during simulation.
You cannot put a static construct inside a dynamic one. Trying to declare an interface instance as a class property is a compile error:
// ✗ ILLEGAL — compile error
class apb_driver;
apb_if bus; // ← ERROR: cannot instantiate an interface inside a class
// "Interface instance not allowed in class body"
task run();
bus.cb.psel <= 1;
endtask
endclass
// ✓ LEGAL — virtual interface handle inside a class
class apb_driver;
virtual apb_if vif; // ← handle to an interface, not an instance
task run();
vif.cb.psel <= 1; // ← access via the handle — works fine
endtask
endclassThe virtual keyword changes the meaning completely. virtual apb_if vif is not an interface instance — it is a handle (pointer) that can point to any existing apb_if instance. The actual interface still lives in the static hierarchy (in the TB top). The class just holds a reference to it.
What is a Virtual Interface?
A virtual interface is a variable that holds a reference (handle) to an actual interface instance. It gives any class — regardless of where in the dynamic hierarchy it lives — indirect access to the static design hierarchy through the interface.
The actual apb_if instance lives in tb_top (static). The apb_driver class holds only a lightweight handle. Assignment at runtime connects them. The class accesses the hardware signals as if it owned the interface directly:
// ── Static hierarchy: tb_top ─────────────────────────────────────
module tb_top;
logic clk, rst_n;
apb_if apb (.pclk(clk), .presetn(rst_n)); // actual interface instance
apb_driver drv;
initial begin
drv = new();
drv.vif = apb; // ← assign the handle (drv.vif now points at apb)
end
endmodule
// ── Dynamic class: apb_driver ────────────────────────────────────
class apb_driver;
virtual apb_if vif; // null until assigned
task run();
vif.cb.psel <= 1; // access via the handle
##1;
endtask
endclassDeclaring & Using a Virtual Interface
// ① Declare with 'virtual' keyword — this is a handle, not an instance
virtual apb_if vif; // handle to any apb_if instance
virtual apb_if.master vif_m; // handle restricted to master modport
virtual apb_if.tb vif_tb; // handle restricted to tb modport
// ② Default value is null — must be assigned before use
if (vif == null)
$fatal("Virtual interface not assigned!");
// ③ Assign from an actual interface instance
apb_if apb (.pclk(clk), .presetn(rst_n)); // actual instance in TB top
vif = apb; // assign full interface
vif_m = apb; // also legal — modport is applied on access
vif_m = apb.master; // alternatively specify modport explicitly
// ④ Access signals through the virtual interface handle
vif.pclk // direct signal access (no modport enforcement)
vif.cb.psel <= 1; // access via clocking block
vif_m.cb.psel <= 1; // same — modport enforces directionFour things to remember about every declaration:
virtualkeyword — the only difference from a regular interface port. Withoutvirtual, the compiler treats it as an interface instantiation (which is illegal in a class). Withvirtual, it is a handle type.- Default is
null— exactly like a class handle. Using a null virtual interface causes a run-time error ("null object dereference"). Always check or assign before use. - Assigning — you assign an actual interface instance to the virtual interface handle. After assignment, the virtual interface "points at" the actual instance and all signal reads/writes go through it to the real wires.
- Access pattern — once assigned, use
vif.signal_nameorvif.cb.signaljust as if you had the interface directly. Every access goes through the handle to the real interface in the TB hierarchy.
Passing the Interface to a Class — Three Patterns
There are three standard patterns for getting the virtual interface handle into a class. All three are used in practice — the right one depends on your testbench architecture.
Pattern 1 — Constructor argument (simplest)
// ─── The driver class ──────────────────────────────────────────────────
class apb_driver;
virtual apb_if.tb vif; // virtual interface handle (null initially)
// Constructor accepts the interface and stores it
function new(virtual apb_if.tb vif);
this.vif = vif;
endfunction
task drive_write(input logic [31:0] addr, data);
@(vif.cb);
vif.cb.paddr <= addr;
vif.cb.pwdata <= data;
vif.cb.psel <= 1;
vif.cb.pwrite <= 1;
@(vif.cb);
vif.cb.penable <= 1;
while (!vif.cb.pready) @(vif.cb);
@(vif.cb);
vif.cb.psel <= 0;
vif.cb.penable <= 0;
endtask
endclass
// ─── TB top: create driver and pass the interface ─────────────────────
module tb_top;
logic clk = 0, rst_n;
apb_if apb (.pclk(clk), .presetn(rst_n));
apb_driver drv;
initial begin
drv = new(apb.tb); // pass interface at construction
rst_n = 0; ##3;
rst_n = 1; ##2;
drv.drive_write(32'h4000, 32'hFF);
end
always #5 clk = ~clk;
endmodulePattern 2 — Set method / property after construction
class apb_driver;
virtual apb_if.tb vif; // null until set
function new(); // no interface argument
endfunction
// Setter — can be called any time before run()
function void set_if(virtual apb_if.tb vif);
this.vif = vif;
endfunction
task run();
if (vif == null) $fatal("Virtual interface not set!");
// ... drive signals ...
endtask
endclass
// In TB top
apb_driver drv = new();
drv.set_if(apb.tb); // set after construction
drv.run();Pattern 3 — UVM config database (industry standard)
In UVM-based testbenches, virtual interfaces are not passed directly. They are stored in a central configuration database — and any component in the UVM hierarchy can retrieve them by name. This decouples the TB topology from the interface connections completely.
// ─── TB top: store the interface in the config database ────────────────
module tb_top;
logic clk = 0, rst_n;
apb_if apb (.pclk(clk), .presetn(rst_n));
initial begin
// Store the virtual interface in the UVM config database
// Path "*" means any UVM component can retrieve it
uvm_config_db#(virtual apb_if)::set(
null, "*", "apb_vif", apb
);
run_test();
end
endmodule
// ─── Inside the UVM agent/driver: retrieve the interface ───────────────
class apb_driver extends uvm_driver#(apb_seq_item);
virtual apb_if vif;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Retrieve from the config database — no TB-level dependencies
if (!uvm_config_db#(virtual apb_if)::get(
this, "", "apb_vif", vif))
`uvm_fatal("NO_VIF", "Virtual interface not found in config_db")
endfunction
task run_phase(uvm_phase phase);
forever begin
apb_seq_item req;
seq_item_port.get_next_item(req);
// Drive via virtual interface exactly as before
vif.cb.paddr <= req.addr;
vif.cb.pwdata <= req.data;
vif.cb.psel <= 1;
// ...
seq_item_port.item_done();
end
endtask
endclassVirtual Interface with Modport
A virtual interface can carry a modport restriction — exactly like a regular interface port. This enforces signal directions even inside the class, giving you the same compile-time safety that modport provides in static module connections.
// ─── Three virtual interface types — different restrictions ────────────
// Full interface — no direction enforcement (monitor/scoreboard)
virtual apb_if vif;
// Master modport — driver: can only drive master-side signals
virtual apb_if.master vif_master;
// TB modport — includes clocking block and safe sampling
virtual apb_if.tb vif_tb;
// ─── Classes use the appropriate restriction ───────────────────────────
class apb_driver;
virtual apb_if.tb vif; // tb modport — clocking block + enforced outputs
task drive(input logic [31:0] addr, data);
vif.cb.paddr <= addr; // ✓ paddr is output in tb modport
// vif.cb.prdata <= data; ← ✗ ERROR: prdata is input in tb modport
endtask
endclass
class apb_monitor;
virtual apb_if vif; // full interface — read every signal freely
task observe();
@(vif.cb);
if (vif.cb.psel && vif.cb.penable && vif.cb.pready)
$display("APB: addr=%h data=%h wr=%b",
vif.cb.paddr, vif.cb.pwrite?vif.cb.pwdata:vif.cb.prdata,
vif.cb.pwrite);
endtask
endclassMultiple Virtual Interfaces — Real Testbench Pattern
A real SoC testbench has many interfaces — one per bus or protocol. The TB top instantiates all of them and passes each to the appropriate driver/monitor via virtual interface handles. This complete example ties everything together.
// ─── Classes ───────────────────────────────────────────────────────────
class apb_driver;
virtual apb_if.tb vif;
function new(virtual apb_if.tb v); this.vif = v; endfunction
task run(); /* ... */ endtask
endclass
class axi_driver;
virtual axi_lite_if.master vif;
function new(virtual axi_lite_if.master v); this.vif = v; endfunction
task run(); /* ... */ endtask
endclass
class apb_monitor;
virtual apb_if vif;
function new(virtual apb_if v); this.vif = v; endfunction
task run(); /* ... */ endtask
endclass
// ─── TB top: one interface per bus, one class per role ─────────────────
module tb_top;
logic clk = 0, rst_n;
// Interface instances — live in static hierarchy
apb_if apb (.pclk(clk), .presetn(rst_n));
axi_lite_if axi (.aclk(clk), .aresetn(rst_n));
// Class instances — live on the heap
apb_driver apb_drv;
apb_monitor apb_mon;
axi_driver axi_drv;
initial begin
// Create classes, passing the matching interface
apb_drv = new(apb.tb); // APB driver gets tb modport
apb_mon = new(apb); // APB monitor gets full interface
axi_drv = new(axi.master); // AXI driver gets master modport
rst_n = 0; #20 rst_n = 1;
// Fork all components in parallel
fork
apb_drv.run();
apb_mon.run();
axi_drv.run();
join_any
$finish;
end
always #5 clk = ~clk;
endmoduleinterface vs virtual interface — Side by Side
| Feature | interface (instance) | virtual interface (handle) |
|---|---|---|
| What it is | The actual signal bundle — allocates wires | A pointer/reference to an existing interface instance |
| Where it lives | Static design hierarchy (module scope) | Anywhere — including inside a class |
| Memory | Allocated at elaboration (compile-time) | Just a pointer — minimal overhead |
| Default value | Signals initialized to X/0 | null |
| Can appear in class? | No — compile error | Yes — this is its primary use |
| Multiple handles to same instance? | N/A (one instance) | Yes — many classes can share one interface |
| Declare syntax | apb_if bus (.pclk(clk)) | virtual apb_if vif; |
Common Mistakes & How to Fix Them
// ════ MISTAKE 1: Using a null virtual interface — run-time fatal ══════
class apb_driver;
virtual apb_if vif; // ❌ never assigned
task run();
vif.cb.psel <= 1; // ← null pointer dereference at simulation
endtask
endclass
// ✅ FIX: always assign before run() and add a null-check guard
class apb_driver_ok;
virtual apb_if vif;
task run();
if (vif == null) $fatal("VIF not assigned!");
vif.cb.psel <= 1;
endtask
endclass
// ════ MISTAKE 2: Forgetting `virtual` — trying to instantiate ═════════
class apb_driver_bad;
apb_if vif; // ❌ compile error: interface instance in class
endclass
// ✅ FIX: add the `virtual` keyword
class apb_driver_good;
virtual apb_if vif; // ✅ handle, not an instance
endclass
// ════ MISTAKE 3: Assigning vif after run() starts — null window ═══════
// initial begin drv = new(); fork drv.run(); join_none drv.vif = apb; end
// ❌ run() may execute the first vif access before the assignment lands
// ✅ FIX: assign before forking run, or use Pattern 1 (constructor arg)
initial begin
apb_driver_ok drv = new();
drv.vif = apb; // ← assign first
fork drv.run(); join_none
end
// ════ MISTAKE 4: Passing the wrong modport to a driver class ══════════
// drv = new(apb.slave); // ❌ driver expects apb.tb / apb.master
// ✅ FIX: pass the modport that matches the class's role.
// Constructor signature catches mismatches at compile time:
// function new(virtual apb_if.tb v);Quick Reference — Virtual Interface at a Glance
// ── Declare a virtual interface handle ───────────────────────────────
virtual my_if vif; // full interface — no direction enforcement
virtual my_if.master vif_m; // with modport restriction
virtual my_if.tb vif_tb; // tb modport — includes clocking block
// ── In a class ────────────────────────────────────────────────────────
class my_driver;
virtual my_if.tb vif;
function new(virtual my_if.tb v); // Pattern 1: via constructor
this.vif = v;
endfunction
task run();
if (vif == null) $fatal("VIF not assigned");
@(vif.cb);
vif.cb.data <= 8'hAB; // drive via clocking block
vif.cb.valid <= 1;
endtask
endclass
// ── In TB top ─────────────────────────────────────────────────────────
my_if intf (.clk(clk), .rst_n(rst_n)); // actual interface instance
my_driver drv = new(intf.tb); // pass to class via constructor
// ── UVM config_db pattern ─────────────────────────────────────────────
// TB top: uvm_config_db#(virtual my_if)::set(null, "*", "vif", intf);
// Driver: uvm_config_db#(virtual my_if)::get(this, "", "vif", vif);Verification Usage — Virtual Interfaces Are the Spine of Every UVM Testbench
Every UVM driver, every UVM monitor, every reference model that touches DUT signals holds a virtual interface. The handle is published once by TB top via uvm_config_db, retrieved by every consumer in build_phase, and used through clocking blocks for the entire simulation. This pattern is the same across every UVM testbench in production — once you've internalised it, you can navigate any team's testbench in minutes.
// ── TB top: instantiate, publish, run ───────────────────────────
module tb_top;
logic clk;
initial begin clk = 0; forever #5 clk = ~clk; end
apb_if u_apb(.clk);
apb_slave u_dut (.apb(u_apb.slave));
initial begin
// Publish modport-qualified handles — driver and monitor get different views
uvm_config_db#(virtual apb_if.tb)::set(
null, "uvm_test_top.env.agt.drv", "vif", u_apb);
uvm_config_db#(virtual apb_if.monitor)::set(
null, "uvm_test_top.env.agt.mon", "vif", u_apb);
run_test();
end
endmodule
// ── UVM driver: get handle, never instantiate ─────────────────
class apb_driver extends uvm_driver #(apb_xact);
virtual apb_if.tb vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if.tb)::get(
this, "", "vif", vif))
`uvm_fatal("NOVIF",
"No virtual interface for driver — check tb_top config_db::set")
endfunction
task run_phase(uvm_phase phase);
forever begin
apb_xact tr; seq_item_port.get_next_item(tr);
@(vif.tb_cb);
vif.tb_cb.paddr <= tr.paddr;
vif.tb_cb.pwdata <= tr.pwdata;
vif.tb_cb.psel <= 1'b1;
@(vif.tb_cb);
vif.tb_cb.penable <= 1'b1;
while (!vif.tb_cb.pready) @(vif.tb_cb);
vif.tb_cb.psel <= 1'b0;
vif.tb_cb.penable <= 1'b0;
seq_item_port.item_done();
end
endtask
endclass
// ── UVM monitor: input-only handle, samples bus into transactions ──
class apb_monitor extends uvm_monitor;
virtual apb_if.monitor vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if.monitor)::get(
this, "", "vif", vif))
`uvm_fatal("NOVIF", "No monitor vif")
endfunction
endclassSimulation Behavior — How Virtual Interface Handles Resolve
The virtual interface lifecycle has three distinct phases. Elaboration: the actual interface instance is created inside TB top. build_phase: the handle is published to uvm_config_db and retrieved by every consumer. run_phase: consumers use the handle to read and drive bus signals. Every phase has a specific failure mode if it's skipped.
// Phase 1 — ELABORATION (before time 0)
module tb_top;
apb_if u_apb(.clk); // ← actual interface created, lives in module hierarchy
initial begin
// Phase 2 — build_phase
uvm_config_db#(virtual apb_if)::set(null, "*", "vif", u_apb);
// ← handle stored in config_db keyed by hierarchical path + "vif"
run_test();
end
endmodule
// Inside the UVM env, in build_phase:
class apb_agent extends uvm_agent;
virtual apb_if vif; // ← null until build_phase resolves it
function void build_phase(uvm_phase phase);
$display("[%0t] %m before get: vif = %s", $time,
(vif == null) ? "null" : "resolved");
// Phase 2 — retrieve into the class member
void'(uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif));
$display("[%0t] %m after get: vif = %s", $time,
(vif == null) ? "null" : "resolved");
endfunction
task run_phase(uvm_phase phase);
// Phase 3 — use the handle
@(vif.tb_cb); // crashes here if vif still null
vif.tb_cb.psel <= 1;
endtask
endclass
// Expected output at time 0:
// [0] uvm_test_top.env.agt before get: vif = null
// [0] uvm_test_top.env.agt after get: vif = resolvedWaveform Analysis — Virtual Interface Access Looks Identical to Direct Access
A virtual interface read or write produces the same waveform as the equivalent direct interface access. The simulator routes vif.tb_cb.paddr <= 32'h100; to the same storage cell as u_apb.tb_cb.paddr <= 32'h100; would. The handle is purely an indirection at source-code level; by the time the simulator generates the waveform, the handle has been dereferenced to the underlying signal.
cycle 0 1 2 3 4 5
clk _│‾│_│‾│_│‾│_│‾│_│‾│_│‾│_
tb_top.u_apb.paddr -- 100 100 100 -- -- ← driven by class-based driver
tb_top.u_apb.pwdata -- AA AA AA -- --
tb_top.u_apb.psel 0 1 1 1 0 0
tb_top.u_apb.penable 0 0 1 1 0 0
tb_top.u_apb.pready 0 0 0 0 1 --
In Verdi, browse hierarchy:
tb_top → u_apb (the interface instance, owned by the module)
├── paddr, pwdata, psel, penable, ... ← visible here
└── tb_cb, mon_cb ← clocking blocks
The driver class at uvm_test_top.env.agt.drv holds 'vif' as a handle.
The waveform shows the signal under tb_top.u_apb regardless — the class
doesn't get its own hierarchy node for the bus.Industry Insights — Virtual Interface Patterns in Production Teams
Debugging Academy — 5 Real Virtual Interface Bugs
Each lab is a real failure mode pulled from production projects. Buggy code, symptom, root cause, fix.
Forgotten config_db::set in TB Top — Null Handle at run_phase
NULL POINTERmodule tb_top;
apb_if u_apb(.clk);
apb_slave u_dut(.apb(u_apb.slave));
initial begin
// ← uvm_config_db::set(...) call FORGOTTEN
run_test();
end
endmodule
class apb_driver extends uvm_driver;
virtual apb_if vif; // ← stays null
function void build_phase(uvm_phase phase);
void'(uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif));
// No check on the return value — vif silently stays null
endfunction
task run_phase(uvm_phase phase);
@(vif.tb_cb); // crash: null pointer dereference
endtask
endclassSimulator crash at the first @(vif.tb_cb) with an error like "null reference at vif.tb_cb". No useful stack trace; the crash happens deep in run_phase. If run_phase happens to be 30 minutes into the simulation, debug starts looking at recent transactions when the actual fix is in tb_top.
config_db::get returns false when the key isn't found, but the consumer discards the return value. The vif handle stays at its initial null value; the run_phase access dereferences null.
Always check the return value: if (!uvm_config_db#(virtual apb_if)::get(...)) `` uvm_fatal("NOVIF", ...) ``. The fatal fires at time 0 with a clear message identifying which component and which vif key failed. The TB top's missing set call is obvious from the message; fix takes 10 seconds instead of 30 minutes of stepping through run_phase.
config_db Scope String Doesn't Match Consumer Path
SCOPE MISMATCH// TB top publishes for "uvm_test_top.env.driver"
uvm_config_db#(virtual apb_if)::set(
null, "uvm_test_top.env.driver", "vif", u_apb);
// Actual component path is "uvm_test_top.env.agt.drv":
class apb_env extends uvm_env;
apb_agent agt;
function void build_phase(uvm_phase phase);
agt = apb_agent::type_id::create("agt", this); // env.agt
endfunction
endclass
class apb_agent extends uvm_agent;
apb_driver drv;
function void build_phase(uvm_phase phase);
drv = apb_driver::type_id::create("drv", this); // env.agt.drv
endfunction
endclass
// Driver's config_db::get fails — wrong scope, no handleSame null-pointer crash as Lab 1, except the engineer knows there's a config_db::set in TB top. Debug becomes "why isn't the set matching the get?" — usually traced by enabling uvm_config_db::dump() at end of build_phase, which shows the published key and lets the engineer eyeball the mismatch.
The scope string in set uses an outdated or wrong path. The consumer's get with this as scope and "" as inst-name evaluates to the component's full path; if that doesn't match a published key, the get returns false. Path mismatches are common when the env hierarchy is refactored without updating the TB top's set calls.
Use wildcards aggressively in TB top: uvm_config_db#(virtual apb_if)::set(null, "*", "vif", u_apb); matches any scope. Combined with modport-qualified types, this is disambiguated enough: driver requests virtual apb_if.tb, monitor requests virtual apb_if.monitor. The wildcard means the env hierarchy can be refactored freely without TB top edits. Trade-off: harder to do published-per-component overrides — but those are rare.
Set/Get Type Mismatch — Bare vs Modport-Qualified
TYPE DRIFT// TB top publishes with bare type
uvm_config_db#(virtual apb_if)::set(null, "*", "vif", u_apb);
// Driver requests modport-qualified type
class apb_driver extends uvm_driver;
virtual apb_if.tb vif;
function void build_phase(uvm_phase phase);
if (!uvm_config_db#(virtual apb_if.tb)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "No driver vif") // ← fires
endfunction
endclass`uvm_fatal("NOVIF") fires in build_phase. Engineer confirms the set is in TB top and the scope wildcard matches — debug stalls until someone notices the type mismatch: published as virtual apb_if, requested as virtual apb_if.tb.
uvm_config_db is type-strict. The published handle's type must match the requested type exactly. Bare virtual apb_if and modport-qualified virtual apb_if.tb are different types; the lookup fails.
Two conventions, pick one and enforce it. (a) Publish per-modport: uvm_config_db#(virtual apb_if.tb)::set(...) and uvm_config_db#(virtual apb_if.monitor)::set(...) separately; consumers request the matching type. (b) Publish bare, query bare: TB top publishes virtual apb_if; every consumer requests bare and then uses .tb or .monitor at access time. Mixing the two conventions is the bug. Pick one and document it as the team standard.
Attempting to new() a Virtual Interface in a Class
BAD MENTAL MODELclass apb_driver extends uvm_driver;
virtual apb_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
vif = new(); // ← syntactic nonsense
endfunction
function void build_phase(uvm_phase phase);
// vif is "set" but null — new() on a pointer type allocates nothing
// No config_db::get either
endfunction
task run_phase(uvm_phase phase);
@(vif.tb_cb); // crash
endtask
endclassCompiles (some tools warn). Crashes at @(vif.tb_cb) with null-pointer. Engineer believes vif = new(); "created" the interface and is confused why it doesn't work.
Misunderstanding the virtual interface mental model. A virtual interface is a pointer; new() on a pointer type allocates nothing useful. Interfaces are module-world constructs that must be instantiated in a module (typically tb_top); classes only hold handles to them.
Remove the vif = new(); line entirely. Add the proper config_db::get in build_phase with the return-value check. The interface is instantiated in tb_top with apb_if u_apb(.clk);; the class receives a handle to that instance via config_db. Never instantiate an interface from a class.
Virtual Interface Held in a Transaction Object — Performance Collapse
DESIGN SMELLclass apb_xact extends uvm_sequence_item;
virtual apb_if vif; // ← transactions hold a vif?
rand bit [31:0] paddr;
rand bit [31:0] pwdata;
function void post_randomize();
// Each transaction tries to do its own config_db lookup
void'(uvm_config_db#(virtual apb_if)::get(null, "*", "vif", vif));
if (vif != null) begin
// Some transactions drive the bus directly, bypassing the driver
vif.tb_cb.paddr <= paddr;
end
endfunction
endclassRegression slows to a crawl — every transaction does its own config_db::get, which is expensive. Worse, transactions that drive the bus directly race with the driver, producing intermittent multi-driver X. Scoreboard fires occasional mismatches that the engineer attributes to the DUT.
Misuse of the transaction abstraction. Transactions are data objects — they describe what happens but don't do anything. The driver does the bus access on the transaction's behalf. Putting bus-driving logic in the transaction breaks the data/control separation that UVM's architecture depends on.
Remove the vif field and the post_randomize logic from the transaction. The transaction holds data only: rand bit [31:0] paddr; rand bit [31:0] pwdata; and the constraints. The driver holds the vif and does the bus access in run_phase. This is the UVM canonical pattern; deviations cost performance and correctness.
Interview Q&A — 12 Questions on Virtual Interfaces
Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.
A virtual interface is a handle (pointer) to an interface instance, declared inside a class. It's needed because classes cannot directly contain interface instances — interfaces are module-world (static) constructs and classes are class-world (dynamic). The virtual interface bridges the two: TB top instantiates the interface in the module world; UVM classes hold handles to it and use them to drive and sample bus signals. Without virtual interfaces, UVM couldn't exist — there'd be no way for class-based test code to interact with the DUT.
Best Practices — Virtual Interface Rules to Walk Away With
- The interface is instantiated in TB top, not in any class. Classes only hold handles to existing instances.
- Always type the virtual interface to a specific modport.
virtual apb_if.tb vif;— never bare in production code. - Match config_db set and get types exactly. Pick a convention (publish per-modport or publish-bare) and enforce it across the codebase.
- Always check
config_db::get's return value.`uvm_fatal("NOVIF", ...)on false — fast-fail at time 0 with a clear message. - Use clocking blocks for every bus access through the vif.
vif.tb_cb.signalnotvif.signal. Lint rule rejects bare access. - Bundle multiple vifs in a configuration object. One
config_db::setin TB top publishes all of them; env fetches the cfg and propagates. - Never put a virtual interface in a transaction object. Transactions hold data; components hold vifs.
- Use wildcards in TB top's scope strings.
set(null, "*", "vif", ...)— env refactors don't require TB top edits. - Print "all vifs resolved" status at end of env's build_phase. Fast feedback when a future TB top change accidentally drops a set call.
- Look at
tb_top.u_apb.signalin waveforms, not the class handle. Virtual interfaces don't appear as hierarchy nodes; the bus lives at the interface instance's location.