SystemVerilog Interface: Modport & Clocking Blocks
As soon as a design grows past a handful of pins, the port list of your testbench starts to look like a phone book. Every signal between the design-under-test (DUT) and the testbench has to be declared, instantiated, and wired by hand, and every time the spec changes you edit the same signal name in four places. A SystemVerilog interface solves exactly this problem: it is a named bundle of signals, plus the rules for who drives them and when. This tutorial builds that idea from first principles with code you can run, moving from a plain signal group to modports, clocking blocks, and a full DUT-to-testbench connection.
What an interface actually is
An interface is a container, much like a module, but its purpose is to hold signals rather than logic. You declare it once, instantiate it like any component, and then connect it to both the DUT and the testbench. Both sides see the same set of nets, so there is a single source of truth for the pin list. Add a signal to the interface and both ends inherit it automatically.
Start with the smallest possible example: a combinational adder. The interface groups three signals; the DUT reads two and drives one; the testbench pokes values and reads the result. Notice that the top module is named tb and contains everything.
interface adder_if;
logic [7:0] a;
logic [7:0] b;
logic [8:0] sum;
endinterface
module adder(adder_if aif);
always_comb aif.sum = aif.a + aif.b;
endmodule
module tb;
adder_if aif();
adder dut(aif);
initial begin
aif.a = 8'd10;
aif.b = 8'd20;
#1;
$display("a=%0d b=%0d sum=%0d", aif.a, aif.b, aif.sum);
aif.a = 8'd200;
aif.b = 8'd100;
#1;
$display("a=%0d b=%0d sum=%0d", aif.a, aif.b, aif.sum);
$finish;
end
endmodulea=10 b=20 sum=30 a=200 b=100 sum=300
Three things are worth naming. The interface is instantiated with adder_if aif();, exactly like a module with an empty port list. Inside the DUT, every signal is reached with dot notation: aif.sum, aif.a. And because the testbench and the DUT share aif, assigning aif.a in the testbench is immediately visible to the always_comb inside the adder. One bundle, two users.
Giving signals a direction with modport
The adder works, but it is permissive: nothing stops the DUT from accidentally driving aif.a, or the testbench from writing aif.sum. In real verification you want the compiler to enforce who drives what. That is the job of a modport (module port). A modport is a named view of the interface that fixes the direction of each signal from the perspective of the component using it.
Here is a 4-bit counter. The interface now carries a clock into the bundle through its own port, and declares two modports: dut (the counter drives count, reads the rest) and tb (the mirror image). The counter is written against counter_if.dut, so if it ever tried to write rst, elaboration would fail.
interface counter_if(input logic clk);
logic rst;
logic en;
logic [3:0] count;
modport dut (input clk, input rst, input en, output count);
modport tb (input clk, output rst, output en, input count);
endinterface
module counter(counter_if.dut cif);
always_ff @(posedge cif.clk) begin
if (cif.rst)
cif.count <= 4'd0;
else if (cif.en)
cif.count <= cif.count + 4'd1;
end
endmodule
module tb;
logic clk = 0;
always #5 clk = ~clk;
counter_if cif(clk);
counter dut(cif.dut);
initial begin
cif.rst = 1;
cif.en = 0;
@(posedge clk);
cif.rst = 0;
cif.en = 1;
repeat (5) @(posedge clk);
$display("count after 5 enabled cycles = %0d", cif.count);
cif.en = 0;
@(posedge clk);
$display("count with en=0 = %0d", cif.count);
$finish;
end
endmodulecount after 5 enabled cycles = 5 count with en=0 = 5
The clock enters the interface as a port (counter_if cif(clk);), which keeps the clock generation in the testbench where it belongs. The DUT connects with counter dut(cif.dut);, selecting the dut view. The tb modport is declared for symmetry; the testbench here reaches signals directly, which is common and perfectly legal. The point of the modport is intent made enforceable: direction is no longer a comment, it is a rule the tool checks.
Why direction matters
- It catches a whole class of bugs at compile time, such as two drivers fighting over one net.
- It documents the protocol: a reader sees instantly which side owns each signal.
- It lets the same interface serve many roles; a monitor modport can make every signal
inputso a passive checker can never disturb the DUT.
Putting stimulus inside the interface
An interface can hold more than wires. It can carry tasks and functions, which is how you package a protocol so the testbench talks in transactions instead of toggling pins. Below, the interface owns a send task that performs a tiny handshake: drive data on the falling edge, let the DUT sample it on the rising edge. Driving and sampling on opposite edges removes any race, so the result is deterministic.
interface bus_if(input logic clk);
logic valid;
logic [7:0] data;
modport dut (input clk, input valid, input data);
task automatic send(input logic [7:0] d);
@(negedge clk);
valid = 1'b1;
data = d;
@(posedge clk);
@(negedge clk);
valid = 1'b0;
endtask
endinterface
module sink(bus_if.dut b);
always_ff @(posedge b.clk)
if (b.valid)
$display("received data = %0d", b.data);
endmodule
module tb;
logic clk = 0;
always #5 clk = ~clk;
bus_if b(clk);
sink dut(b.dut);
initial begin
b.valid = 0;
b.data = 0;
b.send(8'd42);
b.send(8'd99);
repeat (2) @(posedge clk);
$display("done");
$finish;
end
endmodulereceived data = 42 received data = 99 done
The testbench now reads as b.send(42) and b.send(99), with no edge handling in sight. The protocol lives in one place; if the handshake changes, you fix the task, not every test. This encapsulation is the stepping stone to class-based verification environments, where drivers reach into exactly this kind of interface handle.
Clocking blocks: sampling and driving without races
Edge-juggling works, but a clocking block formalizes it. A clocking block ties a group of signals to a clock and specifies input skew (how long before the edge you sample) and output skew (how long after the edge you drive). This closes the gap between the clean 0/1 view the testbench wants and the real propagation happening in the DUT, and it is the standard way to write race-free stimulus. The block below samples gnt one time step before the edge and drives outputs two time units after it.
interface cpu_if(input logic clk);
logic req;
logic gnt;
logic [7:0] addr;
clocking cb @(posedge clk);
default input #1step output #2;
output req;
output addr;
input gnt;
endclocking
modport tb (clocking cb, input clk);
endinterface
module tb_demo;
logic clk = 0;
always #5 clk = ~clk;
cpu_if cif(clk);
initial begin
cif.cb.req <= 1'b0;
cif.cb.addr <= 8'h00;
@(cif.cb);
cif.cb.req <= 1'b1;
cif.cb.addr <= 8'hA5;
@(cif.cb);
if (cif.cb.gnt)
$display("grant received after driving addr");
$finish;
end
endmoduleRead @(cif.cb) as “wait for the clocking block’s edge.” Signals are accessed as cif.cb.req, which means “the clocking-block view of req,” carrying the skew with it. Outputs use non-blocking assignment so they are scheduled, not applied immediately. This example is shown for structure rather than a captured run, because clocking-block timing semantics vary between simulators; treat it as the pattern you will reuse inside a real verification environment.
The connection, end to end
Every example follows the same assembly, and that repetition is the whole point.
- Declare the
interfacewith its signals, optional clock port, modports, and any tasks. - Instantiate it once in the top module:
counter_if cif(clk);. - Pass the right modport view to the DUT:
counter dut(cif.dut);. - Let the testbench drive and observe through the same handle.
| Concept | Keyword | What it buys you |
|---|---|---|
| Signal bundle | interface | One declaration shared by DUT and testbench |
| Direction control | modport | Compile-time enforcement of who drives what |
| Transactions | task in interface | Tests speak in operations, not pin wiggles |
| Race-free timing | clocking | Defined sample and drive skew around a clock |
Master these four and your testbenches shrink, your port lists stop lying, and your protocols live in exactly one place. That is the real value of the SystemVerilog interface: not fewer keystrokes, but a single, enforceable definition of how two sides of a design agree to talk.
Quick check
Why use an interface instead of listing signals in the port map?
An interface declares the signal bundle once and shares it between the DUT and testbench. Adding or changing a signal happens in a single place, instead of editing the same name across every module port list, which removes a common source of wiring bugs.
What does a modport enforce?
A modport fixes the direction (input/output) of each interface signal as seen by the component that uses it. Selecting a view such as counter_if.dut lets the compiler flag an illegal drive, for example if the DUT tried to write a signal it is only supposed to read.
Why drive stimulus on the falling edge when the DUT samples on the rising edge?
Driving and sampling on opposite clock edges guarantees the data is stable before it is captured, eliminating the read-write race you would risk if both happened on the same edge. The result is deterministic output across runs.
What problem do clocking blocks solve that plain edge-waiting does not?
A clocking block attaches signals to a clock with explicit input and output skew, so sampling happens a defined time before the edge and driving a defined time after it. This formalizes race-free timing instead of relying on manual negedge/posedge juggling.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses