Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
    • Agent Architecture
      • What an Agent Owns, and What It Shows the World
      • Agent Configuration: Knobs, Config Objects and Who Owns Them
      • Mode-Gated Build: One Agent Class, Active or Passive
      • The Driver: Owning the Pins, One Transfer at a Time
      • The Monitor: Watch the Pins, Publish a Fresh Transaction
      • The Sequencer: The Agent's Stimulus Socket
      • Monitor Implementation Detail: Sampling, Races and X
      • Interface Abstraction: Virtual Interfaces vs Abstract BFMs
    • Reusable VIP: Active, Passive and Protocol Agents
    • Error and Fault Injection
    • Agent Patterns: Slaves, Reset, Interrupts and Pipelines
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Agent Architecture
  1. Topics
  2. UVM Verification
  3. UVM Agents & Protocol VIP
  4. Agent Architecture
  5. Interface Abstraction: Virtual Interfaces vs Abstract BFMs

Part 6 · Agents & Protocol IP · Intermediate

Interface Abstraction: Virtual Interfaces vs Abstract BFMs

How class code reaches the pins: virtual interfaces through config_db, the typing pain of parameterized interfaces, and the abstract-class BFM pattern used by emulation-friendly benches.

On this page

    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?

    The idea: a phone number, or a remote control

    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.

    The picture: two ways to reach the pins

    Comparison: with a virtual interface the driver accesses apb_if signals directly; with an abstract BFM the driver calls write on an abstract handle, the concrete implementation declared inside the interface drives the pins. In both cases the top module passes the handle through config_db.
    Left: the bench's style, where the class holds a virtual interface and touches signals itself. Right: the abstract-BFM style, where the class holds an abstract-class handle and calls transaction-level methods implemented inside the interface. Either handle reaches the class through config_db from the top module.

    Build it

    Virtual interfaces through config_db: the bench's top

    top.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // 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
    endmodule

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

    Parameterized interfaces: the typing pain

    Suppose the interface grows parameters so one agent can serve several bus widths:

    apb_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 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;
      // ...
    endclass

    Every 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 abstract BFM: no interface type in class code

    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.

    apb_bfm_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // 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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 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
    endtask

    The 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).

    Why emulation-friendly benches use it

    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

    virtual apb_if

    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.

    Watch it run

    The model above prints one line when every read-back matches:

    Diagram
    PASS: 2 registers written and read back through the abstract BFM handle

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

    What breaks: the 64-bit interface nobody could find

    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:

    Diagram
    UVM_FATAL dma_tb.sv(...) @ 0: uvm_test_top.env.apb.drv [NOVIF] apb_if not set for this driver

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

    Interview

    "How does a class-based driver get access to DUT signals? Have you used anything other than a virtual interface?"

    A weak answer: "Through a virtual interface from config_db." Correct, and it stops where the interesting part begins.

    A strong answer: "Classes can't hold wires, so the top module puts a virtual interface handle in config_db at the agent's path, and the driver and monitor get it in build_phase with a fatal if it's missing. The config_db type must match exactly, which hurts with parameterized interfaces: each specialization is a different type, and the parameters spread through every class. The alternative is an abstract BFM: an abstract class with write and read methods, a concrete implementation declared inside the interface, and only the abstract handle in config_db. Class code then has no interface type at all, and on an emulator the class calls a transaction-level method instead of touching pins every cycle."

    Expect the follow-ups: Why does a vif get with the wrong parameters fail silently until the fatal? (the type is part of the config_db key, so the get simply finds no match) and What do you lose with an abstract BFM? (signal visibility in class code, more boilerplate, and some tools' support for classes in interfaces).

    Check yourself The top sets 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.

    Check yourself In the model, why does the interface hand out its BFM through 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.

    Check yourself A team wants one APB agent for 32- and 64-bit buses and no abstract classes. What is the common alternative to parameterizing every class?

    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.

    Related topics

    • Agent architecture: all lessons

    • Reusable VIP

    • config_db paths and wildcards

    • Advanced factory: parameterized classes

    Previous

    101 / 280

    Next: Reusable VIP: Active, Passive and Protocol Agents

    Up next

    Up nextReusable VIP: Active, Passive and Protocol Agents

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • What an Agent Owns, and What It Shows the World
    • Agent Configuration: Knobs, Config Objects and Who Owns Them
    • Mode-Gated Build: One Agent Class, Active or Passive