Verification

Virtual interfaces in UVM

By CourseTron Team ·  4 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: A virtual interface is a handle (a class-member pointer) to a real SystemVerilog interface instance, letting dynamic UVM class objects that are constructed at run time reach down and drive or sample the static DUT pins they otherwise could not touch.

    Every UVM newcomer hits the same wall: the testbench is a sea of classes created with new() and the design-under-test (DUT) is a lump of static RTL wired through an interface. Classes live in the dynamic world (they come and go on the heap); modules and interfaces live in the static world (elaborated once, forever). You cannot instantiate an interface inside a class, and you cannot instantiate a class inside an interface. The virtual interface is the officially sanctioned bridge between these two universes, and understanding it is the single most common UVM interview question after the factory.

    Why a plain interface will not do

    A SystemVerilog interface is a static container of signals, clocking blocks and modports. It is elaborated at time zero and bound to the DUT's ports in your top module. Because a UVM driver, monitor or agent is a class, it is constructed during the build_phase, long after elaboration. A class simply has no legal way to contain a static interface instance. What it can hold, though, is a reference to one — and that reference is declared with the keyword virtual:

    virtual dut_if vif;   // a handle, not an instance

    The word virtual here has nothing to do with virtual methods or polymorphism. It simply tells the compiler “this is a pointer to an interface instance that will be assigned at run time,” much like a class handle is a pointer to an object.

    Virtual interface: bridge from class to pins STATIC (elaborated once) DUT (RTL) interface dut_if instance u_if DYNAMIC (heap objects) uvm_driver uvm_monitor virtual dut_if vif virtual interface handle (set/get via config_db) CourseTron · coursetron.com
    The virtual interface handle lets heap-allocated class objects reach the single static interface bound to the DUT.

    The four-step connection recipe

    Connecting the static pins to the dynamic testbench is always the same ritual. Miss any step and you get the infamous null-handle crash at the first vif.signal access.

    1. Instantiate the interface in the top module and wire it to the DUT ports.
    2. Publish the handle into the UVM configuration database with uvm_config_db#(virtual dut_if)::set(...).
    3. Retrieve it inside the component's build_phase with the matching ::get(...).
    4. Use it — drive or sample signals, synchronized to a clocking block.
    // 1. top.sv
    module top;
      bit clk;
      dut_if u_if(clk);               // static instance
      dut d (.clk(clk), .data(u_if.data), .valid(u_if.valid));
    
      initial begin
        // 2. publish the handle, keyed by name "vif"
        uvm_config_db#(virtual dut_if)::set(null, "uvm_test_top.env.agt.*",
                                            "vif", u_if);
        run_test("my_test");
      end
    endmodule
    
    // 3. inside the driver
    class my_driver extends uvm_driver#(my_item);
      virtual dut_if vif;
      function void build_phase(uvm_phase phase);
        if(!uvm_config_db#(virtual dut_if)::get(this, "", "vif", vif))
          `uvm_fatal("NOVIF", "virtual interface not set for driver")
      endfunction
    endclass

    Always test the return value of config_db::get and issue a `uvm_fatal. A silently null vif is the number-one cause of “Null object access” fatals in Synopsys VCS, Cadence Xcelium and Siemens Questa simulations alike.

    Four-step connection recipe 1 Instantiate dut_if u_if(clk) top module 2 Publish config_db::set top initial block 3 Retrieve config_db::get build_phase 4 Use vif.drv_cb driver task Skip step 2 or 3 and vif stays null: check the get() return CourseTron · coursetron.com
    The four-step recipe that carries the interface handle from the top module to a UVM component.

    Driving and sampling through a clocking block

    Raw signal pokes invite race conditions. Production interfaces expose a clocking block with a defined input skew (commonly 1step) and output skew (e.g. #1), and the driver talks only to that. This gives you deterministic sample/drive timing relative to the DUT clock edge:

    task my_driver::drive(my_item it);
      @(vif.drv_cb);                 // align to clocking event
      vif.drv_cb.data  <= it.data;   // clocked, skew-protected drive
      vif.drv_cb.valid <= 1'b1;
    endtask

    See our companion piece on SystemVerilog interface and modport for how modports restrict direction and how clocking blocks pin down timing — the foundation this whole mechanism rests on. If your agents share one physical bus across several protocols, our note on UVM agent architecture shows how to scope the config_db keys so each agent grabs the correct slice.

    One handle, many interfaces: the array pattern

    A SoC verification environment rarely has one interface. A design with 8 AXI masters needs 8 virtual interface handles. Rather than hand-writing eight config_db calls, index them:

    ScenarioDeclarationTypical config_db key
    Single DUT portvirtual dut_if vif;"vif"
    N identical portsvirtual axi_if vif[8];$sformatf("vif[%0d]", i)
    Mixed protocolsseparate typed handles"apb_vif", "spi_vif"

    Common traps that cost hours

    • Type mismatch: the type in set and get (including any parameterization) must be identical, or get returns 0.
    • Scope string typos: a wrong hierarchical path in set means the component never finds its handle. Use wildcards (*) carefully.
    • Driving a wire with a procedural block: interface signals a driver pokes should be of type logic, not wire, unless a continuous assign owns them.
    • Accessing vif in new(): the handle is only valid after build_phase retrieval — never dereference it in the constructor.

    For hands-on projects, the design-services team at Avecas uses exactly this virtual-interface-plus-config_db pattern across their tapeout verification flows, which is a good sanity check that the recipe scales beyond a classroom example.

    One item, one clocked step uvm_driver (class) virtual dut_if drv_cb DUT pins (static) D @drv_cb edge: item sampled, then driven CourseTron · coursetron.com
    A transaction item is driven from the class-based driver, through the virtual interface's clocking block, onto the static DUT pins — one clocked step at a time.

    Ready to go from “it compiles” to building a full reusable agent? Our CourseTron UVM Verification track walks you through virtual interfaces, sequences, the config database and scoreboards on a real AXI DUT. Pair it with the applied labs at ChipXpert for guided practice.

    FAQ

    Why is it called a "virtual" interface and not just a pointer?

    SystemVerilog reused the virtual keyword to signal “late binding” — the handle is resolved at run time, not elaboration. It is conceptually a pointer to an interface instance, but it is not a general pointer: it can only refer to interfaces of the exact declared type.

    Can I assign the virtual interface without the config_db?

    Yes, you can directly assign driver.vif = top.u_if from the top module, but this defeats UVM reuse and hard-wires paths. The uvm_config_db keeps components location-agnostic, which is why every production methodology uses it.

    Why do I get a null-object fatal at the first signal access?

    Almost always the config_db::get failed (type or scope-string mismatch) and your code did not check its return value, so vif stayed null. Add a `uvm_fatal on a failed get to catch it at build time.

    Do monitors also need a virtual interface?

    Yes. The monitor is passive but still class-based, so it needs its own handle to sample the pins (typically through a separate monitor clocking block with input skew), feeding observed transactions to the scoreboard.

    Advertisement