UVM-07: Virtual Interfaces — VLSI Trainers
VLSI Trainers UVM Series · UVM-07
UVM Series · UVM-07

Virtual Interfaces

The two-kingdom problem — why class-based testbenches cannot directly access DUT signals, what a virtual interface is, the recommended config_db pattern for passing virtual interfaces, clocking blocks, and the most common pitfalls.

📋 The Two-Kingdom Problem

SystemVerilog has two fundamentally different worlds that cannot directly communicate with each other:

The problem: your UVM driver (dynamic class world) needs to drive DUT signals (static module world). A class object cannot have a wire or port declaration, and cannot make hierarchical references to module signals.

The Two-Kingdom Problem — and its Solution Static World (modules / interfaces) DUT module gpio_dut wire PCLK, PADDR… SV Interface apb_if (instance) Connected to DUT ports tb_top module instantiates DUT + interface + clock gen Dynamic World (UVM classes) Test Driver needs interface to drive DUT! UVM environment hierarchy pure class objects — cannot ref module signals directly virtual interface The bridge vlsitrainers.com
Figure 1 — The two-kingdom problem. The DUT and its signals live in the static module world (left). UVM drivers, monitors and other components live in the dynamic class world (right). A virtual interface is the bridge — it is a class-compatible handle that points to the static interface instance, allowing class objects to access module-world signals.

📋 What a Virtual Interface Is

A virtual interface is a SystemVerilog language construct: a variable whose type is virtual <interface_name>. It acts as a handle — a reference — to an actual interface instance in the static world. It is not a copy of the interface; it is a pointer to the original.

The word “virtual” here does not mean the same as virtual in object-oriented programming (polymorphism). It is the IEEE 1800 committee’s term for “a reference to an interface instance that can be used inside a class.” Think of it as a pointer to a module-world object, usable from the class world.

// ── Actual interface (static, in module world) ────────────
interface apb_if (
  input logic PCLK,
  input logic PRESETn
);
  logic [11:0] PADDR;
  logic        PSELx, PENABLE, PWRITE, PREADY, PSLVERR;
  logic [31:0] PWDATA, PRDATA;
  logic [3:0]  PSTRB;
endinterface

// ── Virtual interface type declaration (in class world) ───
// This is just a type — no memory allocated yet
virtual apb_if vif;           // handle — starts as null

// ── After assignment, use it to drive signals ─────────────
vif.PADDR   <= 12'h004;       // drives the actual interface signal
vif.PSELx   <= 1;
@(posedge vif.PCLK);          // waits on the interface clock
data = vif.PRDATA;             // reads from the interface
A virtual interface handle starts as null. If you try to drive signals through a null virtual interface handle, the simulator will throw a runtime error. This is the most common source of “null handle” errors in UVM testbenches. The handle must be assigned (through config_db) before run_phase begins.

📋 Declaring the SV Interface

The SystemVerilog interface declaration lives in its own .sv file. It contains all the signals for one protocol bus. Clocking blocks (covered in the next section) can also be declared inside the interface to control signal sampling timing.

// File: apb_if.sv
interface apb_if (
  input logic PCLK,
  input logic PRESETn
);
  // APB signals
  logic [11:0] PADDR;
  logic        PSELx, PENABLE, PWRITE;
  logic [31:0] PWDATA, PRDATA;
  logic [3:0]  PSTRB;
  logic [2:0]  PPROT;
  logic        PREADY, PSLVERR;

  // Clocking block for driver — synchronises signal drives
  clocking driver_cb @(posedge PCLK);
    default input #1 output #1;
    output PADDR, PSELx, PENABLE, PWRITE, PWDATA, PSTRB, PPROT;
    input  PRDATA, PREADY, PSLVERR;
  endclocking

  // Clocking block for monitor — defines sampling point
  clocking monitor_cb @(posedge PCLK);
    default input #1;
    input PADDR, PSELx, PENABLE, PWRITE, PWDATA, PSTRB, PPROT;
    input PRDATA, PREADY, PSLVERR;
  endclocking

  // Modport for driver (restricts which signals it can access)
  modport driver_mp  (clocking driver_cb,  input PRESETn);
  modport monitor_mp (clocking monitor_cb, input PRESETn);
endinterface

📋 The config_db Pattern

The recommended pattern for passing virtual interfaces through the testbench has four steps. This is the only approach that cleanly separates the static and dynamic worlds while maintaining full reusability.

