Skip to content

Interfaces

Why not just use ports?

A DUT with a moderately complex bus (say, AXI) can have 30+ signals. Connecting that with individual module ports means every level of hierarchy between a testbench and the DUT has to redeclare and repropagate all 30+ signals. An interface bundles them into one named group that's passed around as a single item:

interface axi_if (input bit clk);
  logic        awvalid, awready;
  logic [31:0] awaddr;
  logic        wvalid, wready;
  logic [31:0] wdata;
  // ... the rest of the AXI channel signals
endinterface

A module or class-based component now just takes one axi_if connection instead of dozens of individual ports.

Modports

A modport declares which signals are inputs vs. outputs from a given component's point of view — the same interface looks different to a driver (which drives awvalid/awaddr and samples awready) than to a monitor (which only ever samples, never drives):

interface axi_if (input bit clk);
  logic awvalid, awready;
  logic [31:0] awaddr;

  modport driver_mp  (output awvalid, awaddr, input awready, clk);
  modport monitor_mp (input awvalid, awaddr, awready, clk);
endinterface
class axi_driver extends uvm_driver #(axi_txn);
  virtual axi_if.driver_mp vif;   // only sees the driver's view
  // ...
endclass

Using the wrong direction through a modport (e.g. a monitor trying to drive a signal) is a compile-time error, not a runtime bug — this is the actual value of modports: turning a class of connectivity mistakes into something caught before simulation even starts.

Clocking blocks

A clocking block declares a sampling/driving skew relative to a clock edge, which solves race conditions between the testbench and the DUT that would otherwise depend on simulator event-ordering (notoriously nondeterministic across tools/versions for signals that change exactly on a clock edge):

interface axi_if (input bit clk);
  logic awvalid, awready;
  logic [31:0] awaddr;

  clocking drv_cb @(posedge clk);
    output awvalid, awaddr;   // drive slightly after the edge
    input  awready;           // sample slightly before the edge
  endclocking

  modport driver_mp (clocking drv_cb, input clk);
endinterface
task drive_addr(bit [31:0] addr);
  vif.drv_cb.awvalid <= 1;
  vif.drv_cb.awaddr  <= addr;
  @(vif.drv_cb);              // wait for next clocking-block edge
endtask

Default skew

A clocking block with no explicit #1step/#delay uses a default skew (input: #1step, i.e. sampled just before the edge; output: #0, driven right at the edge) that's correct for the overwhelming majority of synchronous testbench code — you rarely need to override it unless you're deliberately modeling setup/hold-adjacent timing.

virtual interface — the bridge between classes and hardware

Interfaces are instantiated in a module (typically the top-level testbench), but drivers/monitors are classes, which can't directly instantiate or reference module-level constructs. A virtual interface is a class-compatible handle to an already-instantiated interface:

// in the testbench top module:
axi_if dut_if(.clk(clk));
initial uvm_config_db#(virtual axi_if)::set(null, "*", "vif", dut_if);
// in a UVM component's build_phase:
if (!uvm_config_db#(virtual axi_if)::get(this, "", "vif", vif))
  `uvm_fatal("NOVIF", "virtual interface not set")

This uvm_config_db set/get pair is the standard, near-universal pattern for getting a real interface handle from the static module world into the dynamic class world — covered in more depth in Testbench Architecture.