SystemVerilog Series · SV-19

SystemVerilog Series — SV-19: Interfaces — VLSI Trainers
SystemVerilog Series · SV-19

Interfaces

Named signal bundles, ports in interfaces, modports for direction control and task/function access, specify blocks, tasks and functions inside interfaces (import, export, extern forkjoin), parameterised interfaces, virtual interfaces, and hierarchical access to interface objects.

🔗 What Interfaces Solve

In Verilog-2001, the communication between design blocks is described by long, repetitive port lists. Adding a signal to a bus means editing every module declaration and every instantiation site. SystemVerilog interfaces solve this by encapsulating a bundle of nets and variables as a named type that passes through a port as a single item.

At higher levels of abstraction, interfaces also encapsulate the functionality of a communication protocol — tasks and functions — so that a module need only call a.read(addr) rather than manually toggling protocol signals. Swapping the interface for a different-abstraction version leaves the connected modules unchanged.

  • Signal bundling — replace a long port list with one interface port.
  • Direction control — modports assign per-module view of directions.
  • Protocol encapsulation — tasks/functions in the interface perform bus transactions.
  • Abstract/reusable testbenches — virtual interfaces decouple test code from specific signal paths.

📄 Interface Syntax and Instantiation

// Basic interface declaration
interface my_ifc;
  // ... nets, variables, tasks, functions, clocking blocks, modports
endinterface : my_ifc   // optional closing name

// With a clock port
interface simple_bus (input bit clk);
  // ... signals
endinterface

// Instantiation (same syntax as a module)
simple_bus sb_intf(clk);          // one instance
myinterface #(100) scalar1;      // parameterised instance
myinterface #(100) vector[9:0];   // array of 10 instances

// Connecting an interface to a module — three equivalent forms:
memMod mem(sb_intf, clk);            // positional
cpuMod cpu(.b(sb_intf), .clk(clk)); // named
memMod mem2(.*);                    // implicit .* (when port names match)

// Modules can be declared/instantiated IN interfaces?
// NO — modules cannot be declared or instantiated inside interfaces.
// Interfaces CAN be instantiated hierarchically inside modules.

📋 Named Bundle — simple_bus

Without interface — repetitive ports

module memMod(
  input bit       req, clk, start,
  input logic[1:0]mode,
  input logic[7:0]addr,
  inout wire[7:0] data,
  output bit      gnt, rdy);
  // same list repeated on cpuMod and top
endmodule

With interface — single port

interface simple_bus;
  logic      req, gnt;
  logic[7:0] addr, data;
  logic[1:0] mode;
  logic      start, rdy;
endinterface

module memMod(simple_bus a, input bit clk);
  always @(posedge clk) a.gnt <= a.req;
endmodule
module top;
  logic clk = 0;
  simple_bus sb_intf();           // instantiate interface
  memMod mem(sb_intf, clk);       // connect by position
  cpuMod cpu(.b(sb_intf), .clk(clk)); // connect by name
endmodule

// When port and interface names match — .* works too
module memMod (simple_bus sb_intf, input bit clk); ...
endmodule
memMod mem(.*);   // sb_intf and clk auto-connected

📋 Generic Interface Ports

A module can accept any interface type by using the keyword interface as a placeholder. The actual interface is specified at instantiation time. Generic interface ports require ANSI-style port declarations and cannot use implicit .* connections.

// Generic interface — accepts any interface
module memMod (interface a, input bit clk); ... endmodule
module cpuMod (interface b, input bit clk); ... endmodule

module top;
  simple_bus sb_intf(); // any interface type
  memMod mem (.a(sb_intf), .clk(clk)); // named connection required for generic port
  cpuMod cpu (.b(sb_intf), .clk(clk));
endmodule

// Can mix .* with explicit for the generic port
memMod mem (.*, .a(sb_intf));   // .* handles clk; .a explicit for generic
Generic interface cannot use .* alone. The .* connection cannot infer a generic interface port because there is no type information to match. Always connect generic interface ports explicitly using .port_name(interface_instance).

📋 Ports in Interfaces

Signals declared inside an interface are shared among all connected modules. Signals declared as ports of the interface can additionally be connected externally — making them accessible from outside the interface when it is instantiated.

// Simple interface with one port (clk shared externally)
interface simple_bus (input bit clk);
  logic      req, gnt;
  logic[7:0] addr, data;
endinterface

module memMod(simple_bus a);       // clk available as a.clk
  always @(posedge a.clk) a.gnt <= a.req;
endmodule

