Here is a real-life example (I'm sure any more-or-less experienced designer will recognize the bus):
// Interface: axi4_lite_intf
//
interface axi4_lite_intf #(
parameter DATA_WIDTH = 32,
parameter ADDRESS_WIDTH = 32
);
//read address channel
logic [ADDRESS_WIDTH-1:0] araddr;
logic [`AXI4_MM_PROT-1:0] arprot;
logic arvalid;
logic arready;
//read data channel
logic [DATA_WIDTH-1:0] rdata;
logic [`AXI4_MM_RESP-1:0] rresp;
logic rvalid;
logic rready;
//write address channel
logic [ADDRESS_WIDTH-1:0] awaddr;
logic [`AXI4_MM_PROT-1:0] awprot;
logic awvalid;
logic awready;
//write data channel
logic [DATA_WIDTH-1:0] wdata;
logic [DATA_WIDTH/8-1:0] wstrb;
logic wvalid;
logic wready;
//write response channel
logic [`AXI4_MM_RESP-1:0] bresp;
logic bvalid;
logic bready;
modport slave( input araddr, arprot, arvalid,
rready,
awaddr, awprot, awvalid,
wdata, wstrb, wvalid,
bready,
output arready,
rdata, rresp, rvalid,
awready,
wready,
bresp, bvalid);
modport master( output araddr, arprot, arvalid,
rready,
awaddr, awprot, awvalid,
wdata, wstrb, wvalid,
bready,
input arready,
rdata, rresp, rvalid,
awready,
wready,
bresp, bvalid);
endinterface: axi4_lite_intf
Here is a module declaration using that interface:
module gpio #(
parameter ADDR_WIDTH = 64,
parameter DATA_WIDTH = 64,
parameter GPIO0_WIDTH = 6,
parameter GPIO1_WIDTH = 4
)(
input clk,
input axi_areset,
axi4_lite_intf.slave bus,
output [GPIO0_WIDTH-1:0] gpio0_pins,
output [GPIO1_WIDTH-1:0] gpio1_pins
);
And here is instantiation of that module:
axi4_lite_intf #(.ADDRESS_WIDTH(ADDR_WIDTH), .DATA_WIDTH(DATA_WIDTH)) periph_bus();
//<....>
gpio #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH)
) gpio_inst(
.clk(clk_periph),
.axi_areset(axi_areset),
.bus(periph_bus),
.gpio0_pins(gpio0_pins),
.gpio1_pins(gpio1_pins)
);
Imagine what the code would look like if all interface wires will be declared individually
This radically reduces the amount of noise in the code, and so dramatically improves readability.
Modports define direction of a wire (input or output), and also make it clear which side of the bus (master or slave) the module expects to be connected to. Of course there can be more than two modports - imagine a control bus which connects to a control module on one end, and to all slave modules will have their own modports. This makes it clear which wire goes to which module, also adding new signals is trivial - you declare it in the interface and in required modports, and there is no need to make any other changes in the wiring up. So modports provide a level of abstraction above wires, kinda like interfaces in Vivado's IPI (you can think of interface as a cable, and modports as connectors on the ends of this cable). This makes working with large buses (like AXI) MUCH easier because there is much less noise from all individual wires' declarations. Just think about how many wires would you have to deal with in typical peripheral interconnect with a dozen of AXI peripherals and few masters (like CPU cores, DMA modules)!!