Group protocol signals, define each block's role, and reduce connection mistakes.
Group a protocol
An interface collects the signals that form one connection. It can also contain types, parameters, functions, tasks, and assertions related to the protocol.
Instead of repeating four ports at every level, a module receives an interface instance.
module stream_register ( input logic i_rst, simple_stream_if.sink upstream, simple_stream_if.source downstream); always_ff @(posedge upstream.clk) begin if (i_rst) begin downstream.valid <= 1'b0; downstream.data <= '0; downstream.last <= 1'b0; end else if (upstream.valid && upstream.ready) begin downstream.data <= upstream.data; downstream.last <= upstream.last; downstream.valid <= 1'b1; end else if (downstream.ready) begin downstream.valid <= 1'b0; end end assign upstream.ready = downstream.ready || !downstream.valid;endmodule
Reset is an explicit register port. It initializes at least downstream.valid so that startup cannot advertise an unknown transaction.
The role of modports
A modport describes one participant's view. For source, valid, data, and last are outputs, while ready is an input. For sink, directions are reversed.
This information helps the compiler catch a signal driven from the wrong side. It also documents the protocol without requiring readers to inspect every connected module.
Modport directions are viewed from the module that uses the modport. This is a common source of confusion when writing the first interface.
Defining when exchanges occur
Modports describe who can read or drive a signal. A clocking block describes when the testbench samples or drives it. In tb_cb, inputs are sampled by default one precision step before the edge with #1step, while outputs are driven at the current edge with #0.
The testbench waits for the clocking-block event and accesses signals through its name:
@bus.tb_cb;bus.tb_cb.valid <= 1'b1;bus.tb_cb.data <= value;do @bus.tb_cb; while (!bus.tb_cb.ready);bus.tb_cb.valid <= 1'b0;
Clocking-block outputs are driven with nonblocking assignments. Writing bus.valid instead of bus.tb_cb.valid would bypass the synchronization that the interface is meant to provide.
A clocking block has a single clock expression. An interface that crosses several clock domains must therefore define one clocking block for each domain used by the testbench.
Connecting classes to signals
A class is created dynamically, whereas a physical interface is instantiated statically alongside the DUT. A virtual interface connects these two worlds: it is a handle to an existing interface instance.
Here the virtual type retains the tb modport. The driver therefore receives only the view and clocking block intended for it. A typedef avoids repeating a long declaration and reduces the risk of omitting the modport.
Top-level instantiation
simple_stream_if #(.WIDTH(16)) link (.clk(i_clk));producer u_producer (.bus(link.source));consumer u_consumer (.bus(link.sink));
One link instance contains the signals. Modports restrict the view supplied to each module.
What an interface does not solve
An interface does not define timing behavior by itself. You must still specify when valid may change, when a transfer occurs, how long data remains stable, and how reset behaves.
Many modern tools synthesize interfaces, but limits can exist around tasks, parameters, arrays of interfaces, or IP boundaries. A simple flat interface is usually the most portable form.
Key takeaways
An interface groups protocol signals and declarations.
A modport gives each role a view and directions.
Directions are expressed from the using module's point of view.
Interfaces reduce repeated wiring, not the need to specify a protocol.
A clocking block centralizes the testbench clock, sampling, and driving rules.
A virtual interface lets a class work with a physical interface instance.