// Two interface instances sharing the same external clk
module top;
  logic clk = 0;
  simple_bus sb_intf1(clk);   // both share the same clk
  simple_bus sb_intf2(clk);
  memMod mem1(.a(sb_intf1));
  cpuMod cpu1(.b(sb_intf1));
  memMod mem2(.a(sb_intf2));
  cpuMod cpu2(.b(sb_intf2));
endmodule

Modports

A modport declares the signal directions as seen from a particular module role. Without a modport, all interface signals are accessible as inout (nets) or ref (variables). Modports add direction restrictions and — crucially — control which tasks and functions are visible through the connection.

interface i2;
  wire a, b, c, d;
  modport master (input a, b, output c, d); // master drives c,d; reads a,b
  modport slave  (output a, b, input c, d);  // slave drives a,b; reads c,d
endinterface

// Used in module header
module m (i2.master i); ... endmodule  // i2 with master directions
module s (i2.slave  i); ... endmodule

module top;
  i2 i();
  m u1(.i(i));   // interface instance connected to module with master modport
  s u2(.i(i));   // interface instance connected to module with slave modport
endmodule

📋 Modport in Module Header vs Port Connection

The modport can be specified either in the module’s port declaration or in the instantiation’s port connection. Both are equivalent. If specified in both places, the two modport names must be identical.

Modport in the module header

// Modport fixed at module definition time
module memMod(simple_bus.slave a);
  // always sees slave directions
endmodule

memMod mem(.a(sb_intf)); // no modport in connection

Modport in the port connection

// Module accepts any modport — direction specified at instantiation
module memMod(simple_bus a);
  // access all signals
endmodule

// Instantiation specifies modport
memMod mem(sb_intf.slave);  // slave modport applied here
cpuMod cpu(sb_intf.master); // master modport applied here

Nested modports — hierarchical interface

interface i1;
  interface i3;
    wire a, b, c, d;
    modport master (input a, b, output c, d);
    modport slave  (output a, b, input  c, d);
  endinterface
  i3 ch1(), ch2();
  modport master2 (ch1.master, ch2.master); // modport of nested interfaces
endinterface

// Modport restriction: all names used must be declared by the SAME interface.
// Cannot reference names from an enclosing interface or from nowhere.

📋 Modport Expressions

Like named port expressions on modules, modport entries can use array slices, struct members, or expressions as explicitly-named ports — visible only through that modport connection.

interface I;
  logic [7:0] r;
  const int x = 1;
  bit R;
  // .P maps to r[3:0] in modport A, to r[7:4] in modport B
  modport A (output .P(r[3:0]), input .Q(x), R);
  modport B (output .P(r[7:4]), input .Q(2),   R);
endinterface

module M (interface i);
  initial i.P = i.Q; // P and Q mean different bits depending on modport
endmodule

module top;
  I i1;
  M u1(i1.A);  // u1.i.P → i1.r[3:0], i1.i.Q → i1.x (=1)
  M u2(i1.B);  // u2.i.P → i1.r[7:4], u2.i.Q → constant 2
  initial #1 $display("%b", i1.r); // displays 00010010
endmodule

Modports and Clocking Blocks

A modport can include clocking blocks for synchronous testbench access alongside asynchronous DUT modports.

interface A_Bus(input bit clk);
  wire req, gnt;
  wire [7:0] addr, data;

  clocking sb @(posedge clk);
    input  gnt;
    output req, addr;
    inout  data;
    property p1; req ##[1:3] gnt; endproperty
  endclocking

  modport DUT (input  clk, req, addr, output gnt, inout data);
  modport STB (clocking sb);                  // synchronous testbench
  modport TB  (input gnt, output req, addr, inout data); // async TB
endinterface

program T (A_Bus.STB b1, A_Bus.STB b2);
  assert property (b1.p1);               // assert property from inside program
  initial begin
    b1.sb.req <= 1;                       // synchronous drive via clocking block
    wait(b1.sb.gnt == 1);
    b1.sb.req <= 0;
    b2.sb.req <= 1;
    wait(b2.sb.gnt == 1);
  end
endprogram

📋 Interfaces and Specify Blocks

When an interface is connected as a module port, each signal in the interface becomes an available timing-check terminal. Directions from the modport (or default inout) determine input/output role in the specify block. ref ports cannot be used as terminals.

interface itf;
  logic c, q, d;
  modport flop (input c, d, output q);
endinterface

module dtype (itf.flop ch);
  always_ff @(posedge ch.c) ch.q <= ch.d;
  specify
    (posedge ch.c => (ch.q+:ch.d)) = (5, 6);
    $setup(ch.d, posedge ch.c, 1);
  endspecify
endmodule

Tasks and Functions in Interfaces

Tasks and functions defined directly inside an interface are available to all connected modules, enabling transaction-level modelling without knowing the underlying signal details.