Four-Step Virtual Interface Distribution Pattern ① tb_top Instantiate interface config_db::set(vif) context = null ② test.build_phase config_db::get(vif) → agent_cfg.vif = vif Assign into config obj ③ agent.build_phase get cfg from config_db config_db::set(vif) to drv and mon ④ drv/mon build_phase config_db::get(vif) assign to local handle Use in run_phase config_db — global database key: (context, path, name) → value: virtual interface handle set get then set cfg set to drv/mon get tb_top: set(null, “uvm_test_top”, “apb_vif”, apb_bus) test: get(this, “”, “apb_vif”, m_cfg.vif) agent: set(this, “m_drv”, “vif”, m_cfg.vif) driver: get(this, “”, “vif”, vif) vlsitrainers.com
Figure 2 — Four-step virtual interface distribution via config_db. ① tb_top places the interface handle into config_db using a null context. ② The test retrieves it and assigns it into the agent config object. ③ The agent pushes the handle (from the config object) back into config_db for the driver and monitor. ④ Driver and monitor each retrieve it and store in a local member variable for use in run_phase.

📋 End-to-End Flow — Complete Code

Step ① — tb_top: instantiate and set

module tb_top;
  import uvm_pkg::*;
  `include "uvm_macros.svh"

  logic clk, rst_n;
  apb_if apb_bus(.PCLK(clk), .PRESETn(rst_n));   // static instance

  gpio_dut dut(
    .PCLK(clk), .PRESETn(rst_n),
    .PADDR(apb_bus.PADDR), .PWRITE(apb_bus.PWRITE) /* ... */
  );

  initial begin
    // ① Pass virtual interface BEFORE run_test()
    uvm_config_db #(virtual apb_if)::set(
      null,             // null = from top-level static context
      "uvm_test_top",  // scope: accessible to test and all below
      "apb_vif",       // key name — test retrieves with this name
      apb_bus           // the actual interface instance handle
    );
    rst_n = 0; #20ns; rst_n = 1;
    run_test();
  end
endmodule

Step ② — test: retrieve and assign into config object

function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  m_env_cfg = gpio_env_cfg::type_id::create("m_env_cfg");
  m_env_cfg.m_agent_cfg = gpio_agent_cfg::type_id::create("m_agent_cfg");

  // ② Retrieve vif from config_db and assign into agent config
  if (!uvm_config_db #(virtual apb_if)::get(
        this,       // this component's context
        "",         // no path suffix — search from this component up
        "apb_vif", // same key as the set() call
        m_env_cfg.m_agent_cfg.vif))
    `uvm_fatal("TEST", "No virtual interface in config_db!")
endfunction

Step ③ — agent: distribute vif from config object

function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  if (!uvm_config_db #(gpio_agent_cfg)::get(
        this, "", "cfg", m_cfg))
    `uvm_fatal("AGT", "No agent config!")

  m_mon = gpio_monitor::type_id::create("m_mon", this);
  if (m_cfg.is_active == UVM_ACTIVE) begin
    m_seqr = gpio_sequencer::type_id::create("m_seqr", this);
    m_drv  = gpio_driver::type_id::create("m_drv", this);
  end

  // ③ Push vif (from config object) down to driver and monitor
  uvm_config_db #(virtual apb_if)::set(
    this, "m_mon", "vif", m_cfg.vif);
  if (m_cfg.is_active == UVM_ACTIVE)
    uvm_config_db #(virtual apb_if)::set(
      this, "m_drv", "vif", m_cfg.vif);
endfunction

Step ④ — driver/monitor: retrieve and use

class gpio_driver extends uvm_driver #(gpio_seq_item);
  `uvm_component_utils(gpio_driver)

  virtual apb_if vif;    // local handle — null until build_phase

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // ④ Get vif from config_db — fatal if not found
    if (!uvm_config_db #(virtual apb_if)::get(
          this, "", "vif", vif))
      `uvm_fatal("DRV", "No virtual interface!")
  endfunction

  task run_phase(uvm_phase phase);
    // Now safe to use vif — handle is assigned
    @(posedge vif.PCLK);
    vif.PSELx <= 1;
    // ...
  endtask
endclass

📋 Clocking Blocks

A clocking block declared inside a SystemVerilog interface defines the timing relationship between the testbench and the DUT clock. It specifies when signals are sampled (input skew) and when they are driven (output skew) relative to the clock edge. This eliminates race conditions where the testbench drives signals at exactly the same edge the DUT samples them.

// Inside apb_if — clocking block for the driver
clocking driver_cb @(posedge PCLK);
  default input  #1step   // sample 1 step before posedge
          output #1;       // drive 1 time unit after posedge
  output PADDR, PSELx, PENABLE, PWRITE, PWDATA, PSTRB;
  input  PRDATA, PREADY, PSLVERR;
endclocking

// In the driver — access through clocking block
task drive_transfer(gpio_seq_item item);
  @(vif.driver_cb);                   // wait for clock edge
  vif.driver_cb.PADDR  <= item.addr;  // drives 1ns after posedge
  vif.driver_cb.PSELx  <= 1;
  vif.driver_cb.PENABLE <= 0;
  @(vif.driver_cb);                   // next cycle
  vif.driver_cb.PENABLE <= 1;
endtask
Use clocking blocks to avoid race conditions. Without a clocking block, driving signals at @(posedge clk) is a race — the DUT samples at the same edge you drive. With a clocking block using output #1, your drives happen 1ns after the edge, giving the DUT a clean setup window. This is especially important for fast DUTs running at hundreds of MHz.
MethodSignal timingRace safe?Recommended for
Direct assignment vif.signal <= valDriven at NBA phase of current timestepRisk of race at posedgeSimple testbenches, slow DUTs
Clocking block vif.cb.signal <= valDriven #output_skew after clock edgeRace-freeProduction testbenches, all designs
Clocking block input vif.cb.signalSampled #input_skew before clock edgeStable value guaranteedMonitor signal sampling

📋 Multiple Interfaces

A DUT with multiple protocol interfaces requires one SV interface per bus, one agent per interface, and one virtual interface handle per interface in the relevant config object. Each virtual interface is placed into config_db with a unique key name.

// tb_top — two interfaces for a DUT with APB + AHB
apb_if apb_bus(.PCLK(clk), .PRESETn(rst_n));
ahb_if ahb_bus(.HCLK(clk), .HRESETn(rst_n));

initial begin
  uvm_config_db #(virtual apb_if)::set(
    null, "uvm_test_top", "apb_vif", apb_bus);
  uvm_config_db #(virtual ahb_if)::set(
    null, "uvm_test_top", "ahb_vif", ahb_bus);
  run_test();
end

// test — retrieve both, assign into respective agent configs
if (!uvm_config_db #(virtual apb_if)::get(
      this, "", "apb_vif", m_env_cfg.m_apb_cfg.vif))
  `uvm_fatal("TEST", "No APB vif!")
