A bus protocol is one logical thing. Classic Verilog shreds it into individual ports, repeated on every module and wired by hand at each level.
Each repetition risks a swapped, mis-sized, or forgotten signal. An interface restores the protocol as one named object.
// snippet: separate ports before the bundle, one object after it
// BEFORE: every signal a separate port, repeated everywhere
module dut_old (
input logic clk, rst_n,
input logic req,
output logic gnt,
input logic [31:0] addr,
inout wire [63:0] data,
output logic err
);
// ...
endmodule
// AFTER: the protocol is one named thing
interface bus_if (input logic clk, input logic rst_n);
logic req;
logic gnt;
logic [31:0] addr;
wire [63:0] data;
logic err;
endinterface
module dut (bus_if bus); // one port
assign bus.gnt = bus.req; // dot into the bundle
endmoduleAn interface is instantiated like a module. The instance elaborates into real nets that live at the instantiation point.
Each connected module declares a port of the interface type and reaches members with dot notation. Clocks and resets enter through the interface ports, so later clocking blocks can use them.
The raw drives in the testbench below are scaffolding: they change inputs between edges with # delays. Against a clocked DUT, a blocking drive at the sampling edge would race, so real testbenches drive and sample through a clocking block (Clocking Blocks).
A port declared as plain interface accepts any interface with the members the module uses. That flexibility defers type checking to the elaboration of each connection.
Named ports stay the default. Reserve generic ports for deliberately polymorphic utilities such as monitors.
// snippet: a generic utility port beside the preferred named port
// generic: accepts ANY interface with .clk and .err
module err_counter (interface i);
int count;
always @(posedge i.clk)
if (i.err) count++;
endmodule
// named (preferred): checked against bus_if at compile time
module bus_monitor (bus_if bus);
always @(posedge bus.clk)
if (bus.req && bus.gnt)
$display("[%0t] grant addr=%h", $time, bus.addr);
endmoduleThe monitor still samples on the raw clock; clocking discipline arrives in a later lesson. One name collision to file away: interface class is an object-oriented abstract type, unrelated to the hardware interface in this topic.
bus_if instance remove cross-wiring bugs?The signals travel together through one port. There is no per-signal list left to swap, mis-size, or forget.
bus.req to 1. A second instance, other, is connected to nothing. Does bus.gnt follow, and does other.gnt?bus.gnt goes to 1, because the DUT is connected to bus. other.gnt never changes: nothing drives that copy.
The full bundle plus a testbench that proves the sharing.
interface bus_if (input logic clk, input logic rst_n);
logic req;
logic gnt;
logic [31:0] addr;
wire [63:0] data;
logic err;
endinterface
module dut (bus_if bus);
assign bus.gnt = bus.req;
endmodule
module tb_if_basic;
timeunit 1ns; timeprecision 1ps;
logic clk = 0;
logic rst_n = 1;
bus_if bus (.clk(clk), .rst_n(rst_n)); // shared instance
bus_if other (.clk(clk), .rst_n(rst_n)); // separate copy
dut u_dut (.bus(bus));
int errors = 0;
initial begin
bus.req = 1'b0; bus.addr = 32'h0000_1000; #1;
if (bus.gnt !== 1'b0) errors++;
bus.req = 1'b1; #1;
if (bus.gnt !== 1'b1) errors++; // same instance: gnt follows req
if (bus.addr !== 32'h0000_1000) errors++; // address is visible too
if (other.req === 1'b1) errors++; // separate copy never sees req
if (other.gnt === 1'b1) errors++;
if (errors != 0) $fatal(1, "FAIL: %0d checks differed", errors);
$display("PASS: one shared instance communicates and a second copy stays isolated");
$finish;
end
endmodulePASS: one shared instance communicates and a second copy stays isolatedOne way to write it:
// PacketFIFO's bus: one byte per beat, valid/ready handshake, last on the final byte.
interface pkt_if (input logic clk, input logic rst_n);
logic valid, ready, last;
logic [7:0] data;
modport src (output valid, data, last, input ready, clk, rst_n); // sends bytes
modport snk (input valid, data, last, clk, rst_n, output ready); // receives bytes
endinterface
// A sink that is always ready and counts the beats it accepts.
module pkt_sink (pkt_if.snk p);
int beats = 0;
bit saw_last = 0;
assign p.ready = 1'b1;
always @(posedge p.clk)
if (p.rst_n && p.valid && p.ready) begin
beats <= beats + 1;
if (p.last) saw_last <= 1;
end
endmodule
module tb_pkt_if;
logic clk = 0, rst_n = 0;
always #5 clk = ~clk;
pkt_if bus (.clk(clk), .rst_n(rst_n)); // one shared instance
pkt_sink u_snk (.p(bus));
logic [7:0] pkt [0:3];
int errors = 0;
initial begin
pkt[0] = 8'h22; pkt[1] = 8'hA5; pkt[2] = 8'h3C; pkt[3] = 8'hBB;
bus.valid = 0; bus.last = 0; bus.data = 0;
@(negedge clk) rst_n = 1;
foreach (pkt[i]) begin // raw drives between edges for now
@(negedge clk);
bus.valid = 1;
bus.data = pkt[i];
bus.last = (i == 3);
end
@(negedge clk) bus.valid = 0;
if (u_snk.beats != 4 || !u_snk.saw_last) errors++;
if (errors == 0) $display("PASS: the sink accepted %0d beats through pkt_if and saw last", u_snk.beats);
else $fatal(1, "FAIL: sink saw %0d beats, last=%0b", u_snk.beats, u_snk.saw_last);
$finish;
end
endmodulePASS: the sink accepted 4 beats through pkt_if and saw lastRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 cannot run these benches: it rejects interface ports.