Interfaces
Verilog's Modules and Ports showed named port connection (.port_name(signal_name)) as the safe way to wire a module instance — but that safety comes at a cost that grows with the design: every module in a bus's path has to declare, and every instantiation has to connect, every single signal in that bus, one at a time. A simple ready/valid handshake bus with data, valid, ready, and a few control bits, threaded through five modules, means the same five-or-so signal names typed out at every one of those five module boundaries. SystemVerilog's interface bundles all of it into one named unit, connected with a single port.
Declaring an interface
interface simple_bus_if (input logic clk);
logic [7:0] data;
logic valid;
logic ready;
endinterface
This looks like a module declaration, and in one sense it is one — an interface is compiled once and instantiated, exactly like a module, and its internal signals are real wires connecting whatever's plugged into it.
Using an interface: one port instead of many
module producer (simple_bus_if.producer bus);
always_comb begin
bus.data = 8'hA5;
bus.valid = 1'b1;
end
endmodule
module consumer (simple_bus_if.consumer bus);
always_ff @(posedge bus.clk) begin
if (bus.valid && bus.ready)
$display("received %0h", bus.data);
end
endmodule
module top;
logic clk;
simple_bus_if bus (.clk(clk));
producer p (.bus(bus));
consumer c (.bus(bus));
endmodule
producer and consumer each declare a single port of type simple_bus_if instead of data, valid, ready, and clk individually — and top connects the whole bundle with one .bus(bus) instead of four separate signal connections. Adding a fifth signal to the bus later means editing the interface declaration once; every module using simple_bus_if picks it up automatically, instead of every module's port list and every instantiation needing the same edit repeated.
Modports: restricting direction per role
The .producer/.consumer suffixes on the port types above are modports — named views into the interface that restrict which signals a given module can drive versus only read:
interface simple_bus_if (input logic clk);
logic [7:0] data;
logic valid;
logic ready;
modport producer (input clk, output data, output valid, input ready);
modport consumer (input clk, input data, input valid, output ready);
endinterface
Without a modport, every module connected to the interface could drive every signal in it — which defeats the whole point of a producer/consumer handshake, where each side is only supposed to drive its half. With producer/consumer modports declared, the compiler enforces the intended direction for each role: producer attempting to write bus.ready (the consumer's signal to drive) is now a compile error, not a silent multi-driver bug waiting to be found in simulation.
Interfaces can bundle behavior too, not just signals
Everything above bundles only signals — but an interface can also declare tasks and functions that operate on those signals, sharing the same encapsulation benefit: write the logic once, and every module connected to the interface can call it instead of re-implementing the same sequence of signal reads/writes itself.
interface simple_bus_if (input logic clk);
logic [7:0] data;
logic valid;
logic ready;
task automatic wait_for_transfer(output logic [7:0] got);
@(posedge clk iff (valid && ready));
got = data;
endtask
endinterface
Any module or class holding a handle to simple_bus_if can now call bus.wait_for_transfer(d) instead of writing the same @(posedge clk iff ...) handshake wait inline everywhere it's needed — the same one-copy-instead-of-many payoff modports and shared signals already provide, just for behavior instead of wiring.
Parameterized interfaces: one declaration, multiple widths
Like a module, an interface can take a parameter, most commonly to make its bus width reusable across DUTs that need different widths from the same handshake shape:
interface sized_bus_if #(parameter WIDTH = 8) (input logic clk);
logic [WIDTH-1:0] data;
logic valid;
logic ready;
endinterface
sized_bus_if #(.WIDTH(16)) wide_bus (.clk(clk)); // a 16-bit instance
sized_bus_if #(.WIDTH(8)) narrow_bus(.clk(clk)); // an 8-bit instance, same declaration
Both instances come from the exact same interface declaration — only WIDTH differs per instantiation, the same override-the-default-at-instantiation pattern a parameterized module already uses.
Why this matters most for verification
Interfaces are used in RTL, but they earn their keep even more in testbenches — a DUT with a wide bus is connected to a virtual interface handle inside a class-based testbench (once Section C introduces classes), letting a driver/monitor class manipulate an entire bus through one handle rather than juggling dozens of individually-passed signals. Testbench and UVM both lean on this pattern heavily — it's one of the main reasons interface exists in the language at all, not just a port-list convenience for RTL. The word "virtual" here means pointer, the same sense it has for a class handle: a virtual simple_bus_if vif; declaration is a reference to a real, already-instantiated interface, not a second copy of one — and, exactly like an uninitialized class handle, an uninitialized (null) virtual interface used before it's assigned crashes simulation the moment code tries to drive or sample through it, so testbench code that receives one (typically via uvm_config_db) needs to check it was actually set before relying on it.
What's next
The last piece of Section B is package — how related typedefs, parameters, functions, and enums (like the frame_state_e enum from Data Types) get shared across multiple files without copy-pasting them or relying on `include duplication.