A modport list defines signal directions inside an interface, restricting how a particular module is allowed to access that interface's signals. The keyword modport means the directions are declared as if looking from inside the module using that view.

7 min read | Intermediate Level

What You'll Learn

  • What a modport is, and the driver-conflict problem it solves
  • The basic syntax for declaring one or more modports inside an interface
  • The difference between a named port bundle and a connecting port bundle, and when a module gets its modport view
  • How a module can accept a generic interface and still receive a specific modport view at connection time
  • How a modport and a clocking block work together in the same interface, and why some signals (like reset) are usually left out of the clocking block

Syntax


modport  [identifier]  (
	input  [port_list],
	output [port_list]
);

Below is an interface myInterface with a few signals and two modport declarations. dut0 states that ack and sel are inputs, and gnt and irq0 are outputs, to whatever module uses that modport. dut1 declares the mirror image: gnt and irq0 are inputs, and ack and sel are outputs, for any module using it instead:


interface 	myInterface;
	logic 	ack;
	logic 	gnt;
	logic 	sel;
	logic 	irq0;

	// ack and sel are inputs to the dut0, while gnt and irq0 are outputs
	modport  dut0 (
		input 	ack, sel,
		output 	gnt, irq0
	);

	// ack and sel are outputs from dut1, while gnt and irq0 are inputs
	modport  dut1 (
		input 	gnt, irq0,
		output 	ack, sel
	);
endinterface

Example of a Named Port Bundle

In this style, the design takes the required, correct modport definition directly from the interface type named in its own port list. The testbench only needs to provide the whole interface object:


module dut0  ( myinterface.dut0  _if);
	...
endmodule

module dut1  ( myInterface.dut1 _if);
	...
endmodule

module tb;
	myInterface 	_if;
	dut0  	d0 	( .* );
	dut1 	d1 	( .* );
endmodule

dut0's port list references myinterface.dut0 (lowercase "i"), but the interface was declared as myInterface (capital "I") above — SystemVerilog identifiers are case-sensitive, so these are two different names. As written, dut0 would fail to compile because no interface type named myinterface exists. It should read myInterface.dut0, matching dut1's declaration directly below it.

Example of a Connecting Port Bundle

In this style, the design simply accepts whatever directional view it's handed — the testbench is responsible for supplying the correct modport:


module dut0  ( myinterface  _if);
	...
endmodule

module dut1  ( myInterface _if);
	...
endmodule

module tb;
	myInterface 	_if;
	dut0  	d0 	( ._if (_if.dut0));
	dut1 	d1 	( ._if (_if.dut1));
endmodule

The same case-sensitivity issue shows up again here — dut0's port list should read myInterface _if, matching the actual declared interface name (and matching dut1 just below it), not myinterface _if.

Quick Check

Q: In the named port bundle style versus the connecting port bundle style, which one determines the modport view: the design's own port list, or the testbench's instantiation?

Show Answer

In the named port bundle style, the design's own port list fixes the modport (myInterface.dut0) — the testbench just hands over the whole interface object. In the connecting port bundle style, the design accepts a generic view of the interface, and the testbench decides which modport to apply at instantiation (._if(_if.dut0)).

What Is the Need for a Modport?

Nets declared within a plain interface are inout by default, so any module connected to the same net can either drive values onto it or read values from it — there are no restrictions on the direction of value propagation. You could end up with an X on the net because both the testbench and the design are driving two different values onto the same interface net at once. Special care would otherwise be needed by the testbench writer to avoid this. A modport avoids the problem inherently, by restricting each connected module to only the directions its own modport declares.

Example of Connecting to a Generic Interface

A module can also have a generic interface as its port list. The generic handle accepts any modport passed down to it from the hierarchy above:


module dut0  ( interface  _if);
	...
endmodule

module dut1  ( interface _if);
	...
endmodule

module tb;
	myInterface 	_if;
	dut0  	d0 	( ._if (_if.dut0));
	dut1 	d1 	( ._if (_if.dut1));
endmodule

Where Are Modports Used?

Modports are what let a single interface serve both sides of a connection safely — a bus master and a bus slave, or a DUT and its testbench monitor, all sharing the exact same physical signals while each only being able to drive the direction it's actually supposed to.

Design Example

Consider two modules, master and slave, connected by a simple bus. The bus sends an address and data which the slave is expected to capture and store in its internal registers. The master always initiates the transfer, and the slave indicates via sready whether it's ready to accept data.

Interface

Below is the interface shared between master and slave:


interface ms_if (input clk);
  logic sready;      // Indicates if slave is ready to accept data
  logic rstn;        // Active low reset
  logic [1:0] addr;  // Address
  logic [7:0] data;  // Data
  
  modport slave ( input addr, data, rstn, clk,
                 output sready);
  
  modport master ( output addr, data,
                  input  clk, sready, rstn);
endinterface

Design

The master iterates the address from 0 to 3 and sends data equal to the address multiplied by 4, only sending when the slave indicates it's ready via sready:


// This module accepts an interface with modport "master"
// Master sends transactions in a pipelined format
// CLK    1   2   3   4   5   6
// ADDR   A0  A1  A2  A3  A0  A1
// DATA       D0  D1  D2  D3  D4
module master ( ms_if.master mif);
  always @ (posedge mif.clk) begin
  
  	// If reset is applied, set addr and data to default values
    if (! mif.rstn) begin
      mif.addr <= 0;
      mif.data <= 0;
  
    // Else increment addr, and assign data accordingly if slave is ready
    end else begin
    // Send new addr and data only if slave is ready
      if (mif.sready) begin
      	mif.addr <= mif.addr + 1;
      	mif.data <= (mif.addr * 4);

     // Else maintain current addr and data
      end else begin
        mif.addr <= mif.addr;
        mif.data <= mif.data;
      end
    end
  end
endmodule

Note how data lags addr by one cycle in the timing comment above: since both assignments are non-blocking and both read mif.addr's current value in the same clock edge, data is computed from the address before it increments — matching the pipelined ADDR/DATA table shown in the comment.

The slave accepts data for every address and stores it into internal registers. When the address wraps from 3 back to 0, the slave requires one additional clock to become ready again:


module slave (ms_if.slave sif);
  reg [7:0] reg_a;
  reg [7:0]	reg_b;
  reg 		reg_c;
  reg [3:0] reg_d;
  
  reg		dly;
  reg [3:0] addr_dly;

  always @ (posedge sif.clk) begin
    if (! sif.rstn) begin
      addr_dly <= 0;
    end else begin
      addr_dly <= sif.addr;
    end
  end
  
  always @ (posedge sif.clk) begin
    if (! sif.rstn) begin
      	reg_a <= 0;
    	reg_b <= 0;
    	reg_c <= 0;
    	reg_d <= 0;
  	end else begin
      case (addr_dly)
        0 : reg_a <= sif.data;
        1 : reg_b <= sif.data;
        2 : reg_c <= sif.data;
        3 : reg_d <= sif.data;
      endcase
    end
  end
  
  assign sif.sready = ~(sif.addr[1] & sif.addr[0]) | ~dly;
  
  always @ (posedge sif.clk) begin
    if (! sif.rstn)
      dly <= 1;
    else
      dly <= sif.sready;
  end

endmodule

The two design modules are tied together at the top level — note that both are connected to the same single ms_if instance, which is correct here, since a master and slave genuinely need to share the exact same physical bus signals:


module d_top (ms_if tif);
	// Pass the "master" modport to master
  	master 	m0 (tif.master);

  	// Pass the "slave" modport to slave
  	slave 	s0 (tif.slave);
endmodule

Testbench

The testbench passes the interface handle to the design, which assigns the master and slave modports to its own sub-modules:


module tb;
  reg clk;
  always #10 clk = ~clk;
  
  ms_if 	if0 (clk);
  d_top 	d0  (if0);  
  
  // Let the stimulus run for 20 clocks and stop
  initial begin
    clk <= 0;
    if0.rstn <= 0;
    repeat (5) @ (posedge clk);
    if0.rstn <= 1;
  
    repeat (20) @ (posedge clk);
    $finish;
  end
endmodule

The master initiates bus transactions, and the slave captures data and stores it in its internal registers (reg_a through reg_d) for the corresponding address.

Combining a Modport with a Clocking Block

In practice, a testbench-facing modport very often references a clocking block directly, instead of listing plain signal directions itself. This gets you both benefits at once: the clocking block's race-free timing (covered in Clocking Blocks) and the modport's access restriction, in a single named view.


interface req_gnt_if (input logic clk);
  logic req;
  logic gnt;
  logic rstn;

  clocking cb @(posedge clk);
    default input #1step output #2ns;
    output req;   // testbench drives: goes into design
    input  gnt;   // testbench reads: comes from design
  endclocking

  // TB gets the clocking block's timed view of req/gnt, plus a direct
  // (unclocked) handle on rstn, since reset is typically async
  modport TB  (clocking cb, output rstn);

  // DUT gets a plain, undirected-by-clock view - it doesn't need skew,
  // it just needs correct signal directions
  modport DUT (input req, rstn, clk, output gnt);
endinterface