if (!uvm_config_db #(virtual ahb_if)::get(
      this, "", "ahb_vif", m_env_cfg.m_ahb_cfg.vif))
  `uvm_fatal("TEST", "No AHB vif!")

📋 Common Pitfalls

PitfallSymptomFix
config_db::set() after run_test()get() returns 0. Virtual interface is null in driver. Simulation crashes on first signal access.All set() calls must happen in the initial block before run_test().
Wrong path string in set()get() returns 0. The key exists in config_db but the path does not match any component that calls get().Use "uvm_test_top" (not "*" or "") in the tb_top set() for global visibility. Debug with uvm_config_db::dump().
Wrong key name in get()get() returns 0. The name string in get() must match the name in set() exactly — case-sensitive.Use constants or package parameters for key strings to avoid typos across multiple files.
Using vif before build_phase assigns itNull handle dereference. Runtime fatal. Typically in a constructor or static initialiser.Only access vif in run_phase or later. build_phase must complete before run_phase starts.
Race condition without clocking blockIntermittent failures at specific frequencies. DUT samples signals at the same edge they are driven.Use clocking blocks with output skew in the interface. Drive through vif.cb.signal <= val.
Sharing one virtual interface handle across multiple driversSignal contention. Two drivers overwriting each other’s values.Each driver must have its own virtual interface instance. Multi-master designs need separate interface instances or interface arrays.
Virtual interface is parameterisedType mismatch in config_db. virtual my_if #(32) and virtual my_if #(64) are different types.The type parameter in set() and get() must match exactly. Use a fixed parameter in the interface or specialised config objects per parameter value.

📋 Quick Reference

ItemKey fact
Two kingdomsStatic (modules/interfaces) cannot be directly accessed by dynamic (classes). Virtual interface bridges the two.
Virtual interface definitionA class-compatible variable holding a reference/handle to a static interface instance
Declarationvirtual apb_if vif; — starts as null, must be assigned before use
tb_top set() syntaxconfig_db #(virtual apb_if)::set(null, "uvm_test_top", "key", instance)
Component get() syntaxconfig_db #(virtual apb_if)::get(this, "", "key", vif)
set() must happen whenIn initial block of tb_top, before run_test()
Recommended vif storageInside agent config object — not directly in driver/monitor’s config_db path
Why config object not directKeeps driver/monitor reusable — they only need their config object, not config_db key names
Clocking block purposeDefines sampling (input skew) and drive timing (output skew) relative to clock — eliminates races
Drive with clocking blockvif.driver_cb.SIGNAL <= value — driven at specified skew after clock edge
Sample with clocking blockvalue = vif.monitor_cb.SIGNAL — sampled at specified skew before clock edge
Multiple interfacesOne interface instance per bus, one virtual interface handle per config object, unique key per bus
Debug get() failureuvm_config_db::dump() prints all entries — compare path and key with what get() expects
Null handle errorOccurs when vif is used before build_phase assigns it, or when get() silently returned 0
Scroll to Top