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
inoutby 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
interfaceport 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
clockingblock directly for timed signals, while still declaring other signals (like an async reset) as plaininput/outputalongside it