SystemVerilog · Module 8
Modport — Restricting Port Directions
Per-role direction control on interface signals, master/slave/monitor modports, task imports, and structural enforcement of bus role contracts.
Module 8 · Page 8.2
How modport gives each module a correctly directed view of a shared interface — enforcing who can drive and who can only read each signal, at compile time. This page covers the problem modport solves, syntax and anatomy, three ways to apply a modport, importing protocol tasks, multi-modport AXI-Lite design, UVM driver/monitor patterns, elaboration-time direction checking, five real debugging labs, twelve interview questions, and ten production best practices.
The Problem — Interfaces Without Directions
In page 8.1 you saw how an interface bundles all APB signals into a single named connection. But there is a fundamental issue: inside an interface, all signals are declared as plain logic — with no direction. By default, any module connected to the interface can drive any signal.
That means nothing stops the slave from accidentally driving paddr (which belongs to the master), or the master from driving prdata (which belongs to the slave). The simulator will not catch this — you will get a multiple-driver conflict that produces X's and takes hours to track down.
// Without modport: slave can accidentally drive master signals
module apb_slave (apb_if bus); // generic interface — no direction enforcement
always_ff @(posedge bus.pclk) begin
bus.pready <= 1;
bus.prdata <= 32'hBEEF_CAFE;
// BUG: nothing prevents the slave from writing these master signals!
bus.paddr <= 32'h0; // ← should be impossible — no error!
bus.pwrite <= 0; // ← same — multiple drivers → X
end
endmoduleModport fixes this. It gives each module a named, direction-restricted view of the interface. The slave module that uses apb_if.slave physically cannot drive paddr — the compiler rejects it as a direction violation.
What is modport?
modport stands for module port. It is a named subset declaration inside an interface that specifies, from one particular module's perspective, which signals are inputs, which are outputs, and which tasks/functions are accessible. One interface can have many modports — one per participant in the protocol.
The master modport gives the master module output access to paddr, psel, penable, pwrite, pwdata and input access to prdata, pready, pslverr. The slave modport is the exact mirror view: input access to paddr, psel, penable, pwrite, pwdata, and output access to prdata, pready, pslverr. The monitor modport gives input-only access to every signal — read everything, drive nothing. Each direction is enforced at compile time.
Declaring a modport — Syntax & Anatomy
interface apb_if (input logic pclk, presetn);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready, pslverr;
// ① ② ③ ④ ⑤
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);
// Monitor sees everything as input — never drives
modport monitor (input pclk, presetn, paddr, psel, penable, pwrite,
pwdata, prdata, pready, pslverr);
endinterface- modport keyword — declares a named, direction-restricted view. Multiple modports can exist in one interface — one per role in the protocol.
- master — the modport name. Used when connecting a module:
apb_if.master. Pick meaningful names that match roles: master/slave, driver/monitor, source/sink, producer/consumer. - input — signals this module can only read. Attempting to drive an input-declared signal from this module causes a compile error — exactly what we want.
- pclk, presetn, prdata, pready, pslverr — the specific signals the master receives. Note: clock and reset are always input to every module.
- output — signals this module can drive. The simulator treats these as the authoritative source for these signals. Two modules both declared as output drivers of the same signal will cause a multiple-driver conflict.
Using modport in a Module — Three Ways
There are three places where the modport can be specified. All three are legal. The first two are most common.
// ─── Way 1: In the module port declaration (most explicit) ────────────
// The modport is locked in at the module definition level.
// This module will ALWAYS use the master view, regardless of how it's instantiated.
module apb_master (apb_if.master bus);
// bus.paddr ← output — can drive
// bus.prdata ← input — can only read
always_ff @(posedge bus.pclk) bus.paddr <= 32'h4000_0000;
endmodule
// ─── Way 2: At instantiation in the parent (flexible) ─────────────────
// The module port is a generic interface type, modport chosen at connect time.
module apb_master (apb_if bus); // no modport here
always_ff @(posedge bus.pclk) bus.paddr <= 32'h4000_0000;
endmodule
// In TB top: modport specified when connecting
apb_if apb (.pclk(clk), .presetn(rst_n));
apb_master u_mst (.bus(apb.master)); // ← modport applied at connection
apb_slave u_slv (.bus(apb.slave)); // ← same interface, different modport
// ─── Way 3: No modport — full interface (monitor use case) ────────────
// No direction enforcement — the module can read all signals freely.
// Use only for monitors/checkers that observe but never drive.
module apb_monitor (apb_if bus);
always @(posedge bus.pclk)
if (bus.psel && bus.penable)
$display("APB: addr=%h data=%h wr=%b",
bus.paddr, bus.pwrite?bus.pwdata:bus.prdata, bus.pwrite);
endmodulemodport with import — Exposing Tasks
A modport can also make specific tasks or functions from the interface accessible to the module — using the import keyword inside the modport declaration. Without this, a module connected via a modport cannot call tasks defined in the interface.
interface apb_if (input logic pclk, presetn);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready, pslverr;
// ── Protocol tasks defined once, used by any driver ─────────────
task automatic apb_write(
input logic [31:0] addr, data
);
@(posedge pclk); #1;
paddr = addr; pwdata = data;
psel = 1; pwrite = 1;
@(posedge pclk); #1;
penable = 1;
@(posedge pclk iff pready);
psel = 0; penable = 0; pwrite = 0;
endtask
task automatic apb_read(
input logic [31:0] addr,
output logic [31:0] data
);
@(posedge pclk); #1;
paddr = addr; psel = 1; pwrite = 0;
@(posedge pclk); #1;
penable = 1;
@(posedge pclk iff pready);
data = prdata;
psel = 0; penable = 0;
endtask
// ── modport: signals + imported tasks ────────────────────────────
modport master (
input pclk, presetn, prdata, pready, pslverr,
output paddr, psel, penable, pwrite, pwdata,
import apb_write, // ← task accessible through this modport
import apb_read // ← tasks not listed here are NOT accessible
);
// Monitor: only reads, no task access
modport monitor (
input pclk, presetn, paddr, psel, penable,
pwrite, pwdata, prdata, pready, pslverr
);
endinterface
// ── Driver module: calls tasks via the modport ─────────────────────────
module apb_driver (apb_if.master bus);
initial begin
// Clean, readable protocol calls — no low-level signal juggling
bus.apb_write(32'h4000_0000, 32'h1234_5678);
bus.apb_write(32'h4000_0004, 32'h9ABC_DEF0);
logic [31:0] rd_data;
bus.apb_read(32'h4000_0000, rd_data);
$display("Read back: 0x%0h", rd_data);
end
endmoduleMultiple Modports — AXI-Lite Example
A realistic bus interface needs modports for every participant in the protocol. Here is an AXI-Lite interface with modports for the master, slave, and monitor — showing how one interface definition supports an entire bus ecosystem.
interface axi_lite_if #(parameter DW = 32, AW = 32)
(input logic aclk, aresetn);
// Write address channel
logic [AW-1:0] awaddr;
logic awvalid, awready;
// Write data channel
logic [DW-1:0] wdata;
logic [DW/8-1:0] wstrb;
logic wvalid, wready;
// Write response channel
logic [1:0] bresp;
logic bvalid, bready;
// Read address channel
logic [AW-1:0] araddr;
logic arvalid, arready;
// Read data channel
logic [DW-1:0] rdata;
logic [1:0] rresp;
logic rvalid, rready;
// ── Master modport: initiates transactions ──────────────────────
modport master (
input aclk, aresetn,
input awready, wready, bvalid, bresp, arready, rvalid, rdata, rresp,
output awaddr, awvalid, wdata, wstrb, wvalid, bready,
araddr, arvalid, rready
);
// ── Slave modport: responds to transactions ─────────────────────
modport slave (
input aclk, aresetn,
input awaddr, awvalid, wdata, wstrb, wvalid, bready,
araddr, arvalid, rready,
output awready, wready, bvalid, bresp, arready, rvalid, rdata, rresp
);
// ── Monitor modport: observes only ──────────────────────────────
modport monitor (
input aclk, aresetn,
input awaddr, awvalid, awready,
wdata, wstrb, wvalid, wready,
bresp, bvalid, bready,
araddr, arvalid, arready,
rdata, rresp, rvalid, rready
);
endinterfacemodport Direction Keywords — Full Reference
| Direction | Meaning | Use when |
|---|---|---|
input | Module reads this signal — cannot drive it | Signals driven by the other side of the bus (e.g., prdata in the master modport) |
output | Module drives this signal — is the sole driver | Signals this module is responsible for (e.g., paddr in the master modport) |
inout | Module can both drive and read (tri-state) | Bidirectional buses (I2C SDA, tri-state data buses) |
ref | Pass by reference — no direction enforced | Task arguments that need to modify the signal value; not commonly used in synthesisable code |
import task/func | Makes an interface task callable through this modport | Protocol helper tasks that should be accessible from the module using this modport |
export task/func | Module provides a task that the interface can call | Rare — callback mechanisms where the interface calls back into the module |
Common Mistakes & How to Fix Them
// ════ MISTAKE 1: Forgetting to add clock/reset to every modport ═══
// ❌ Missing pclk and presetn
modport master (output paddr, psel, penable, pwrite, pwdata,
input prdata, pready);
// Master module's always @(posedge bus.pclk) → "signal not in modport" error
// ✅ FIX: always include clock/reset as input in every modport
modport master (input pclk, presetn, prdata, pready,
output paddr, psel, penable, pwrite, pwdata);
// ════ MISTAKE 2: Same signal as output in two modports — multi-driver ═══
// ❌ Both master and slave claim 'paddr' as output → multi-driver X
modport master (output paddr, psel, penable, ...);
modport slave (output paddr, prdata, pready); // ← paddr again!
// ✅ FIX: exactly one modport per signal as output; all others input
modport master (output paddr, psel, penable, pwrite, pwdata,
input prdata, pready);
modport slave (input paddr, psel, penable, pwrite, pwdata,
output prdata, pready);
// ════ MISTAKE 3: Calling interface tasks without importing them ═══
// ❌ Modport has no import — task call is rejected
modport master (input pclk, output paddr, psel, ...); // no import!
module drv (apb_if.master bus);
initial bus.apb_write(addr, data); // ← compile error
endmodule
// ✅ FIX: import every task the module needs to call
modport master (input pclk, output paddr, psel, ...,
import apb_write, import apb_read);
// ════ MISTAKE 4: Omitting a signal from a modport — invisible ═══
// ❌ 'pslverr' missing from master modport
modport master (input pclk, prdata, pready, // ← pslverr missing
output paddr, psel, penable, pwrite, pwdata);
// Master tries: if (bus.pslverr) ... → "signal not visible" error
// ✅ FIX: every signal the module accesses must be listed in its modport
modport master (input pclk, prdata, pready, pslverr,
output paddr, psel, penable, pwrite, pwdata);Quick Reference — modport at a Glance
// ── Declare modports inside interface ────────────────────────────────
interface my_if (input logic clk, rst_n);
logic [7:0] data;
logic valid, ready;
task automatic send(input logic [7:0] d); /*...*/ endtask
modport source (input clk, rst_n, ready,
output data, valid,
import send); // task accessible here
modport sink (input clk, rst_n, data, valid,
output ready);
modport monitor (input clk, rst_n, data, valid, ready);
endinterface
// ── Way 1: modport in module port declaration ─────────────────────────
module producer (my_if.source intf); // direction enforced at definition
// ── Way 2: modport at instantiation ──────────────────────────────────
module producer (my_if intf); // generic — modport applied when connected
my_if bus (.clk(clk), .rst_n(rst_n));
producer u (.intf(bus.source)); // modport chosen at instantiation
// ── Compile-time enforcement ──────────────────────────────────────────
// Inside producer (using source modport):
intf.data = 8'hAB; // ✓ valid — data is output in source modport
intf.ready = 1; // ✗ ERROR: ready is input in source modport
intf.send(8'h42); // ✓ valid — send is imported in source modportVerification Usage — Modport Role in UVM Components
Every UVM component that touches the bus picks the modport that matches its role. The driver attaches via the master (or slave) modport — it drives the bus. The monitor attaches via the monitor modport — it observes without ever being able to drive. The scoreboard never holds a virtual interface directly; it receives transactions from the monitor. This three-tier separation is enforced at elaboration by the modport directions; no test can accidentally turn the monitor into a driver.
interface apb_if(input logic clk);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready;
modport master (output paddr, pwdata, pwrite, psel, penable,
input prdata, pready);
modport slave (input paddr, pwdata, pwrite, psel, penable,
output prdata, pready);
modport monitor (input paddr, pwdata, pwrite, psel, penable,
input prdata, pready);
endinterface
// ── UVM driver — uses master modport ──────────────────────────
class apb_driver extends uvm_driver;
virtual apb_if.master vif; // ← role baked into type
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if.master)::get(
this, "", "vif", vif))
`uvm_fatal("NOVIF", "No master vif")
endfunction
task run_phase(uvm_phase phase);
forever begin
apb_xact tr; seq_item_port.get_next_item(tr);
vif.paddr <= tr.paddr; // ✓ master can drive paddr
vif.pwdata <= tr.pwdata;
vif.psel <= 1'b1;
// vif.prdata = ... ✗ would fail at elaboration
@(posedge vif.clk);
seq_item_port.item_done();
end
endtask
endclass
// ── UVM monitor — uses monitor modport ───────────────────────
class apb_monitor extends uvm_monitor;
virtual apb_if.monitor vif; // ← cannot drive ANY signal
task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
if (vif.psel && vif.penable && vif.pready) begin
apb_xact obs = apb_xact::type_id::create("obs");
obs.paddr = vif.paddr; // ✓ monitor can read everything
obs.pwdata = vif.pwdata;
// vif.paddr = ... ✗ would fail at elaboration
ap.write(obs);
end
end
endtask
endclassSimulation Behavior — How Modports Resolve at Elaboration
The elaborator processes each modport connection by walking every signal reference inside the connected module and checking it against the modport's declared direction. The check is structural: a connected module declared as modport master may write only signals listed as output or inout in the master modport. Any violation fails elaboration with a specific error pointing to the offending line.
interface bus_if;
logic [7:0] data;
logic valid, ready;
modport producer(output data, valid, input ready);
modport consumer(input data, valid, output ready);
endinterface
module producer_block(bus_if.producer bus);
always_ff @(posedge clk) begin
bus.data <= next_data; // ✓ data is output in producer
bus.valid <= 1'b1; // ✓ valid is output in producer
// bus.ready = 1'b0; ✗ ERROR: ready is input in producer
last_seen <= bus.ready; // ✓ producer may READ ready
end
endmodule
// Elaboration output (Questa/VCS/Xcelium produce the same kind of error):
// Error: producer_block.sv:7: 'bus.ready' is declared as 'input' in
// modport 'producer' of interface 'bus_if'. Cannot assign.Waveform Analysis — Identifying Modport Connections in Verdi/DVE
Modport restrictions don't appear in the waveform — the signals are normal signals once elaborated. But the connection topology is visible: opening a module's instance in Verdi shows the interface port labelled with its modport name (e.g., apb.master), making it instantly clear which role the block plays. This matters for debug: when a bus signal is misbehaving, you can confirm the modport role at a glance and rule out wrong-role wiring as a cause.
cycle 0 1 2 3 4 5
clk _│‾│_│‾│_│‾│_│‾│_│‾│_│‾│_
DRIVER side (apb.master modport — outputs paddr,pwdata,psel,penable; reads pready):
u_drv.paddr -- 100 100 100 -- -- ← driven by master
u_drv.pwdata -- AA AA AA -- -- ← driven by master
u_drv.psel 0 1 1 1 0 0 ← driven by master
u_drv.penable 0 0 1 1 0 0 ← driven by master
SLAVE side (apb.slave modport — reads paddr,pwdata,psel,penable; outputs pready,prdata):
u_dut.paddr -- 100 100 100 -- -- ← READ by slave (same wire)
u_dut.pready 0 0 0 1 0 0 ← driven by slave
MONITOR side (apb.monitor — all input):
u_mon.paddr -- 100 100 100 -- -- ← READ-only visibility
u_mon.psel 0 1 1 1 0 0 ← READ-only visibility
The waveform shows the same signal values everywhere (paddr=100 across cycles 1-3
on driver/slave/monitor) — modport doesn't duplicate signals, it only restricts
WHO can write them. The monitor sees everything but couldn't even attempt to drive
without elaboration failure.Industry Insights — How Senior Teams Use Modports
Debugging Academy — 5 Real Modport Bugs
Each lab is a real failure mode pulled from production projects. Buggy code, symptom, root cause, fix.
Driver Wired Through monitor Modport — All Writes Fail Silently
WRONG ROLEinterface apb_if(input logic clk);
logic [31:0] paddr, pwdata;
logic psel, penable;
modport master (output paddr, pwdata, psel, penable);
modport monitor (input paddr, pwdata, psel, penable);
endinterface
// In TB top — wrong modport assigned to driver:
apb_if u_apb(.clk);
apb_driver u_drv(.apb(u_apb.monitor)); // ← should be .master!
// Or in UVM:
uvm_config_db#(virtual apb_if.monitor)::set( // ← wrong type!
null, "uvm_test_top.env.agent.driver", "vif", u_apb);Elaboration fails with an error like "variable 'paddr' is read-only in modport 'monitor'". If the driver was already written with vif.paddr <= tr.paddr, the elaborator points directly at the offending line. If the configuration lookup is by type, the config_db::get returns false because the published type (virtual apb_if.monitor) doesn't match the requested type (virtual apb_if.master) — the driver gets a null vif and `uvm_fatals at build_phase.
The TB wired the driver as if it were a monitor. The bug is either at instantiation (passing the wrong modport on the port connection) or in the config_db set/get pair (publishing one modport-typed handle and requesting another).
Both the port connection and the config_db type must match the role. Driver: .apb(u_apb.master) on the instantiation, virtual apb_if.master on the config_db set/get. Monitor: .apb(u_apb.monitor) and virtual apb_if.monitor. Enforce with a code-review checklist: every TB component declares its modport role next to its port; every config_db lookup uses the matching type.
Two Masters on the Same Interface — Multi-Driver X
DUPLICATE ROLEapb_if u_apb(.clk);
apb_driver u_drv (.apb(u_apb.master)); // master #1
apb_driver u_drv2(.apb(u_apb.master)); // ← second driver, also master!
apb_slave u_dut (.apb(u_apb.slave));
// Both drv and drv2 drive paddr, pwdata, etc.
// Tool may or may not warn — many don't.paddr, pwdata, psel, penable show X in the waveform whenever both drivers are simultaneously active. Tests pass when only one driver is exercising the bus; fail when both fire near the same clock edge.
Modport restricts direction per role, but doesn't enforce "at most one connected module per master role." Two modules legally claim the master role; both drive the same signals; the simulator resolves to X. The language is silent here; lint must catch it.
Lint rule: at most one connected module per master/slave modport on the same interface instance (Spyglass modport_multi_use). For genuine multi-master buses (e.g., an SoC interconnect crossbar), use separate interface instances per master, then arbitrate explicitly in a bridge module. The "multiple masters on one interface" pattern shouldn't be modelled by wiring two masters to the same interface; it should be modelled by an explicit arbiter.
Forgotten Signal in Modport — Module Sees X
MISSING SIGNALinterface apb_if(input logic clk);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready, perr; // ← perr added
// Modport declared BEFORE perr was added — never updated
modport master(output paddr, pwdata, pwrite, psel, penable,
input prdata, pready);
// ^^^^^ perr missing — master can't see it!
endinterface
// Master tries to use the new error signal:
module apb_master(apb_if.master apb);
always_ff @(posedge apb.clk)
if (apb.perr) ... // ← compile error or X-read
endmoduleCompile error: "signal 'perr' not visible through modport 'master'." Engineer adds input perr to the modport, recompiles, problem "solved." But if the modport check is too loose (older tools), the access silently sees X because the signal isn't routed through the modport interface — looks like a runtime X bug.
Adding a signal to the interface requires updating every modport that needs to access it. The error message is clear when the tool catches it, but it's an extra step that engineers sometimes skip — especially when adding a "debug-only" signal that the master "doesn't really need."
Discipline: every time a signal is added to an interface, every modport declaration in that interface gets reviewed. Some teams use a "wildcard-style" pattern — though SystemVerilog doesn't have one natively, you can declare an "all" modport that lists every signal as inout for internal helper modules; never expose this to integration code, only use it in interface-internal tasks. Best is to keep the signal list small enough that updating modports stays manageable.
Imported Task Drives Bus from Wrong Side
TASK BLEED-THROUGHinterface apb_if(input logic clk);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready;
// Master-side handshake task — drives the bus
task automatic do_write(input logic [31:0] a, d);
@(posedge clk);
paddr <= a; psel <= 1;
@(posedge clk); penable <= 1;
while (!pready) @(posedge clk);
psel <= 0; penable <= 0;
endtask
// ❌ Imported into BOTH modports
modport master(/*...*/, import do_write);
modport slave (/*...*/, import do_write); // ← slave shouldn't have this!
endinterface
// Slave code does this:
module apb_slave(apb_if.slave apb);
always @(error_condition) apb.do_write(32'h0, 32'h0); // slave drives bus
endmoduleTwo writers (the real master and the slave's misuse) end up driving the same bus signals concurrently → multi-driver X. The slave's "helpful" initialisation behaviour corrupts every master-initiated transaction near the same time. The symptom looks like random bus glitches; the cause is structural.
The handshake task was imported into both modports as a convenience. A slave-side developer used it because it was available. The interface didn't stop them — the import allowed the call; the modport directions allowed the task body's writes (because the task itself runs in the interface's scope, not the caller's modport context).
Import each task into only the modport that owns the role it encodes. do_write is a master-side action; import only into master. If the slave needs a response-driving helper, write a separate do_respond task and import only into slave. Lint rule: "modport task import must match the modport's owned role" (custom rule; most production teams add this).
Modport Mismatch Between TB Top and UVM Component Type
CONFIG_DB DRIFT// TB top
apb_if u_apb(.clk);
initial begin
// Published as the BASE interface type
uvm_config_db#(virtual apb_if)::set(null, "*", "vif", u_apb);
run_test();
end
// Driver expects the MASTER modport type:
class apb_driver extends uvm_driver;
virtual apb_if.master vif;
virtual function void build_phase(uvm_phase phase);
if (!uvm_config_db#(virtual apb_if.master)::get( // ← mismatch
this, "", "vif", vif))
`uvm_fatal("NOVIF", "No master vif found")
endfunction
endclassDriver's build_phase calls `uvm_fatal("NOVIF", "No master vif found"). Engineer assumes something is wrong with the interface instantiation or the config_db scope string, spends an hour grepping for typos before noticing the type mismatch.
config_db is type-strict. Publishing as virtual apb_if and looking up as virtual apb_if.master are different types; the lookup fails. The same handle could be accessed via the master modport, but config_db doesn't perform that conversion.
Publish the handle with the exact modport-qualified type the consumers expect. Either publish per-role (uvm_config_db#(virtual apb_if.master)::set(...), uvm_config_db#(virtual apb_if.monitor)::set(...)), or pick a convention where the TB top always publishes the bare type and every component looks up with the bare type and uses .master/.monitor only at access time. Mixing conventions is the bug; pick one and enforce it.
Interview Q&A — 12 Questions on Modports
Drawn from real interviews. Try to answer before reading each response.
A modport is a named view of an interface that declares the direction of each signal from a particular connected module's perspective. It solves the "anyone can drive anything" problem of a plain interface: without modports, any module connected to the interface can drive any signal, which makes multi-driver bugs trivial to introduce. With modports, the compiler enforces who can read and who can write each signal — wrong-direction access fails at elaboration, not in silicon.
Best Practices — Modport Rules to Walk Away With
- Declare modports the moment a second module connects. Don't defer until the multi-driver bug bites.
- Every protocol interface has master, slave, and monitor at minimum. Add tb only if backdoor fault injection is needed.
- Type virtual interfaces to specific modports in UVM components.
virtual apb_if.master vif;— not the barevirtual apb_if. - config_db set and get types must match exactly. Both
virtual apb_if.masteron the publish and the lookup, or both bare. - Import tasks into only the modport that owns the role they encode. Master-handshake tasks → master only. Never imported into all modports.
- Use the monitor modport for every observer. Coverage, assertions, scoreboard — none should hold a modport that can drive.
- Lint for "at most one module per master/slave modport instance." Spyglass
modport_multi_useor equivalent — multi-driver bugs catch in CI, not silicon. - Document the IP's modport contract in the user guide. Each connected module's required modport and the signal directions on each.
- Update every modport when adding a signal to the interface. The new signal needs to be declared in every modport that should access it; review all modports as one transaction.
- For runtime role switching, instantiate two modules. SystemVerilog doesn't support dynamic modport rebinding; use explicit mux/control logic between two role-fixed instances.