The abstract/concrete class approach connects a UVM testbench to a DUT or a bus functional model (BFM) without a virtual interface. The testbench defines an abstract class, a SystemVerilog virtual class that lists the operations it needs, and the hardware side defines a concrete class that implements them by calling the BFM. The testbench keeps only a handle to the abstract class, which it receives through uvm_config_db.

Virtual interfaces are the standard way to connect a class-based testbench to a design, and are the first choice. This pattern exists for the cases where an interface can't be used, most often a legacy Verilog or VHDL BFM that exists as a module and can't be turned into an interface, or a design that must be driven through tasks instead of pins.

8 min read | Intermediate

What You'll Learn

  • Explain why a class-based testbench can't call a module directly
  • Write an abstract class and a concrete class that wraps a BFM
  • Pass the concrete instance to the testbench through uvm_config_db
  • Decide when to use it instead of a virtual interface

The Problem

A class can't access the ports of a module, and a class defined in a package can't refer to a module instance by name, because the package is compiled before the modules that use it. A virtual interface solves this: an interface can be referenced from a class, so the driver gets a handle to it and drives the pins through it.

If the design's pins are already wrapped by a BFM written as a module, with tasks such as write() and read() that perform the bus cycles, a package class can't call it either. The abstract/concrete pattern bridges that gap. A class declared inside the module can see everything in the module, including the BFM instance. That class is the concrete class. The testbench uses it through a base class, the abstract class, which it can see, because it's declared in a package.

How the Pieces Connect

The abstract class lists the operations as pure virtual methods and contains no implementation. The concrete class extends it, lives inside the wrapper module, and implements each method by calling the BFM. The wrapper module creates one instance of it, and gives it to the testbench through uvm_config_db. From then on, the driver and monitor call methods on an abstract handle and never see the module.

The Abstract Class

It goes in the agent's package, next to the driver and monitor that use it. In this example the bus is a simple memory bus with write and read operations:


virtual class mem_bus_abs_c extends uvm_object;
  // The public interface: operations only, no implementation
  pure virtual task write(bit [31:0] addr, bit [31:0] data);
  pure virtual task read (bit [31:0] addr, output bit [31:0] data);
  pure virtual task wait_clocks(int unsigned n);
endclass

The class extends uvm_object here so that it can be passed through the configuration database. The Cookbook example extends uvm_component instead, so its concrete class can have a run_phase. Either works. An object is simpler when no phase code is needed.

The Concrete Class in the Wrapper Module

The wrapper module contains the BFM instance and, inside the module, the concrete class. Because the class is declared in the module's scope, its methods may call the BFM by its instance name:


module mem_bus_wrapper (/* DUT-side ports */);
  import uvm_pkg::*;
  import mem_agent_pkg::*;                       // brings in mem_bus_abs_c

  logic clk;
  mem_bus_bfm bfm (.clk(clk) /* , pins to the DUT */);   // the existing BFM module

  // The concrete class: declared inside the module, so it can see "bfm"
  class mem_bus_concr_c extends mem_bus_abs_c;
    `uvm_object_utils(mem_bus_concr_c)
    function new(string name = "mem_bus_concr_c");
      super.new(name);
    endfunction

    virtual task write(bit [31:0] addr, bit [31:0] data);
      bfm.write(addr, data);                     // proxy to the BFM's own task
    endtask
    virtual task read(bit [31:0] addr, output bit [31:0] data);
      bfm.read(addr, data);
    endtask
    virtual task wait_clocks(int unsigned n);
      repeat (n) @(posedge clk);
    endtask
  endclass

  // Create the single instance and publish it to the testbench
  mem_bus_concr_c m_concr = new("mem_bus_concr_c");
  initial uvm_config_db #(mem_bus_abs_c)::set(null, "uvm_test_top", "mem_bus", m_concr);
endmodule

The methods are simple proxies. The class adds nothing to the BFM's behaviour, it only makes it reachable from class-based code. If you have several buses of the same kind, the module can be instantiated several times, with each instance publishing its own handle under its own field name, such as mem_bus_0 and mem_bus_1.

Using It in the Testbench

The testbench fetches the handle exactly like a virtual interface. The test gets it from the database and passes it down, usually through a configuration object. The driver then holds an abstract handle:


class mem_driver extends uvm_driver #(mem_item);
  `uvm_component_utils(mem_driver)
  mem_bus_abs_c m_bus;                           // abstract handle, set from the agent's config
  // Rest of the code (constructor, build_phase getting m_bus)

  virtual task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      if (req.write) m_bus.write(req.addr, req.data);
      else           m_bus.read (req.addr, req.data);
      seq_item_port.item_done();
    end
  endtask
endclass

The driver's code doesn't change if the BFM underneath is replaced, say by one that drives a different bus, as long as a new concrete class implements the same abstract methods. The same abstract class can also be implemented by a model that talks to a virtual interface, so the same driver runs against either implementation.

Quick Check

Q: Why must the concrete class be declared inside the wrapper module rather than in a package?

Show Answer

A class in a package can't refer to a module instance, so it couldn't call bfm.write(). A class declared inside the module is in the module's scope and can name anything declared there, including the BFM instance and its tasks. That visibility is the whole reason for the pattern.

The Alternative: Two Kingdoms

The same idea can be done with the factory instead of the configuration database. This form, called "two kingdoms", makes the driver itself the abstract class and puts a concrete version of the driver in the hardware module. A factory override then replaces the abstract driver with the concrete one. It's workable, but it doesn't follow the usual route for connecting a design to a testbench, and it makes the testbench depend on an override being in place. The configuration-database form above is the recommended one.

Virtual Interface or Abstract/Concrete?

SituationUse
A new design, or an interface you can write or changeA virtual interface
A legacy BFM that exists only as a moduleAbstract/concrete, wrapping the BFM
The testbench must be portable between pin-level and emulated or transaction-level back endsAbstract/concrete: the abstract API is the same, the concrete class changes

Virtual interfaces are simpler and are supported everywhere. Add abstract/concrete classes only when you need one of the two situations in the table.

Common Mistakes

Mistake #1: Putting the concrete class in a package

It then can't see the BFM. The concrete class must be declared inside the module that contains the BFM.

Mistake #2: Setting the handle before the object exists

The set() in the module's initial block must receive a created object, not a null handle. Create the instance first, as m_concr is in the example.

Mistake #3: Mismatched types in config_db

The set() must use the abstract class as its type parameter, and the get() must use the same one. A get() for the concrete type, which the testbench cannot see, will never match.

Mistake #4: Testbench code that depends on the concrete class

If the driver refers to anything that is not in the abstract class, the pattern's benefit is lost. Keep the abstract API complete enough that the testbench needs nothing else.

Summary

In this article, you learned:

  • A class can't reference a module, so a BFM can't be called from a driver directly
  • An abstract class in the testbench defines the operations; a concrete class inside the wrapper module implements them by calling the BFM
  • The module creates one concrete instance and passes it through uvm_config_db, typed as the abstract class
  • The testbench holds only an abstract handle, so the back end can be replaced without touching it
  • Virtual interfaces remain the first choice; use this pattern for legacy BFMs and portable back ends
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5