SystemVerilog · Module 8
Introduction to Interfaces
Why interfaces exist, signal bundling, the elimination of long port lists, and the backbone abstraction of every modern UVM testbench.
Module 8 · Page 8.1
Why port lists become unmanageable, how interfaces bundle signals into a single named connection, and how to write, instantiate, and use your first SystemVerilog interface. This page covers the problem interfaces solve, every part of an interface declaration, instantiation patterns, embedded tasks, the interface-vs-module-vs-struct decision, UVM/TB integration, five debugging labs from production projects, twelve interview questions, and ten best-practice rules.
The Problem Interfaces Solve
Imagine a simple APB bus. It has 10 signals: pclk, presetn, paddr, psel, penable, pwrite, pwdata, prdata, pready, and pslverr. Now your design has a master, a slave, a monitor, a driver, a scoreboard, and a testbench top. Every one of them needs all 10 signals.
Without interfaces, here is what your port list looks like for every single module:
// You write this SAME list in the master, slave, monitor, driver, and TB top
module apb_slave (
input logic pclk,
input logic presetn,
input logic [31:0] paddr,
input logic psel,
input logic penable,
input logic pwrite,
input logic [31:0] pwdata,
output logic [31:0] prdata,
output logic pready,
output logic pslverr
);
// ... and again in every module that uses APB
endmoduleNow what happens when you add a new signal to the bus? You find and update every module's port list. Miss one and you have a compile error — or worse, a silent X. In a real project with dozens of modules, this is a constant source of bugs and rework.
- Repeated port lists. The same 10-signal list is copy-pasted into every module. Adding or renaming one signal requires touching every file.
- Direction confusion. The master drives
paddras an output; the slave receives it as input. Two modules, two different port declarations — easy to get them backwards. - Wire explosion in TB top. The testbench top must declare all individual wires and connect them one-by-one to every module. Ten signals × five modules = 50 wire declarations.
- No reuse across projects. The bus protocol definition is scattered across every port list. There is no single source of truth for "what signals does APB have".
Interfaces solve all four problems by bundling the signals into a single named construct that you declare once and pass as a single connection everywhere.
What is an Interface?
An interface is a named bundle of signals — like a module, but its purpose is to carry connections between modules rather than to implement logic. You declare the signals inside it once, give the interface a name, and then pass that single name as a port to any module that needs those signals.
Think of it as a cable. An HDMI cable bundles dozens of wires — video, audio, power, control — into one connector. You plug one end into the TV, the other into the source. You do not manually route each wire separately. A SystemVerilog interface is exactly that cable, defined in software.
Declaring an Interface — Every Part Explained
The syntax is almost identical to a module. Replace module / endmodule with interface / endinterface.
// ① interface keyword + name + optional ports (usually just the clock)
interface apb_if (input logic pclk, presetn);
// ② Signal declarations — no input/output here, just logic
logic [31:0] paddr;
logic psel;
logic penable;
logic pwrite;
logic [31:0] pwdata;
logic [31:0] prdata;
logic pready;
logic pslverr;
// ③ modport: defines directions for each side (covered in 8.2)
modport master (input pclk, presetn, prdata, pready, pslverr,
output paddr, psel, penable, pwrite, pwdata);
modport slave (input pclk, presetn, paddr, psel, penable, pwrite, pwdata,
output prdata, pready, pslverr);
endinterface // ← always endinterface, never endmoduleWhat each part means
① The interface keyword opens the declaration. Ports in the port list are usually the clock and reset — signals driven from outside (not by either side of the bus). They can be passed straight through to the internal signal declarations.
② Signal declarations inside an interface have no direction — they are not input or output here. Direction is specified per-module via modport. Without a modport, every signal is bidirectional by default.
③ The modport clauses define which signals each module sees as input vs output. The master module uses apb_if.master and sees prdata as input and paddr as output — exactly right. The slave sees the opposite. One interface, two views.
Instantiating an Interface & Connecting Modules
An interface is instantiated like a module — a single line in the parent. That instance is then passed to each sub-module as a port. From that module's perspective, it has one port that gives it access to the entire signal bundle.
module tb_top;
logic clk = 0;
logic rst_n;
// ── Step 1: Instantiate the interface ────────────────────────────
apb_if apb (.pclk(clk), .presetn(rst_n)); // one line replaces 10 wires
// ── Step 2: Connect modules via the interface ─────────────────────
apb_master u_master (.bus(apb.master)); // master uses master modport
apb_slave u_slave (.bus(apb.slave)); // slave uses slave modport
apb_monitor u_mon (.bus(apb)); // monitor uses full interface
// ── Clock generation ─────────────────────────────────────────────
always #5 clk = ~clk;
initial begin
rst_n = 0; #20 rst_n = 1;
end
endmoduleInside the master — how it receives the interface
// ── Option A: Interface port with modport specified in the declaration ─
module apb_master (apb_if.master bus); // modport enforced here
always_ff @(posedge bus.pclk) begin
if (!bus.presetn) begin
bus.paddr <= 0;
bus.psel <= 0;
bus.penable <= 0;
end else begin
bus.paddr <= 32'h4000_0000; // access via dot notation
bus.psel <= 1;
bus.penable <= 1;
bus.pwrite <= 1;
bus.pwdata <= 32'hA5A5_A5A5;
end
end
endmodule
// ── Option B: Generic interface port (modport not enforced) ────────────
module apb_monitor (apb_if bus); // full interface — can read everything
always @(posedge bus.pclk) begin
if (bus.psel && bus.penable && bus.pready)
$display("APB transfer: addr=%h data=%h write=%b",
bus.paddr, bus.pwrite ? bus.pwdata : bus.prdata, bus.pwrite);
end
endmoduleBefore and After — Side by Side
Here is the same testbench top — first without interfaces, then with. Read both. The difference makes the value of interfaces immediately obvious.
Without interfaces — 30 wire declarations and connections
module tb_top_old;
// 10 wires declared individually
logic clk, rst_n;
logic [31:0] paddr;
logic psel, penable, pwrite;
logic [31:0] pwdata, prdata;
logic pready, pslverr;
// Master: 10 connections
apb_master u_master (
.pclk(clk), .presetn(rst_n), .paddr(paddr), .psel(psel),
.penable(penable), .pwrite(pwrite), .pwdata(pwdata),
.prdata(prdata), .pready(pready), .pslverr(pslverr)
);
// Slave: 10 more connections (same list, reversed directions)
apb_slave u_slave (
.pclk(clk), .presetn(rst_n), .paddr(paddr), .psel(psel),
.penable(penable), .pwrite(pwrite), .pwdata(pwdata),
.prdata(prdata), .pready(pready), .pslverr(pslverr)
);
// Monitor: 10 MORE — total: 30 connections. Adding a signal = 30 changes.
endmoduleWith interfaces — 1 interface instance, 3 clean connections
module tb_top_new;
logic clk = 0, rst_n;
// One interface instantiation replaces all 10 wires
apb_if apb (.pclk(clk), .presetn(rst_n));
// One connection per module — not ten
apb_master u_master (.bus(apb.master));
apb_slave u_slave (.bus(apb.slave));
apb_monitor u_mon (.bus(apb));
// Adding a signal to the bus: change ONLY the interface declaration.
// Zero changes needed here or in any module.
endmoduleTasks and Functions Inside an Interface
Interfaces can contain tasks and functions — these are protocol-level operations that any module connected via the interface can call. This is one of the most powerful and underused features of SystemVerilog interfaces. You write the APB transfer logic once, inside the interface, and call it from the driver, monitor, or any other TB component.
interface apb_if (input logic pclk, presetn);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready, pslverr;
// ── Task: perform one APB write ──────────────────────────────────
task automatic write(
input logic [31:0] addr,
input logic [31:0] data
);
@(posedge pclk);
paddr = addr; pwdata = data;
psel = 1; pwrite = 1;
@(posedge pclk);
penable = 1;
wait (pready);
@(posedge pclk);
psel = 0; penable = 0;
endtask
// ── Task: perform one APB read ───────────────────────────────────
task automatic read(
input logic [31:0] addr,
output logic [31:0] data
);
@(posedge pclk);
paddr = addr; psel = 1; pwrite = 0;
@(posedge pclk);
penable = 1;
wait (pready);
data = prdata;
@(posedge pclk);
psel = 0; penable = 0;
endtask
modport master (input pclk, presetn, prdata, pready, pslverr,
output paddr, psel, penable, pwrite, pwdata,
import write, read); // ← tasks visible through modport
endinterface
// Inside the driver — calling the interface task
module apb_driver (apb_if.master bus);
initial begin
bus.write(32'h4000_0000, 32'h1234_5678); // one call, full APB transfer
logic [31:0] rdata;
bus.read(32'h4000_0000, rdata);
$display("Read back: %h", rdata);
end
endmoduleInterface vs Module vs Struct — When to Use Which
These three constructs can all group signals, but they serve different purposes.
| Feature | interface | module | struct |
|---|---|---|---|
| Purpose | Bundle signals between modules | Implement hardware logic | Group data fields in memory |
| Has logic? | Optional (tasks, functions, always) | Yes — primary purpose | No — data only |
| Synthesisable? | Signals inside are — tasks are not | Yes | Yes (packed struct) |
| modport support? | Yes — defines per-module directions | No | No |
| Passed as port? | Yes — as a single port | No — connected by name | Yes — as a packed data type |
| Use it for | Bus protocols, DUT↔TB connections | RTL — counters, FSMs, datapaths | Packet headers, register fields |
Simple rule: if it connects two modules and carries dynamic signals during simulation, use an interface. If it groups fixed data fields (like a register map entry or a packet), use a struct.
What the Rest of Module 8 Covers
This page was the introduction. Here is the complete roadmap for Module 8:
- 8.2 — Modport — Restricting Port Directions. How modport enforces correct signal directions for each side of a bus. Why it prevents accidental bidirectional connections. Named vs anonymous modports.
- 8.3 — Clocking Blocks. Race-free testbench sampling using clocking blocks inside interfaces. Input/output skews, the
##Ncycle-based delay operator, and why clocking blocks run in the Reactive region. - 8.4 — Virtual Interfaces. How class-based testbench components access interfaces using virtual interface handles. Why static interface handles do not work inside classes and how virtual interfaces fix that.
- 8.5 — Interface Arrays & Parameterised Interfaces. Arrays of interface instances for multi-channel designs. Parameterising bus widths and depths directly in the interface declaration.
Quick Reference — Interface Syntax at a Glance
// ── Declare an interface ──────────────────────────────────────────────
interface my_if (input logic clk, rst_n);
logic [7:0] data;
logic valid, ready;
// modport: restricts directions for each user
modport source (input clk, rst_n, ready, output data, valid);
modport sink (input clk, rst_n, data, valid, output ready);
endinterface
// ── Instantiate in TB top ─────────────────────────────────────────────
my_if bus (.clk(clk), .rst_n(rst_n));
// ── Connect modules ───────────────────────────────────────────────────
producer u_prod (.intf(bus.source)); // modport enforced
consumer u_cons (.intf(bus.sink)); // modport enforced
monitor u_mon (.intf(bus)); // full interface — no modport
// ── Module port declaration options ──────────────────────────────────
module producer (my_if.source intf); // modport in port list
module monitor (my_if intf); // full interface, no restriction
// ── Access signals inside a module ────────────────────────────────────
intf.data = 8'hAB;
intf.valid = 1;
wait (intf.ready);
// ── Interface task (defined in interface, called from module) ─────────
task automatic send(input logic [7:0] d);
// ... protocol logic
endtask
intf.send(8'h42); // call from any connected moduleVerification Usage — Interfaces as the TB/DUT Seam
In a UVM testbench the interface is the single physical connection point between the test code and the design. The DUT exposes the bus as a port; the testbench builds the same interface at top level; UVM components grab a virtual interface handle from the config database and drive the bus through it. Without interfaces, every signal would need its own virtual-interface-equivalent and the config_db lookup would be a nightmare.
// ── Step 1: declare the interface once, in a shared file ─────────
interface apb_if(input logic clk);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready;
endinterface
// ── Step 2: TB top declares the bus and wires DUT to it ─────────
module tb_top;
logic clk;
initial begin clk = 0; forever #5 clk = ~clk; end
apb_if apb(.clk);
apb_slave u_dut (.apb(apb));
initial begin
uvm_config_db#(virtual apb_if)::set(null, "*", "vif", apb);
run_test();
end
endmodule
// ── Step 3: UVM driver pulls the virtual interface handle ──────
class apb_driver extends uvm_driver;
virtual apb_if vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "No apb_if found in config_db")
endfunction
task run_phase(uvm_phase phase);
forever begin
apb_xact tr;
seq_item_port.get_next_item(tr);
vif.paddr <= tr.paddr;
vif.pwdata <= tr.pwdata;
vif.pwrite <= tr.pwrite;
vif.psel <= 1'b1;
@(posedge vif.clk);
vif.penable <= 1'b1;
@(posedge vif.clk);
while (!vif.pready) @(posedge vif.clk);
vif.psel <= 1'b0;
vif.penable <= 1'b0;
seq_item_port.item_done();
end
endtask
endclassSimulation Behavior — How Interfaces Connect at Elaboration
The elaborator treats an interface instantiation as a structural connection — exactly like a module instantiation, but with the special property that its signals are accessible from every connected module. There is no run-time machinery behind the abstraction; signals are wired structurally at elaboration and the simulator's signal database has them under the interface instance's hierarchical scope.
interface bus_if;
logic [7:0] data;
logic valid;
initial $display("[%0t] %m elaborated", $time);
endinterface
module producer(bus_if bus);
initial $display("[%0t] %m connected — bus path = %m", $time, bus);
endmodule
module tb;
bus_if u_bus();
producer u_p(.bus(u_bus));
endmodule
// Expected $display output at time 0:
// [0] tb.u_bus elaborated
// [0] tb.u_p connected — bus path = tb.u_bus
//
// Note: the interface has its own initial block and its own hierarchical
// node (tb.u_bus). It is treated identically to a sibling module.Waveform Analysis — Interface Signals in the Viewer
Interface signals appear in the waveform viewer under the interface instance's hierarchical scope, indistinguishable from any other signal. Open Verdi, click into tb.u_apb, and you see paddr, pwdata, psel, … each with its own trace. They can be sampled, forced, dumped, and covered like any other signal — the interface is purely a syntactic grouping at the port boundary; the runtime treats each signal independently.
cycle 0 1 2 3 4 5 6
clk _│‾│_│‾│_│‾│_│‾│_│‾│_│‾│_│‾│_
tb.u_apb.paddr -- 100 100 100 100 -- -- ← setup + access phase
tb.u_apb.pwdata -- AA AA AA AA -- --
tb.u_apb.pwrite -- 1 1 1 1 -- --
tb.u_apb.psel 0 1 1 1 1 0 0 ← active during setup+access
tb.u_apb.penable 0 0 1 1 1 0 0 ← active in access phase
tb.u_apb.pready 0 0 0 0 1 -- -- ← slave ready terminates
Reading guide:
- Cycle 1: SETUP (psel=1, penable=0) — addr/data/control valid
- Cycle 2: ACCESS (psel=1, penable=1) — slave starts processing
- Cycle 4: PREADY raised — transfer completes; signals deassert next edge
In Verdi, browse to tb.u_apb in the hierarchy panel; all 7 signals
appear together. Right-click → "Add to wave" adds the entire bundle.
This is the practical payoff of interfaces in debug.Industry Insights — Interface Practices in Production Teams
Debugging Academy — 5 Real Interface Bugs
Each lab is a real failure mode pulled from production projects. Buggy code, symptom, root cause, fix.
Interface Instance Not Driven by Any Module — Floating Bus
UNDRIVEN BUSmodule tb;
apb_if u_apb(.clk);
// DUT connects to interface
apb_slave u_dut(.apb(u_apb));
// TB forgot to instantiate the driver / agent
// Interface signals are never driven by anyone
initial begin
#100ns;
if (u_dut.last_addr != 32'hABCD) $error("DUT didn't see address");
end
endmoduleAll interface signals show as X in the waveform from time 0 onward. The DUT sees X on psel, penable, etc., and either hangs at reset or its outputs all go to X. The error message is misleading because the test never even started.
An interface instance with no driving module is purely a passive bundle of X-initialised storage. The TB declared the bus but forgot to instantiate either a UVM agent or a stimulus driver that writes to u_apb.* signals.
Always confirm interface signals are driven by adding an early-time-zero assertion: initial #1 assert (!$isunknown(u_apb.psel)) else $fatal("APB never initialised");. Better: use a UVM monitor with a "no traffic after N cycles" timeout that fires loud and clear when nothing happens. Best: have the testbench framework instantiate a default-empty-stimulus driver that at least drives idle values onto the bus during reset.
Multi-Driver Conflict — Two Modules Driving the Same Interface Signal
MULTI-DRIVERinterface apb_if(input logic clk);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready;
endinterface
module tb;
apb_if u_apb(.clk);
apb_master u_drv(.apb(u_apb)); // drives paddr, pwdata, etc.
apb_master u_drv2(.apb(u_apb)); // ← ALSO drives the same signals!
apb_slave u_dut(.apb(u_apb));
endmodulepaddr, pwdata, etc. show X in the waveform whenever both drivers are active. Without modports, the simulator allows both drivers to write the same wire; the resolved value is X. Tests pass when only one driver is doing anything; fail randomly when both fire near the same edge.
An interface signal can be driven by any module that connects to it. Without modport restrictions, the language doesn't prevent two writers from stomping on each other. The bug is invisible at elaboration; it surfaces only when both drivers are concurrently active.
Use modports (covered in 8.2) to restrict who can drive what. The interface declares a master modport with output direction on the address / data / control signals; connect at most one module via that modport. The compiler then errors at elaboration if a second module attaches to the master modport. Lint tools (Spyglass multi-driver) catch this even before elaboration.
Interface Signal Width Mismatch After Refactor
SIZE DRIFT// Refactor: AXI bus widened from 64 to 128 in the interface
interface axi_if(input logic clk);
logic [127:0] wdata; // was [63:0]
logic [127:0] rdata; // was [63:0]
// ... other signals
endinterface
// Consumer hardcoded a 64-bit local buffer:
module data_path(axi_if axi);
logic [63:0] reg_buf;
always_ff @(posedge axi.clk) reg_buf <= axi.wdata; // silent truncation
endmoduleDUT silently truncates the upper 64 bits of every write. Test cases that only exercise the lower bits pass; tests that target the upper half fail. The error pattern (failures on the most-significant bits but not the least-significant) is the giveaway.
The interface declaration changed but a consumer's internal signal didn't. SystemVerilog's implicit width conversion silently truncates the wider assignment to fit the narrower target. No compile error, only a warning that gets suppressed in build logs.
Two-step. (1) Parameterise the interface (covered in 8.5): interface axi_if #(parameter int DW = 128)(...);. Consumers declare their internal widths in terms of the same parameter: logic [DW-1:0] reg_buf;. Both sides change in lockstep when the width changes. (2) Enable width-mismatch as an error in CI (VCS +lint=PCWM-L -error=PCWM-L, Questa -pedanticerrors). Any future width refactor surfaces every truncation at the next build.
Interface Task with Race Against External Driver
DELTA-CYCLE RACEinterface apb_if(input logic clk);
logic psel, penable, pwrite, pready;
logic [31:0] paddr, pwdata, prdata;
// Helper task inside the interface
task automatic write(input logic [31:0] a, d);
@(posedge clk);
paddr = a; // blocking assign on interface signal
pwdata = d;
psel = 1;
// ...
endtask
endinterface
// External driver also writes the same signals in always_ff
module ext_driver(apb_if apb);
always_ff @(posedge apb.clk)
if (en) apb.paddr <= other_addr; // non-blocking, also drives paddr
endmodulepaddr sometimes carries the task's value, sometimes the external driver's, depending on simulator scheduling order in the Active region. Test results are non-deterministic across simulator versions.
The interface task uses blocking assigns (Active region) on a signal that an external module drives via non-blocking (NBA region). When both fire at the same edge, the result depends on which side the scheduler runs first.
Apply the "one driver per signal" rule rigorously: the task and the external module cannot both drive paddr. Use modports to declare which module owns the signal, and structure the testbench so only one source ever writes it. If both must contribute, use a multiplexer signal in the interface and have each source drive its own input, with mux-select picking the actual paddr.
Interface Used in RTL Synthesis Path — Tool Drops Tasks Silently
SIM/SYNTH GAPinterface apb_if(input logic clk);
logic psel, penable, ...;
// Helper task used by both TB and an RTL bridge
task automatic single_write(input logic [31:0] a, d);
@(posedge clk); paddr <= a; pwdata <= d; psel <= 1;
@(posedge clk); penable <= 1;
while (!pready) @(posedge clk);
psel <= 0; penable <= 0;
endtask
endinterface
// RTL bridge calls the task internally:
module dma_bridge(apb_if.master apb);
always @(posedge axi_req)
apb.single_write(axi_addr, axi_data); // ← synthesis WARNS, drops
endmoduleRTL simulation passes. Synthesis "succeeds" with a buried warning: "task call ignored in synthesis context." The gate-level netlist never asserts psel or penable at all. Silicon-level DMA writes are dropped. Bug surfaces at SoC validation as "DMA never reaches the slave."
Tasks containing timing controls (@, #, wait) are not synthesisable. The synthesis tool drops them and warns; the warning lives in a 50000-line log and gets ignored. The simulation behaviour and the synthesised behaviour diverge silently.
Two layered defences. (1) Put interface tasks behind a verification guard: `ifndef SYNTHESIS task ... endtask `endif — synthesis can't even see the task. (2) RTL bridges should implement the protocol with an always_ff state machine, not by calling a TB task. The task stays for testbench use; RTL uses the explicit FSM. (3) Promote "task-call-in-synth" warning to error in your synthesis flow; refuse to ship a netlist that triggers it.
Interview Q&A — 12 Questions on Interfaces
Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.
An interface is a named bundle of signals (and optionally tasks/functions) that encapsulates a communication protocol. It solves the "long port list" problem of Verilog: instead of declaring 30+ AXI ports on every module and wiring all 30 at every instantiation, you declare them once in interface axi_if;, then every module accepts axi_if axi as a single port and every parent wires .axi(u_axi). Eliminates typos at the bus boundary; makes spec changes (a new signal) a one-place edit.
Best Practices — Interface Rules to Walk Away With
- One interface per protocol, not per block. Define
axi_ifonce for the entire SoC. Per-block interfaces inevitably drift. - Naming convention is contractual. Interface type =
{protocol}_if; instance ={role}_{protocol}_if; virtual handle =vif. Enforce in code review. - Add a header contract comment. Protocol name, spec version, modport list, parameter contract. The first 20 lines of every interface file.
- Always declare a monitor modport. All signals inputs, no drives — locks down passive observers and prevents the "monitor drove the bus" bug class.
- Parameterise widths in the interface, not the consumers. Covered in 8.5. Width changes propagate from one place.
- Don't bundle unrelated signals. Two protocols = two interfaces. The bus stays clean.
- Guard tasks behind
`ifndef SYNTHESIS. Or move them entirely to a verification package. Synthesis silently drops timing-control tasks. - Save waveform groups for each interface. Verdi/DVE group definitions live in the repo. Debug starts faster.
- Add a time-0 assertion that signals are not X.
initial #1 assert (!$isunknown(intf.psel));— catches floating-bus / no-driver mistakes immediately. - Use modports the moment you have two connected modules. Don't wait until a multi-driver bug bites; declare master/slave/monitor on day one (8.2).