interface simple_bus (input bit clk);
  logic req, gnt;
  logic [7:0] addr, data;
  logic [1:0] mode;
  logic start, rdy;

  task masterRead(input logic[7:0] raddr);
    // ... set up req, addr; wait for gnt; read data
  endtask

  task slaveRead;
    // ... respond to req; drive data
  endtask
endinterface

// cpuMod calls the interface method directly
module cpuMod(interface b);
  always @(posedge b.clk)
    if(instr == read) b.masterRead(raddr);  // call interface task
endmodule

📋 Import and Export via Modports

Modports can import tasks/functions defined in the interface (making them callable by the connected module) or export tasks/functions defined in the connected module (making them callable by other modules through the interface).

import — interface tasks available to modules

modport slave (
  input req, addr, mode, start, clk,
  output gnt, rdy, ref data,
  import task slaveRead(),
         task slaveWrite());
// Module connected with slave modport
// can call slaveRead() and slaveWrite()
// through the interface

export — module tasks visible through interface

modport slave (
  input req, addr, mode, start, clk,
  output gnt, rdy, ref data,
  export task Read(),
         task Write());
// The slave module MUST define
// a.Read and a.Write tasks.
// Other modules call them via the interface.
// Module defines the exported task using interface.task_name syntax
module memMod(interface a);
  task a.Read;    // defines Read for this specific interface connection
    // ... read implementation
  endtask
  task a.Write;
    // ... write implementation
  endtask
endmodule

// cpuMod calls the slave's exported Read/Write through the interface
module cpuMod(interface b);
  always @(posedge b.clk)
    if(instr == read) b.Read(raddr);   // calls memMod's implementation
endmodule

// Master: import requires full prototype
modport master( ..., import task Read(input logic [7:0] raddr),
                     task Write(input logic [7:0] waddr));
Export task must be defined by the connected module. If the modport contains an exported task and the connected module does not define it, an elaboration error occurs. Similarly, if the module’s task prototype doesn’t exactly match the modport’s export prototype, elaboration fails. Import uses just the identifier or a prototype; export always requires the connected module to provide the implementation.

🔂 extern forkjoin — Multiple Exports

Normally only one module can export a given task. When multiple instances of the same module connect to one interface (e.g. two memory banks), use extern forkjoin to declare the task — calling it from the interface executes all connected implementations concurrently.

interface simple_bus (input bit clk);
  int slaves = 0;
  extern forkjoin task countSlaves();   // each connected module runs it
  extern forkjoin task Read (input logic[7:0] raddr);
  extern forkjoin task Write(input logic[7:0] waddr);

  modport slave (..., export Read, Write, countSlaves);

  initial begin
    slaves = 0;
    countSlaves;               // runs mem1.a.countSlaves AND mem2.a.countSlaves
    $display("slaves = %d", slaves);  // = 2
  end
endinterface

module memMod #(parameter int minaddr=0, maxaddr=0) (interface a);
  task a.countSlaves(); a.slaves++; endtask
  task a.Read(input logic[7:0] raddr);
    if(raddr >= minaddr && raddr <= maxaddr) ... // only active memory responds
  endtask
  task a.Write(input logic[7:0] waddr); ... endtask
endmodule

module top;
  simple_bus sb_intf(clk);
  memMod #(0,127)   mem1(sb_intf.slave);
  memMod #(128,255) mem2(sb_intf.slave);
  cpuMod            cpu(sb_intf.master);
endmodule

// Disable behaviour:
// disable sb_intf.Read    → disables BOTH mem1 and mem2's Read tasks
// disable mem1.a.Read     → disables ONLY mem1's Read task

📋 Parameterised Interfaces

Interfaces support parameters the same way modules do — widths and types can be parameterised, with defaults overridden at instantiation.

interface simple_bus #(AWIDTH = 8, DWIDTH = 8) (input bit clk);
  logic             req, gnt;
  logic [AWIDTH-1:0] addr;
  logic [DWIDTH-1:0] data;
  modport master(..., import task masterRead(input logic[AWIDTH-1:0] raddr));
endinterface

module top;
  simple_bus               sb_intf(clk);          // default: 8-bit addr and data
  simple_bus #(.DWIDTH(16)) wide_intf(clk);      // 16-bit data bus
  memMod  mem (sb_intf.slave);
  cpuMod  cpu (sb_intf.master);
  memMod  memW(wide_intf.slave);   // 16-bit wide memory
  cpuMod  cpuW(wide_intf.master);  // 16-bit wide cpu
endmodule

📋 Virtual Interfaces

A virtual interface is a variable that holds a reference to an interface instance. It allows the same task or class method to operate on different interface instances at different times — decoupling testbench logic from specific signal paths.

