Logo

Modports in SV

30 Dec 2022
3 mins

Previously, we saw what interfaces are and why they are useful. Modports are a part of an interface that helps define the direction of signals. But we can also define the direction of signals within the interface, so what is the need for modports? In this article, we will see how modports help us organize signals in complex designs as well as testbenches.

What are modports?

Modports can be considered as a sub-entity of interface which defines a particular direction to the signals. In an interface, there can be many modports and all modports can define different directions to the signal.

Syntax

modport <modport_name> (<direction> <signal_name [range]>, <direction> <signal_name [range]>, ...);

Need for modports

An interface is an entity that helps connect testbenches to designs or different modules of a design. For example, let's consider the simple scenario of connecting a testbench to a design. Any inputs in the design need to be driven by the testbench. So, the signals that are inputs for the design are outputs for the testbench. However, inside an interface, the same signal cannot take on two different directions. This is where modports become helpful. We can define two modports inside the interface. One will define the directions with respect to the design, and the other will define the directions with respect to the testbench.

In later articles, we will see that complex testbenches often have drivers and monitors. For a driver to drive signals, the direction would be output, but a monitor, as its name suggests, needs to monitor the signals and thus all the signals will be inputs in this case. This is another scenario where modports are helpful.

This is the main reason why we logic as a data type for signals inside. If we used reg or wire, then it would be difficult to make it behave as both input and output without worrying about the driver load issue.

Example

//  Example interface having modport to define different
//  direction for the same signals. Also note we are passing
//  clk as an argument.
interface intf( input logic clk);
    // Declare the signals inside the interface
    logic [7:0] data_in;
    logic [7:0] data_out;
    logic [3:0] addr;
    logic rw;

    // Define the modports
    modport dut (input clk, input addr, rw, data_in, output data_out);
    modport tb(input clk, data_out, output addr, rw, data_in);
endinterface

//  Sample dut using interface defined above
module sample_dut(intf.dut vif);
    reg [7:0] mem [0:15];
    always_ff @(posedge vif.clk) begin
        if(vif.rw) begin
            mem[vif.addr] <= vif.data_in;
        end
        else begin
            vif.data_out <= mem[vif.addr];
        end
    end
endmodule

//  Test bench top where we are instantiating interface and
//  connecting dut to the test bench. Inside drv block the inputs
//  to DUT are driven and inside monitor block the signals are
//  sampled and monitored.
module tb_top;
    bit clk;
    intf vif(clk);

    sample_dut dut(vif);

    always #10 clk = ~clk;

    initial begin: drive
        @(negedge clk);
        vif.rw = 1'b1;
        vif.addr = 4'ha;
        vif.data_in = 8'h2f;

        #20;
        @(negedge clk);
        vif.rw = 1'b1;
        vif.addr = 4'h3;
        vif.data_in = 8'h11;

        #20;
        @(negedge clk);
        vif.rw = 1'b0;
        vif.addr = 4'h3;
    end

    always@(posedge clk) begin: monitor
        $display("Sampled signals at t = %0t", $time);
        $strobe("rw = %b, addr = %0h, data_in= %0h, data_out = %0h",
                 vif.rw, vif.addr, vif.data_in, vif.data_out);
    end
endmodule
Output
# Sampled signals at t = 10
# rw = x, addr = x, data_in= x, data_out = x
# Sampled signals at t = 30
# rw = 1, addr = a, data_in= 2f, data_out = x
# Sampled signals at t = 50
# rw = 1, addr = 3, data_in= 11, data_out = x
# Sampled signals at t = 70
# rw = 0, addr = 3, data_in= 11, data_out = 11
# Sampled signals at t = 90
# rw = 0, addr = 3, data_in= 11, data_out = 11