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
endmodule
a=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
endmodule
count 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 input so 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
endmodule
received 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
endmodule

Read @(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.

  1. Declare the interface with its signals, optional clock port, modports, and any tasks.
  2. Instantiate it once in the top module: counter_if cif(clk);.
  3. Pass the right modport view to the DUT: counter dut(cif.dut);.
  4. Let the testbench drive and observe through the same handle.
ConceptKeywordWhat it buys you
Signal bundleinterfaceOne declaration shared by DUT and testbench
Direction controlmodportCompile-time enforcement of who drives what
Transactionstask in interfaceTests speak in operations, not pin wiggles
Race-free timingclockingDefined 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.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD