HomeSystemVerilog VerificationDay 5 — Interfaces
DAY 5 · SV FUNDAMENTALS

SystemVerilog Interfaces & Modports — Clean Testbench Connections Explained

By EcrioniX · Updated Jun 12, 2026

A SystemVerilog interface is one of the most powerful constructs the language added over plain Verilog. Instead of listing every signal individually on every module port, an SV interface bundles them into a single named connection. Pair it with modports for direction enforcement and virtual interfaces for class-based testbenches, and you have the foundation of every professional UVM environment.

DUT uart_dut.sv INTERFACE uart_if clk, tx, rx, valid modport DUT_MP modport TB_MP TESTBENCH tb_top.sv DUT_MP TB_MP Both sides connect via the same interface instance — one port change updates everywhere

What is a SystemVerilog interface?

A SystemVerilog interface is a named group of signals (and optionally tasks, functions, and clocking blocks) that can be passed as a single port to modules and testbenches. Before interfaces, connecting a DUT with 30 signals to a testbench meant listing all 30 on the DUT port, the TB port, and every intermediate module — with no protection against typos or direction errors.

With an SV interface, you declare the signals once. The DUT takes the interface as a port, the testbench instantiates the interface and passes it to the DUT. Change a signal? Update one place. This is especially powerful in UVM, where the driver and monitor both access the same interface through virtual interface handles.

Interface declaration syntax

An interface is declared at the top level (like a module, not inside a module). It begins with interface name(ports); and ends with endinterface. Signal declarations inside are like module ports — logic, wire, and any other signal types.

What is a modport and how does it enforce signal directions?

A modport (module port view) restricts which signals a module sees from an interface and specifies their direction from that module's perspective. A UART master modport would declare tx as output and rx as input — because the master transmits on tx and receives on rx. The slave modport is the mirror: tx is input and rx is output.

When you pass an interface with a modport to a module (.uart_bus(my_if.DUT_MP)), the simulator enforces that the module only drives signals declared as output in that modport. This catches wiring mistakes at elaboration time, not hours into a confusing simulation.

What modports give youWithout modports
Direction enforced by simulatorAny module can drive any signal — silent bugs
Self-documenting: interface shows intentMust read each module's internal code
Master modport = opposite of slave modportMust coordinate manually
Different tasks/functions visible per modportAll tasks visible to everyone

What is a virtual interface and why is it needed?

Here is the fundamental problem: a SystemVerilog class is a software object — it lives in the testbench simulation memory and has no structural connection to hardware. An interface is structural — it is a physical bundle of hardware signals instantiated in the module hierarchy.

You cannot declare an interface directly inside a class because a class is not a module and cannot have structural ports. The solution is a virtual interface: a handle (pointer) to an interface instance. You declare virtual uart_if vif inside the class, and at the top of the testbench you assign driver.vif = uart_bus to point the handle at the actual interface instance. From then on, the class accesses vif.tx, vif.rx, etc. — writing through to the real interface signals.

Virtual interface assignment flow

  1. Declare interface: uart_if uart_bus(clk); at top level
  2. Instantiate DUT with the interface: uart_dut dut(.intf(uart_bus.DUT_MP))
  3. In the testbench class, declare: virtual uart_if vif;
  4. Before simulation starts: driver.vif = uart_bus; (pass handle to class)
  5. In the class: drive signals via vif.tx, sample via vif.rx

Parameterized interfaces

Interfaces can be parameterized like modules — useful for generic bus interfaces where the data width varies per use case:

interface axi_if #(parameter DATA_W = 32, ADDR_W = 32)(input logic clk, rst_n);
  logic [ADDR_W-1:0] awaddr;
  logic [DATA_W-1:0] wdata;
  logic              awvalid, awready, wvalid, wready;
  // modports ...
endinterface

Instantiate with: axi_if #(.DATA_W(64)) axi_bus(clk, rst_n); — the interface adjusts all signal widths automatically.

Complete UART interface and testbench example

uart_if.sv — Interface with modports
// ============================================================
// uart_if.sv — UART interface with master/slave modports
// EcrioniX · SV Verification Course · Day 5
// ============================================================
interface uart_if (input logic clk);

  // ---- Interface signals ----
  logic        tx;        // Transmit data (DUT drives in DUT_MP, TB drives in TB_MP)
  logic        rx;        // Receive data
  logic        tx_valid;  // TX byte ready to send
  logic        tx_ready;  // DUT ready to accept TX byte
  logic [7:0]  tx_data;   // TX byte value
  logic        rx_valid;  // RX byte available
  logic [7:0]  rx_data;   // RX byte received

  // ---- Modport: DUT perspective (DUT transmits, receives from testbench) ----
  modport DUT_MP (
    input  clk,
    input  tx_data,       // DUT reads the byte to send
    input  tx_valid,      // DUT reads: "TB wants to send a byte"
    output tx_ready,      // DUT tells TB it is ready
    output rx_valid,      // DUT signals a byte was received
    output rx_data,       // DUT outputs the received byte
    input  tx,            // DUT samples incoming serial stream
    output rx             // DUT drives outgoing serial stream — note: reversed naming for clarity
  );

  // ---- Modport: Testbench / Driver perspective ----
  modport TB_MP (
    input  clk,
    output tx_data,       // TB writes the byte to send
    output tx_valid,      // TB asserts when a byte is ready
    input  tx_ready,      // TB waits for DUT ready
    input  rx_valid,      // TB monitors received bytes
    input  rx_data,       // TB reads received byte
    output tx,            // TB drives serial stream to DUT
    input  rx             // TB monitors serial stream from DUT
  );

  // ---- Modport: Monitor (read-only view) ----
  modport MON_MP (
    input clk,
    input tx, rx, tx_valid, tx_ready, tx_data, rx_valid, rx_data
  );

  // ---- Helper task: drive a TX transfer (available via TB_MP) ----
  task automatic send_byte(input logic [7:0] data);
    @(posedge clk);
    tx_data  <= data;
    tx_valid <= 1'b1;
    @(posedge clk);
    wait(tx_ready);
    @(posedge clk);
    tx_valid <= 1'b0;
  endtask

