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 Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
    • Reusable Testbench Architecture
      • Policy Classes
      • Abstract BFM
      • Parameterized Interfaces
      • Interface Classes
      • Layered Stimulus
    • Checking Out-of-Order Traffic
    • Assertion Depth
    • Coverage Modelling for Real Features
    • Capstone: Out-of-Order Memory Controller
    • Principal Judgment
Loading lesson

‹Reusable Testbench Architecture
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Reusable Testbench Architecture
  5. Abstract BFM

Part 9 · Principal-Level Verification · Advanced

Abstract BFM

The polymorphic-interface pattern: an abstract class API in a package, the concrete BFM class inside the interface, and an environment with no virtual interface and no width parameter.

On this page

    What a virtual interface drags in

    A driver that holds virtual bus_if #(32) vif knows three things it should not: which interface type the pins use, its parameter values, and the signal names. Every class that holds or passes that handle inherits the same knowledge. Change the width and the edit ripples through the driver, the monitor, the agent, the config object and the test.

    The abstract BFM pattern, sometimes called a polymorphic interface, removes that knowledge. The environment calls methods on an abstract class. The concrete class that implements those methods is declared inside the interface, where the pins and the parameters are already in scope.

    Diagram
    package / $unit                 interface bus_if #(W)
      ┌──────────────────────┐        ┌──────────────────────────────┐
      │ virtual class bus_bfm │◄─extends┤ class bfm_impl               │
      │   pure virtual write  │        │   write(): drives en/addr/... │
      │   pure virtual read   │        │   width(): returns W          │
      └──────────▲───────────┘        │ function bus_bfm get_bfm()    │
                 │ handle             └──────────────────────────────┘
      ┌──────────┴───────────┐
      │ class env             │   no vif, no #(W), one copy for every width
      └──────────────────────┘

    The three pieces

    1. The API: a virtual class with pure virtual tasks and functions, declared outside any interface (a package in real code). Its signatures use the widest data type the family needs, here 64 bits.

    2. The implementation: a class declared inside the interface that extends the API. It reads and drives the interface signals directly and can use the interface parameter W.

    3. The hand-off: a function in the interface returns the implementation as a base-class handle. The top module calls it hierarchically and gives the handle to the environment.

    The concrete class is a different type in every interface specialization, but nothing outside the interface ever names it. The environment only sees bus_bfm, so one environment class serves an 8-bit and a 32-bit bus without a parameter.

    Runnable example: one env, two widths

    Two memories, 8 and 32 bits wide, each behind a bus_if of matching width. The same env class writes 20 random words through each BFM, masks the expected data to the width the BFM reports, then reads every address back.

    bus_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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    // The environment sees only this API: no virtual interface, no width parameter.
    virtual class bus_bfm;
      pure virtual function int unsigned width();
      pure virtual task write(bit [7:0] addr, bit [63:0] data);
      pure virtual task read(bit [7:0] addr, output bit [63:0] data);
    endclass
    
    interface bus_if #(parameter int W = 8) (input logic clk);
      logic         we, en;
      logic [7:0]   addr;
      logic [W-1:0] wdata, rdata;
    
      // Concrete BFM lives inside the interface: it sees the pins and W directly.
      class bfm_impl extends bus_bfm;
        function int unsigned width(); return W; endfunction
        task write(bit [7:0] a, bit [63:0] d);
          @(negedge clk);
          en <= 1; we <= 1; addr <= a; wdata <= d[W-1:0];
          @(negedge clk);
          en <= 0; we <= 0;
        endtask
        task read(bit [7:0] a, output bit [63:0] d);
          @(negedge clk);
          en <= 1; we <= 0; addr <= a;
          @(negedge clk);
          en <= 0;
          d = 64'(rdata);
        endtask
      endclass
    
      bfm_impl bfm;
      // Built on first request, so no initial-block ordering is involved.
      function bus_bfm get_bfm();
        if (bfm == null) bfm = new();
        return bfm;
      endfunction
      initial begin en = 0; we = 0; end
    endinterface
    
    module mem #(parameter int W = 8) (
      input  logic clk, en, we,
      input  logic [7:0] addr,
      input  logic [W-1:0] wdata,
      output logic [W-1:0] rdata
    );
      logic [W-1:0] m [256];
      always_ff @(posedge clk)
        if (en) begin
          if (we) m[addr] <= wdata;
          else    rdata   <= m[addr];
        end
    endmodule
    
    // Written once, used for every width: no parameter, no virtual interface.
    class env;
      bus_bfm bfm;
      string  tag;
      int     errors;
      function new(string tag, bus_bfm bfm); this.tag = tag; this.bfm = bfm; endfunction
      task run(int n);
        bit [63:0] mask, exp [bit [7:0]], got, d;
        bit [7:0] a;
        mask = (bfm.width() == 64) ? '1 : (64'd1 << bfm.width()) - 1;
        for (int i = 0; i < n; i++) begin
          a = 8'(i * 7);
          d = {$urandom, $urandom};
          bfm.write(a, d);
          exp[a] = d & mask;
        end
        foreach (exp[k]) begin
          bfm.read(k, got);
          if (got !== exp[k]) begin
            errors++;
            $display("%s addr %0h: got %h exp %h", tag, k, got, exp[k]);
          end
        end
      endtask
    endclass
    

    Two timing choices keep the bench free of races. The BFM drives on the falling edge with nonblocking assignments, so the memory samples settled values at the rising edge. The read task samples rdata at the falling edge after the read cycle, half a period after the memory updated it.

    get_bfm() builds the object on first use. That removes any dependence on the order in which initial blocks in the interface and in the top module start.

    Mutant run. Hard-coding an 8-bit slice in the BFM's read (d = 64'(rdata[7:0])) still passes on the 8-bit bus and fails all 20 read-backs on the 32-bit bus. Width assumptions are exactly what this pattern moves into the interface, where W is known.

    Adding a variant

    Suppose a third project needs the same protocol with a 64-bit bus and a different wait-state rule. With this pattern the work is local: a new interface (or a new parameter value), a new implementation class inside it, and one line in the top module that hands its BFM to the environment. The environment, the scoreboard and every test stay untouched, because none of them names the interface type.

    Compare that with a parameterized environment: the new width is a new specialization of every class in the chain, and any code that stores agents of different widths in one array stops compiling. The abstract BFM trades that ripple for a one-time cost, which is designing an API general enough for every variant.

    Costs and limits

    Concern

    What to do

    API must cover every variant

    Size data arguments for the widest variant and expose width() so callers can mask.

    Monitors

    Same pattern: an abstract monitor class whose implementation inside the interface samples the pins and returns transactions.

    Clocking blocks

    Declare them in the interface; the concrete class uses them like any interface item.

    Interfaces bound into the DUT

    Works well with bind: the bound interface hands out its BFM handle, and the environment never needs its hierarchical path.

    Debug

    Stack traces show the class inside the interface instance, which helps locate which bus a failure came from.

    Common pitfall

    Declaring the abstract class inside the interface. Each interface specialization would then have its own, unrelated base type, and the environment would be back to naming a parameterized type. The base class must live outside every interface.

    Interview

    Likely question: “Your agent must work on three bus widths without a parameterized environment. How?” Answer: put an abstract BFM API in a package, implement it in a class inside the interface where W and the pins are in scope, and pass the base handle to the environment. In UVM the handle goes through uvm_config_db in place of the virtual interface.

    Check yourself Why can the concrete class inside the interface use W and the signal names without any handle?

    A class declared inside an interface is in the interface's scope, so it sees the interface parameters and signals directly, much like a task declared in that interface. Each interface instance has its own copy of that scope.

    Check yourself The environment needs the bus width to build expected values. Where should it get it?

    From the API, through a method such as width() implemented in the interface. The environment should never read a parameter of a type it does not name.

    Related topics

    • Previous: Policy Classes

    • Next: Parameterized Interfaces

    • Contracts and polymorphic containers (Part 4)

    • Interface BFM

    Previous

    233 / 259

    Next: Parameterized Interfaces

    Up next

    Up nextParameterized Interfaces

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Interface Classes
    • Layered Stimulus