rstn is deliberately left out of the clocking block and declared directly on the TB modport instead. Reset is usually asynchronous by design, so driving it through a clocking block's synchronous skew doesn't make sense — a modport can mix a clocking reference with ordinary input/output signals side by side, exactly as shown here.

The design side only ever sees the plain DUT modport — it has no idea a clocking block even exists on the testbench side:


module des (input logic clk, req, rstn, output logic gnt);
  always @ (posedge clk or negedge rstn)
    if (!rstn) gnt <= 0;
    else       gnt <= req;
endmodule

module d_top (req_gnt_if.DUT dif);
  des dut0 (.clk(dif.clk), .req(dif.req), .rstn(dif.rstn), .gnt(dif.gnt));
endmodule

The testbench drives req and reads gnt through the clocking block, while driving rstn directly:


module tb;
  logic clk;
  always #10 clk = ~clk;

  req_gnt_if vif (.clk(clk));
  d_top      d0  (vif.DUT);

  initial begin
    vif.rstn    <= 0;
    vif.cb.req  <= 0;

    repeat (3) @(vif.cb);
    vif.rstn <= 1;

    repeat (2) @(vif.cb);
    vif.cb.req <= 1;

    repeat (5) @(vif.cb);
    $finish;
  end
endmodule

Note that d0 is instantiated with vif.DUT specifically, giving the design module only the plain directional view — the clocking block itself is only ever referenced from the testbench side.

Quick Check

Q: Why isn't rstn included inside the clocking cb block above, even though it's still part of the TB modport?

Show Answer

Reset is typically asynchronous, and a clocking block's whole purpose is to apply synchronous sampling/driving skew relative to a clock edge. Driving an async reset through a clocking block would force it to follow a clock-relative timing scheme it isn't meant to have — so it's declared as a plain signal on the modport instead, alongside the clocking cb reference.

Common Beginner Mistakes

Mistake #1: A case-sensitivity typo in the interface or modport name

As both examples earlier in this article show, myinterface and myInterface are two entirely different identifiers to SystemVerilog. Why? A single mismatched letter's case is enough to turn a working port declaration into a compile error — always double-check that the interface name used at every module's port list exactly matches its declaration, capitalization included.

Mistake #2: Skipping modports on a shared interface

Without a modport, every net in an interface is inout by default, so nothing stops two connected modules from both trying to drive the same signal. Why? This can silently produce X values on a net whenever both sides happen to drive conflicting values — modports exist specifically to make that impossible by restricting each module to only its intended direction.

Mistake #3: Confusing which side chooses the modport

In the named port bundle style, the design's own port list fixes which modport it gets; in the connecting port bundle style, the testbench chooses at instantiation time. Why? Mixing these up leads to writing a generic-looking port list but then never actually specifying a modport anywhere, or vice versa — pick one style deliberately rather than blending the two.

Mistake #4: Putting every signal, including reset, inside the clocking block

Not every signal on a testbench-facing modport needs to go through the clocking block. Why? Asynchronous signals like reset don't have a meaningful relationship to a clock edge, so forcing them through a clocking block's skew is unnecessary at best and misleading at worst — declare them as plain input/output on the modport instead, alongside the clocking reference.

Practice Exercise

Challenge: Add a Read-Only monitor Modport

Using the ms_if interface from the design example, add a third modport called monitor that declares every signal (addr, data, rstn, clk, sready) as input — a passive observer that can watch the bus without ever driving anything.

Hint

A modport that only reads declares every signal it lists as input, even ones that other modports (like slave) declare as output.

Solution

interface ms_if (input clk);
  logic sready;
  logic rstn;
  logic [1:0] addr;
  logic [7:0] data;
  
  modport slave ( input addr, data, rstn, clk,
                 output sready);
  
  modport master ( output addr, data,
                  input  clk, sready, rstn);

  // A passive observer - every signal is an input, nothing is ever driven
  modport monitor ( input addr, data, rstn, clk, sready );
endinterface

monitor declares sready as input even though slave declares it as output — each modport is just a different view over the same underlying signals, so a monitor module can watch the entire bus transaction without risking driving anything itself.

Summary

In this tutorial, you learned:

  • A modport defines signal directions inside an interface, from the perspective of whatever module uses that particular view
  • Without a modport, interface nets are inout by default, leaving no protection against two modules driving the same net at once
  • In a named port bundle, the design's own port list fixes the modport; in a connecting port bundle, the testbench chooses the modport at instantiation
  • A module can accept a generic interface port and still be handed a specific modport view by whatever instantiates it
  • Multiple modules (like a bus master and slave) can safely share one interface instance when each is restricted to its own modport
  • A modport can reference a clocking block directly for timed signals, while still declaring other signals (like an async reset) as plain input/output alongside it
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5