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.