endinterface : uart_if
tb_uart_if.sv — Testbench using virtual interface
// ============================================================
// tb_uart_if.sv — Top-level TB + Driver class using vif
// EcrioniX · SV Verification Course · Day 5
// ============================================================

// ---- UART Driver class ----
// Classes cannot have interface ports — they use a virtual interface handle
class UartDriver;
  virtual uart_if vif;  // handle to the actual interface (assigned at runtime)

  // Constructor: requires a virtual interface handle
  function new(virtual uart_if vif_in);
    this.vif = vif_in;
  endfunction

  // Drive a sequence of bytes
  task automatic drive(input logic [7:0] bytes[$]);
    foreach (bytes[i]) begin
      @(posedge vif.clk);
      vif.tx_data  <= bytes[i];
      vif.tx_valid <= 1'b1;
      $display("[Driver] Sending byte: 8'h%0h", bytes[i]);
      // Wait for DUT to accept
      do @(posedge vif.clk); while (!vif.tx_ready);
      vif.tx_valid <= 1'b0;
    end
    @(posedge vif.clk);
    $display("[Driver] All %0d bytes sent.", bytes.size());
  endtask

endclass : UartDriver

// ---- Monitor class ----
class UartMonitor;
  virtual uart_if vif;
  logic [7:0] received[$];

  function new(virtual uart_if vif_in);
    this.vif = vif_in;
  endfunction

  task automatic run();
    forever begin
      @(posedge vif.clk);
      if (vif.rx_valid) begin
        received.push_back(vif.rx_data);
        $display("[Monitor] Received byte: 8'h%0h (total: %0d)", vif.rx_data, received.size());
      end
    end
  endtask
endclass : UartMonitor

// ---- Top-level testbench module ----
module tb_uart_if;

  logic clk = 0;
  always #5 clk = ~clk;  // 100 MHz

  // ---- Instantiate the interface ----
  uart_if uart_bus(clk);

  // ---- Instantiate DUT with DUT modport ----
  // (Assume uart_dut module exists; shown as placeholder)
  // uart_dut dut (.intf(uart_bus.DUT_MP));

  // ---- Create driver and monitor — pass virtual interface handle ----
  UartDriver  drv;
  UartMonitor mon;

  initial begin
    drv = new(uart_bus);  // assign virtual interface handle at elaboration
    mon = new(uart_bus);

    // Initialize interface signals
    uart_bus.tx_valid = 0;
    uart_bus.tx_data  = 0;
    uart_bus.tx       = 1;  // UART idle high

    // Start monitor in background
    fork mon.run(); join_none

    // Drive a sequence of bytes
    begin
      automatic logic [7:0] pkt[$] = '{8'hDE, 8'hAD, 8'hBE, 8'hEF};
      drv.drive(pkt);
    end

    repeat(10) @(posedge clk);
    $display("TB complete. Monitor received %0d bytes.", mon.received.size());
    $finish;
  end

endmodule : tb_uart_if

Day 5 takeaways

Frequently Asked Questions

What is a SystemVerilog interface?

A named bundle of signals that replaces dozens of individual port connections between a DUT and testbench. Declared like a module, instantiated in the top-level TB, and passed as a single port. Can also contain modports, tasks, functions, and clocking blocks.

What is a modport in SystemVerilog?

A modport defines one module's restricted view of an interface — specifying which signals it can drive (output) and which it can read (input). A master modport is the opposite direction from a slave modport. The simulator enforces these directions, catching wiring bugs at elaboration time.

What is a virtual interface and why is it needed?

Classes in SystemVerilog are software objects — they cannot have structural ports. A virtual interface is a handle (pointer) to a real interface instance. Declare virtual uart_if vif in the class, and assign it at the TB top level: driver.vif = uart_bus. The class then drives/samples real hardware signals through the handle.

How do I pass an interface to a class in SystemVerilog?

Pass it through the constructor: function new(virtual uart_if vif_in); this.vif = vif_in; endfunction. Alternatively use the UVM config DB: uvm_config_db#(virtual uart_if)::set(null,"*","vif",uart_bus). The virtual interface handle can then be retrieved and used from any class in the hierarchy.

Previous
← Day 4: Tasks & Functions

← Full course roadmap