Skip to content

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:

SystemVerilog — illegal: interface instance inside a class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✗ 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
endclass

The 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:

SystemVerilog — interface lives in tb_top, handle lives in the class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

Declaring & Using a Virtual Interface

SystemVerilog — annotated virtual interface declaration
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ① 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 direction

Four things to remember about every declaration:

  1. virtual keyword — the only difference from a regular interface port. Without virtual, the compiler treats it as an interface instantiation (which is illegal in a class). With virtual, it is a handle type.
  2. 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.
  3. 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.
  4. Access pattern — once assigned, use vif.signal_name or vif.cb.signal just 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)

SystemVerilog — pass virtual interface via new()
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 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;
endmodule

Pattern 2 — Set method / property after construction

SystemVerilog — assign virtual interface after new()
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

SystemVerilog — UVM config_db pattern (preview)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 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
endclass

Virtual 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.

SystemVerilog — virtual interface with modport restriction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 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
endclass

Multiple 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.

SystemVerilog — complete TB with multiple virtual interfaces
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 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;
endmodule

interface vs virtual interface — Side by Side

Featureinterface (instance)virtual interface (handle)
What it isThe actual signal bundle — allocates wiresA pointer/reference to an existing interface instance
Where it livesStatic design hierarchy (module scope)Anywhere — including inside a class
MemoryAllocated at elaboration (compile-time)Just a pointer — minimal overhead
Default valueSignals initialized to X/0null
Can appear in class?No — compile errorYes — this is its primary use
Multiple handles to same instance?N/A (one instance)Yes — many classes can share one interface
Declare syntaxapb_if bus (.pclk(clk))virtual apb_if vif;

Common Mistakes & How to Fix Them

SystemVerilog — virtual interface mistakes and fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ 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

SystemVerilog — virtual interface quick reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

SystemVerilog — Production virtual interface flow through a UVM env
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

Simulation 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.

SystemVerilog — Lifecycle trace showing the three phases
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 = resolved

Waveform 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.

Expected waveform — APB transaction driven from a UVM class via virtual interface
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

1

Forgotten config_db::set in TB Top — Null Handle at run_phase

NULL POINTER
Buggy Code
config_db::set forgotten in tb_top
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module 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
endclass
Symptom

Simulator 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.

Root Cause

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.

Fix

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.

2

config_db Scope String Doesn't Match Consumer Path

SCOPE MISMATCH
Buggy Code
set scope does not match the consumer hierarchy
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 handle
Symptom

Same 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.

Root Cause

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.

Fix

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.

3

Set/Get Type Mismatch — Bare vs Modport-Qualified

TYPE DRIFT
Buggy Code
Published bare, requested modport-qualified — type mismatch
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Symptom

`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.

Root Cause

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.

Fix

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.

4

Attempting to new() a Virtual Interface in a Class

BAD MENTAL MODEL
Buggy Code
new() on a virtual interface — syntactic nonsense
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class 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
endclass
Symptom

Compiles (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.

Root Cause

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.

Fix

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.

5

Virtual Interface Held in a Transaction Object — Performance Collapse

DESIGN SMELL
Buggy Code
Transaction holding its own vif handle — anti-pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class 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
endclass
Symptom

Regression 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.

Root Cause

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.

Fix

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

  1. The interface is instantiated in TB top, not in any class. Classes only hold handles to existing instances.
  2. Always type the virtual interface to a specific modport. virtual apb_if.tb vif; — never bare in production code.
  3. Match config_db set and get types exactly. Pick a convention (publish per-modport or publish-bare) and enforce it across the codebase.
  4. Always check config_db::get's return value. `uvm_fatal("NOVIF", ...) on false — fast-fail at time 0 with a clear message.
  5. Use clocking blocks for every bus access through the vif. vif.tb_cb.signal not vif.signal. Lint rule rejects bare access.
  6. Bundle multiple vifs in a configuration object. One config_db::set in TB top publishes all of them; env fetches the cfg and propagates.
  7. Never put a virtual interface in a transaction object. Transactions hold data; components hold vifs.
  8. Use wildcards in TB top's scope strings. set(null, "*", "vif", ...) — env refactors don't require TB top edits.
  9. 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.
  10. Look at tb_top.u_apb.signal in waveforms, not the class handle. Virtual interfaces don't appear as hierarchy nodes; the bus lives at the interface instance's location.