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.
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.
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.
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 you | Without modports |
|---|---|
| Direction enforced by simulator | Any module can drive any signal — silent bugs |
| Self-documenting: interface shows intent | Must read each module's internal code |
| Master modport = opposite of slave modport | Must coordinate manually |
| Different tasks/functions visible per modport | All tasks visible to everyone |
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.
uart_if uart_bus(clk); at top leveluart_dut dut(.intf(uart_bus.DUT_MP))virtual uart_if vif;driver.vif = uart_bus; (pass handle to class)vif.tx, sample via vif.rxInterfaces 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.
// ============================================================
// 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 — 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_ifvirtual uart_if vif) is a handle to a real interface, used inside classes that cannot have structural ports.driver.vif = uart_bus.interface name #(parameter ...) — adapt to different data widths cleanly.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.
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.
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.
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.