// Declare a virtual interface variable
virtual SBus bus;    // default value: null — must be initialised before use

// Assign to a real interface instance
SBus s1(), s2();
bus = s1;            // bus now refers to s1
bus = s2;            // now refers to s2
bus = null;          // detach

// Reusable transactor — independent of which device it talks to
class SBusTransactor;
  virtual SBus bus;          // virtual interface as class property
  function new(virtual SBus s); bus = s; endfunction
  task request(); bus.req <= 1'b1; endtask
  task wait_for_bus(); @(posedge bus.grant); endtask
endclass

module top;
  SBus s[1:4] ();          // 4 interface instances
  initial begin
    SBusTransactor t[1:4];
    t[1] = new(s[1]);       // each transactor bound to a different interface
    t[2] = new(s[2]);
    // test t[1:4]
  end
endmodule

Virtual interface restrictions

  • Must be initialised before use — accessing a null virtual interface is a fatal runtime error.
  • Allowed operations: assignment (=), equality (==/!=), comparison with null.
  • Not permitted as ports, interface items, or union members.
  • Once initialised, all interface components accessible via dot notation.
  • Component access only in procedural statements — not in continuous assignments or sensitivity lists.
  • To drive nets via virtual interface, the interface must provide a clocking block or a driver from a continuous assignment within the interface.
typedef virtual interface: it is common to typedef virtual SBus VI; to give the virtual interface type a shorter name, especially when passing as task arguments: task do_it(VI v);

Virtual Interfaces with Clocking Blocks

interface SyncBus(input bit clk);
  wire a, b, c;
  clocking sb @(posedge clk);
    input a; output b; inout c;
  endclocking
endinterface

typedef virtual SyncBus VI;   // shorthand type alias

task do_it(VI v);
  if(v.sb.a == 1)     v.sb.b <= 0;       // synchronous access via clocking block
  else               v.sb.c <= ##1 1;
endtask

module top;
  bit clk;
  SyncBus b1(clk), b2(clk);
  initial begin
    VI v[2] = {b1, b2};  // array of virtual interfaces
    repeat(20) do_it(v[$urandom_range(0,1)]);  // randomly pick an interface
  end
endmodule

// With modport + virtual for abstract synchronous models:
// typedef virtual A_Bus.STB SYNCTB;
// task request(SYNCTB s); s.sb.req <= 1; endtask

📋 Access to Interface Objects

All objects in an interface are always accessible via hierarchical reference, regardless of whether a modport is in use. When a modport is specified, port reference access is restricted to the modport’s listed items. However, hierarchical access still works for everything.

interface ebus_i;
  integer  I;          // not in modport mp
  typedef enum {Y,N} choice;
  choice   Q;
  parameter True = 1;
  modport mp(input Q);  // only Q is accessible through modport
endinterface

module sub(interface.mp i);
  typedef i.choice yes_no;  // import type from interface — hierarchical OK
  yes_no P;
  assign  P = i.Q;          // port reference — Q is in modport, OK

  initial Top.s1.Q = i.True;   // hierarchical reference — always works
  initial Top.s1.I = 0;        // hierarchical reference to I — OK
  // i.I = 0;                  // ILLEGAL: I not in modport mp
endmodule
Modport restricts port-reference access — not hierarchical access. Type declarations (typedef), parameters, and any object can always be reached via hierarchical name from any scope where the interface is visible. Modport only restricts what can be accessed using the port variable (i.name) — it does not hide anything from hierarchical paths (Top.s1.name).

📋 Quick Reference

Interface capabilities at a glance

FeatureHow to useKey point
Signal bundleinterface name; logic …; endinterfaceAll signals default inout/ref access
Interface portinterface name(input clk); …Port signal shared externally via clk
Named interface portmodule m(my_ifc port)Must connect exact type
Generic interface portmodule m(interface port)Accepts any interface; named connection only
Modport signal dirsmodport mp(input a, output b)Directions from the module’s perspective
Modport in headermodule m(ifc.mp port)Fixed at definition
Modport at instancem u1(ifc_inst.mp)Applied at instantiation
Import taskmodport mp(…, import task t())Interface-defined task callable by module
Export taskmodport mp(…, export task t())Module must define task; others call it via interface
forkjoin exportextern forkjoin task t()Multiple modules export same task; all run concurrently
Parameterisedinterface i #(W=8)(input clk)Override with #(.W(16)) at instantiation
Virtual interfacevirtual ifc_type varHolds reference to interface instance; decouples testbench code
Coming next: SV-20 covers Functional Coverage — covergroup declarations, coverage points, automatic and explicit bins, cross coverage, coverage options, and procedural sampling.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top