SystemVerilog · Module 8
Interface Arrays & Parameterised Interfaces
Scaling testbenches to N channels with interface arrays, parameterised widths, multi-channel UVM patterns, and per-channel coverage.
Module 8 · Page 8.5
How to declare arrays of interfaces for multi-channel designs, connect them with generate loops, use virtual interface arrays inside classes, and parameterise bus widths directly inside an interface declaration.
Why Interface Arrays?
Real designs rarely have just one instance of a bus. A crossbar switch might have 8 AXI master ports and 4 slave ports. An interrupt controller connects to 32 interrupt sources. A multi-core SoC has one APB bus per core. Without interface arrays, you declare each interface separately — and the pattern repeats painfully:
// ✗ 4 APB slaves: 4 separate interface declarations — hard to scale
apb_if apb0 (.pclk(clk), .presetn(rst_n));
apb_if apb1 (.pclk(clk), .presetn(rst_n));
apb_if apb2 (.pclk(clk), .presetn(rst_n));
apb_if apb3 (.pclk(clk), .presetn(rst_n));
apb_slave u_slv0 (.bus(apb0.slave));
apb_slave u_slv1 (.bus(apb1.slave));
apb_slave u_slv2 (.bus(apb2.slave));
apb_slave u_slv3 (.bus(apb3.slave));
// Adding a 5th slave means 2 more lines everywhere — error-prone.
// ✓ With interface array: one declaration, one generate loop
apb_if apb [4] (.pclk(clk), .presetn(rst_n)); // 4 interfaces, one line
genvar i;
generate
for (i = 0; i < 4; i++) begin : gen_slaves
apb_slave u_slv (.bus(apb[i].slave));
end
endgenerate
// Adding a 5th slave: change 4→5 in one place. Done.Declaring Interface Arrays — Full Syntax
A 4-channel APB bus declared as apb_if apb [4] produces four independent interface elements, each connected to the same pclk and presetn. Each element drives a separate slave module: apb[0].slave → u_slave0, apb[1].slave → u_slave1, apb[2].slave → u_slave2, apb[3].slave → u_slave3. Every element is a full, independent interface instance.
// ── Fixed-size interface array ────────────────────────────────────────
apb_if apb [4] (.pclk(clk), .presetn(rst_n)); // indices 0 to 3
// ── Explicit range ─────────────────────────────────────────────────────
apb_if apb [1:4] (.pclk(clk), .presetn(rst_n)); // indices 1 to 4
// ── Using a parameter for the count ───────────────────────────────────
localparam N_SLAVES = 4;
apb_if apb [N_SLAVES] (.pclk(clk), .presetn(rst_n));
// ── Accessing individual elements ─────────────────────────────────────
apb[0].slave // element 0, slave modport
apb[2].paddr // element 2, direct signal access
apb[i].master // element i, master modport (in a loop)
// ── Connect to modules individually ───────────────────────────────────
apb_slave u_uart (.bus(apb[0].slave));
apb_slave u_gpio (.bus(apb[1].slave));
// ── Connect to modules via generate loop ──────────────────────────────
genvar i;
generate
for (i = 0; i < N_SLAVES; i++) begin : gen_slaves
apb_slave u_slv (.bus(apb[i].slave));
end
endgenerateapb_if apb [4]— declares 4 independent interface instances. The port connections in the( )after the array declaration apply to ALL elements — they all share the samepclkandpresetn.apb[i]— index into the array to get one interface element. Each element is a fully independent interface — its signals are independent from all other elements.apb[i].slave— apply a modport to a specific element. The modport is appended after the index, exactly as with a non-array interface.genvar+generate— the standard way to connect N modules to N interface elements with a loop. The generated instances are namedgen_slaves[0].u_slv,gen_slaves[1].u_slv, etc. — accessible in waveform viewers.
Virtual Interface Arrays — In Classes
Just as a regular virtual interface handle points to one interface instance, a virtual interface array holds an array of handles — one per channel. This is the pattern used in multi-channel agents and scoreboards that need to observe or drive multiple buses from a single class.
// ─── Multi-channel scoreboard class ───────────────────────────────────
class apb_scoreboard;
parameter N = 4;
// Array of virtual interface handles — one per APB channel
virtual apb_if vif [N];
function new(virtual apb_if vif [N]);
this.vif = vif; // whole array assigned in one line
endfunction
task monitor_all();
fork
begin : ch0 watch_channel(0); end
begin : ch1 watch_channel(1); end
begin : ch2 watch_channel(2); end
begin : ch3 watch_channel(3); end
join
endtask
task watch_channel(input int ch);
forever begin
@(vif[ch].cb); // wait on channel ch's clocking block
if (vif[ch].cb.psel && vif[ch].cb.penable && vif[ch].cb.pready)
$display("[SCB] ch%0d: addr=%h data=%h",
ch, vif[ch].cb.paddr,
vif[ch].cb.pwrite ? vif[ch].cb.pwdata : vif[ch].cb.prdata);
end
endtask
endclass
// ─── TB top: pass the array to the class ──────────────────────────────
module tb_top;
logic clk = 0, rst_n;
apb_if apb [4] (.pclk(clk), .presetn(rst_n)); // 4 interface instances
apb_scoreboard scb;
initial begin
scb = new(apb); // pass entire array — all 4 handles at once
fork scb.monitor_all(); join_none
rst_n = 0; #20 rst_n = 1;
// ... drive stimulus ...
#1000 $finish;
end
always #5 clk = ~clk;
endmoduleParameterised Interfaces
An interface can have parameters — exactly like a parameterised module. This lets you define the bus width, address width, data width, and any other structural attribute once in the interface declaration, and then instantiate different configurations without copy-pasting.
| Variant | paddr | pwdata | prdata |
|---|---|---|---|
apb_if #(32, 32) | [31:0] | [31:0] | [31:0] |
apb_if #(16, 8) | [15:0] | [7:0] | [7:0] |
apb_if #(32, 64) | [31:0] | [63:0] | [63:0] |
All three are instances of the same apb_if interface — just with different parameter values. One interface definition supports the entire APB width family.
// ─── Parameterised interface ───────────────────────────────────────────
interface apb_if #(
parameter int ADDR_W = 32, // address bus width
parameter int DATA_W = 32 // data bus width
) (input logic pclk, presetn);
logic [ADDR_W-1:0] paddr; // width set by ADDR_W parameter
logic [DATA_W-1:0] pwdata, prdata;
logic psel, penable, pwrite, pready, pslverr;
logic [DATA_W/8-1:0] pstrb; // byte strobes = DATA_W/8
modport master (input pclk, presetn, prdata, pready, pslverr,
output paddr, psel, penable, pwrite, pwdata, pstrb);
modport slave (input pclk, presetn, paddr, psel, penable, pwrite, pwdata, pstrb,
output prdata, pready, pslverr);
modport monitor (input pclk, presetn, paddr, psel, penable, pwrite,
pwdata, pstrb, prdata, pready, pslverr);
endinterface
// ─── Instantiate with different widths ────────────────────────────────
// Default 32-bit address, 32-bit data
apb_if apb32 (.pclk(clk), .presetn(rst_n));
// 16-bit address, 8-bit data (e.g. a tiny peripheral sub-bus)
apb_if #(.ADDR_W(16), .DATA_W(8)) apb8 (.pclk(clk), .presetn(rst_n));
// 32-bit address, 64-bit data (wide data path for DMA)
apb_if #(.ADDR_W(32), .DATA_W(64)) apb64 (.pclk(clk), .presetn(rst_n));
// Array of parameterised interfaces — all the same width variant
apb_if #(.ADDR_W(32), .DATA_W(32)) apb [4] (.pclk(clk), .presetn(rst_n));Parameterised Interface in a Class
A class that holds a virtual handle to a parameterised interface needs to know the parameter values at compile time. The cleanest way is to make the class itself parameterised, and use the parameter to type the virtual interface handle.
// ─── Parameterised driver class ────────────────────────────────────────
class apb_driver #(parameter int ADDR_W = 32, DATA_W = 32);
// Virtual interface typed with the same parameters
virtual apb_if #(ADDR_W, DATA_W) vif;
function new(virtual apb_if #(ADDR_W, DATA_W) vif);
this.vif = vif;
endfunction
task write(input logic [ADDR_W-1:0] addr,
input logic [DATA_W-1:0] data);
@(vif.cb);
vif.cb.paddr <= addr;
vif.cb.pwdata <= data;
vif.cb.psel <= 1;
vif.cb.pwrite <= 1;
@(vif.cb);
vif.cb.penable <= 1;
while (!vif.cb.pready) @(vif.cb);
@(vif.cb);
vif.cb.psel <= 0;
vif.cb.penable <= 0;
endtask
endclass
// ─── Instantiate with matching parameters ─────────────────────────────
apb_if #(.ADDR_W(32),.DATA_W(32)) apb32 (.pclk(clk),.presetn(rst_n));
apb_if #(.ADDR_W(16),.DATA_W(8)) apb8 (.pclk(clk),.presetn(rst_n));
apb_driver #(32, 32) drv32 = new(apb32);
apb_driver #(16, 8) drv8 = new(apb8);Full Example — Multi-Channel SoC Testbench
Putting it all together: a SoC testbench with 4 parameterised APB slave interfaces, one AXI-Lite master interface, a scoreboard observing all 4 APB channels via a virtual interface array, and a driver for each channel.
module soc_tb_top;
localparam N_APB = 4;
localparam ADDR_W = 32, DATA_W = 32;
logic clk = 0, rst_n;
// ── Array of parameterised APB interfaces ────────────────────────
apb_if #(ADDR_W, DATA_W) apb [N_APB] (.pclk(clk), .presetn(rst_n));
// ── AXI-Lite interface (different bus) ───────────────────────────
axi_lite_if #(.DW(64), .AW(32)) axi (.aclk(clk), .aresetn(rst_n));
// ── DUT: SoC with 4 APB slaves and one AXI master ────────────────
soc_dut #(.N_APB(N_APB)) u_dut (
.clk(clk), .rst_n(rst_n),
.axi_if(axi.slave)
);
// Generate: connect each APB slave inside DUT to matching interface
genvar i;
generate
for (i = 0; i < N_APB; i++) begin : g_apb_conn
assign apb[i].paddr = u_dut.apb_paddr[i];
assign apb[i].psel = u_dut.apb_psel[i];
assign apb[i].penable = u_dut.apb_penable;
assign u_dut.apb_prdata[i] = apb[i].prdata;
end
endgenerate
// ── TB classes ───────────────────────────────────────────────────
apb_scoreboard #(N_APB) scb;
apb_driver #(ADDR_W, DATA_W) drv [N_APB];
axi_driver axi_drv;
initial begin
// Create scoreboard — passes entire interface array
scb = new(apb);
// Create one driver per APB channel
foreach (drv[i])
drv[i] = new(apb[i]);
// Create AXI driver
axi_drv = new(axi.master);
rst_n = 0; #20 rst_n = 1;
// Run all in parallel
fork
scb.monitor_all();
begin
drv[0].write(32'h1000, 32'h01);
drv[1].write(32'h2000, 32'hFF);
drv[2].write(32'h3000, 32'hAA);
drv[3].write(32'h4000, 32'h55);
end
join_any
#200 $finish;
end
always #5 clk = ~clk;
endmoduleCommon Mistakes & How to Fix Them
// ════ MISTAKE 1: Connecting clock/reset to every array element manually
// ❌ apb_if apb [4]; (no port connection)
// apb[0].pclk = clk; apb[1].pclk = clk; ... (verbose, easy to miss)
// ✅ FIX: Always provide port connections in the array declaration
apb_if apb [4] (.pclk(clk), .presetn(rst_n));
// Connections apply to all elements simultaneously.
// ════ MISTAKE 2: Forgetting that interface array elements are independent
// ❌ apb[0].paddr = 32'h100;
// // Expecting apb[1].paddr to also be 32'h100 — NO, elements are independent.
// ✅ FIX: Each element is separate. To broadcast, assign in a loop:
foreach (apb[i]) apb[i].paddr = 32'h100;
// ════ MISTAKE 3: Mismatched parameters between interface and virtual interface
// ❌ virtual apb_if vif; (default parameters: 32/32)
// apb_if #(16, 8) apb8;
// vif = apb8; (type mismatch — compile error)
// ✅ FIX: Match parameter values exactly
virtual apb_if #(16, 8) vif;
// Or make the class parameterised and use:
// virtual apb_if #(ADDR_W, DATA_W) vif;
// ════ MISTAKE 4: Out-of-bounds index on interface array
// ❌ apb_if apb [4]; (indices 0–3)
// apb[4].paddr = ... (out-of-bounds!)
// ✅ FIX: Always use a localparam and bounded loop
localparam N = 4;
for (int i = 0; i < N; i++) begin
// safe — bounds tied to declared size
endQuick Reference — Arrays & Parameters at a Glance
// ── Parameterised interface ────────────────────────────────────────────
interface my_if #(parameter int DW = 32, AW = 32) (input logic clk);
logic [AW-1:0] addr;
logic [DW-1:0] data;
// ... modports, clocking blocks ...
endinterface
// ── Instantiate single (default params) ───────────────────────────────
my_if intf (.clk(clk));
my_if #(.DW(64)) intf64 (.clk(clk));
my_if #(.DW(8),.AW(16)) intf8 (.clk(clk));
// ── Interface array ────────────────────────────────────────────────────
localparam N = 4;
my_if intf [N] (.clk(clk)); // all N elements share same clock
intf[0].addr = 32'h1000; // access element 0
intf[i].slave // element i with modport
// ── Generate: connect N modules to N interface elements ───────────────
genvar i;
generate
for (i = 0; i < N; i++) begin : gen_ch
my_slave u (.bus(intf[i].slave));
end
endgenerate
// ── Virtual interface array in a class ────────────────────────────────
class my_monitor;
virtual my_if vif []; // dynamic array of virtual handles
function new(virtual my_if vif []); this.vif = vif; endfunction
endclass
// Pass array to class ──────────────────────────────────────────────────
my_monitor mon = new(intf); // entire array in one assignment
// ── Parameterised class for parameterised interface ───────────────────
class my_driver #(parameter int DW=32, AW=32);
virtual my_if #(DW, AW) vif;
function new(virtual my_if #(DW, AW) v); this.vif = v; endfunction
endclass
my_driver #(64, 32) drv64 = new(intf64);Verification Usage — Building UVM Agents That Scale with N Channels
A multi-channel DUT (4 AXI masters, 16 PCIe lanes, 8 DMA queues) needs a testbench that scales without N times the code. The pattern is one parameterised UVM agent class instantiated N times, each holding one element of a virtual interface array. All N agents share the same code; only their configuration object differs.
// ── Shared config package (covered in 7.4) ──────────────────
package soc_widths_pkg;
localparam int NUM_AXI_MASTERS = 4;
localparam int AXI_AW = 32;
localparam int AXI_DW = 128;
endpackage
// ── Parameterised interface ─────────────────────────────────
import soc_widths_pkg::*;
interface axi_if #(parameter int AW = AXI_AW, parameter int DW = AXI_DW)
(input logic clk);
logic [AW-1:0] awaddr, araddr;
logic [DW-1:0] wdata, rdata;
logic awvalid, awready, wvalid, wready, rvalid, rready;
clocking tb_cb @(posedge clk);
default input #1step output #0;
output awaddr, wdata, awvalid, wvalid, rready;
input awready, wready, rvalid, rdata;
endclocking
modport tb(clocking tb_cb);
endinterface
// ── TB top: array of interfaces, one config_db set per channel ──
module tb_top;
logic clk;
initial begin clk = 0; forever #5 clk = ~clk; end
axi_if u_axi [NUM_AXI_MASTERS] (.clk);
memory_subsystem u_dut (.axi(u_axi));
initial begin
foreach (u_axi[c]) begin
uvm_config_db#(virtual axi_if.tb)::set(
null,
$sformatf("uvm_test_top.env.agt_axi[%0d]", c),
"vif", u_axi[c]);
end
run_test();
end
endmodule
// ── Env: one agent per channel ──────────────────────────────
class memory_env extends uvm_env;
axi_agent agt_axi [NUM_AXI_MASTERS];
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
foreach (agt_axi[c])
agt_axi[c] = axi_agent::type_id::create(
$sformatf("agt_axi[%0d]", c), this);
endfunction
endclass
// Agent retrieves its own vif — config_db scope per-channel match
class axi_agent extends uvm_agent;
virtual axi_if.tb vif;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual axi_if.tb)::get(
this, "", "vif", vif))
`uvm_fatal("NOVIF",
{"No vif for ", get_full_name()})
endfunction
endclassSimulation Behavior — Interface Array Indexing and Parameter Resolution
Interface arrays mix two scheduling phases. The array size and parameters are resolved at elaboration — the number of channels, each channel's width, the modports — all baked into the netlist before time 0. The per-channel access u_axi[c].signal is runtime when c is a variable; the simulator selects which element's storage to read or write each cycle. Both phases participate; both can fail.
interface bus_if #(parameter int W = 8) (input logic clk);
logic [W-1:0] data;
initial $display("[%0t] %m elaborated W=%0d $bits(data)=%0d",
$time, W, $bits(data));
endinterface
module tb;
// Array of 4 bus_if instances at default W=8
bus_if u_bus_a [4](.clk);
// Array of 2 bus_if instances at W=32 — uses #() on the type
bus_if #(.W(32)) u_bus_b [2](.clk);
initial begin
// Compile-time constant index → resolved at elaboration:
$display("u_bus_a[0].W = %0d", $bits(u_bus_a[0].data));
// Runtime index → selected each cycle:
for (int c = 0; c < 4; c++)
$display("u_bus_a[%0d].data ptr addr = (varies)", c);
end
endmodule
// Expected output at time 0 (one $display per elaborated instance):
// [0] tb.u_bus_a[0] elaborated W=8 $bits(data)=8
// [0] tb.u_bus_a[1] elaborated W=8 $bits(data)=8
// [0] tb.u_bus_a[2] elaborated W=8 $bits(data)=8
// [0] tb.u_bus_a[3] elaborated W=8 $bits(data)=8
// [0] tb.u_bus_b[0] elaborated W=32 $bits(data)=32
// [0] tb.u_bus_b[1] elaborated W=32 $bits(data)=32Waveform Analysis — Per-Channel Signals in the Hierarchy
Each element of an interface array becomes its own hierarchical scope: tb_top.u_axi[0], tb_top.u_axi[1], …, tb_top.u_axi[N-1]. Every signal under each scope is browseable independently in Verdi/DVE. Set up signal groups that nest by channel — one group per channel containing the bundle of bus signals — and channel-N debug becomes a one-click operation.
Hierarchy in Verdi (after browsing tb_top.u_axi):
tb_top.u_axi
├── [0] ← channel 0
│ ├── awaddr
│ ├── wdata
│ ├── awvalid
│ ├── awready
│ └── ...
├── [1] ← channel 1
│ └── ... same set of signals
├── [2] ← channel 2
│ └── ...
└── [3] ← channel 3
└── ...
Saved signal group "AXI per channel":
Channel 0
tb_top.u_axi[0].awaddr ──── 100 ──── 200 ────
tb_top.u_axi[0].awvalid ___/‾‾‾___/‾‾‾___
Channel 1
tb_top.u_axi[1].awaddr ____ 300 ____
tb_top.u_axi[1].awvalid ___/‾‾‾___
Channel 2
tb_top.u_axi[2].awaddr (idle)
tb_top.u_axi[2].awvalid ___________________
Channel 3
tb_top.u_axi[3].awaddr ──────── 400 ────────
tb_top.u_axi[3].awvalid _________/‾‾‾‾‾‾_________
Read by channel: channel 2 isn't doing anything in this trace window;
channels 0/1/3 each show their own transactions independently.Industry Insights — Multi-Channel Patterns in Production Teams
Debugging Academy — 5 Real Interface Array Bugs
Each lab is a real failure mode pulled from production projects. Buggy code, symptom, root cause, fix.
Off-By-One — N-1 Agents Initialised, Last Channel Dead
LOOP BOUND// TB top — interface array of NUM_CHANNELS
axi_if u_axi [NUM_CHANNELS] (.clk);
initial begin
// ❌ wrong loop bound — last channel never gets a vif
for (int c = 0; c < NUM_CHANNELS - 1; c++) begin
uvm_config_db#(virtual axi_if.tb)::set(
null,
$sformatf("uvm_test_top.env.agt[%0d]", c),
"vif", u_axi[c]);
end
run_test();
endThe last channel's agent fires `uvm_fatal("NOVIF") in its build_phase. If the fatal guard is missing, the last channel's run_phase crashes with a null pointer when it first tries to use its vif. Channels 0..N-2 work fine; only the final channel is broken. Engineer initially suspects the DUT's last channel is faulty.
Classic off-by-one: c < NUM_CHANNELS - 1 iterates 0..N-2, missing the final element. The author was thinking "iterate to the last index, which is N-1" and expressed it as "less than N-1" instead of "less than N."
Use foreach: foreach (u_axi[c]) ... — the loop iterates over the actual array elements; no manual bound to get wrong. Standardise on this form for every channel-array operation. Off-by-one errors become structurally impossible.
config_db Scope String Doesn't Include the Channel Index
SCOPE COLLISION// TB top — wildcard scope catches ALL agents, not per-channel
initial begin
foreach (u_axi[c]) begin
uvm_config_db#(virtual axi_if.tb)::set(
null,
"uvm_test_top.env.agt[*]", // ← wildcard, not per-channel
"vif", u_axi[c]);
end
run_test();
end
// Result: only the LAST set call survives (each overwrites the previous).
// All agents get a handle to u_axi[NUM_CHANNELS-1].All N agents drive and monitor the same physical bus (the last channel). The waveform shows channels 0..N-2 silent, channel N-1 carrying all the traffic of all agents. Scoreboards report massive mismatches because every transaction lands on the wrong DUT channel.
uvm_config_db::set uses a literal scope string. When the scope is "uvm_test_top.env.agt[*]", every agent matches. Each successive set call overwrites the same scope/key entry; only the last one survives. All N agents fetch the same (last) handle.
Make the scope string per-channel: $sformatf("uvm_test_top.env.agt[%0d]", c). Each set call now publishes under a unique key; each agent fetches its own handle. Add a uvm_config_db::dump() at end of build_phase to verify the published keys; one per channel, no collisions.
Parameter Mismatch Between Interface Array Element and DUT Port
WIDTH DRIFT// TB top declares array at default widths (AW=32, DW=64)
axi_if u_axi [4] (.clk);
// DUT declares its port at different widths
module memory_subsystem(
axi_if #(.AW(40), .DW(128)) axi [4] // ← AW=40, DW=128
);
endmodule
memory_subsystem u_dut (.axi(u_axi)); // type mismatchElaboration error: "port 'axi' type mismatch: expected axi_if #(40,128)[4], got axi_if #(32,64)[4]." Clear error if the tool checks rigorously; some older tools silently allow the connection and produce N truncation warnings buried in the log — the actual bus operates at the narrower width.
The interface's parameter values are part of its type signature. Two arrays with different parameter values are incompatible types; connecting them either errors or silently truncates depending on tool strictness.
Source widths from one place — the shared SoC config package. Both the TB instantiation and the DUT port reference the package constants: axi_if #(.AW(AXI_AW), .DW(AXI_DW)) u_axi[NUM_CH](.clk); and matching on the DUT port. Width changes propagate from the package; both sides stay in sync automatically.
Virtual Interface Array Element Not Initialised — Channel-N Crash
SPARSE FETCHclass multi_channel_monitor extends uvm_monitor;
virtual axi_if.monitor vif [NUM_CHANNELS];
function void build_phase(uvm_phase phase);
// ❌ Only fetches the first 3 channels
for (int c = 0; c < 3; c++) begin
void'(uvm_config_db#(virtual axi_if.monitor)::get(
this,
$sformatf("channel_%0d", c),
"vif", vif[c]));
end
endfunction
task run_phase(uvm_phase phase);
foreach (vif[c]) begin
fork
forever begin
@(vif[c].mon_cb); // crashes for c=3 (null vif)
end
join_none
end
endtask
endclassMonitor fork-join_none threads for channels 0..2 work fine. The channel 3 thread crashes immediately with null-pointer on vif[3].mon_cb. Engineer initially suspects channel 3's clocking block is broken; actual bug is the build_phase didn't fetch channel 3's vif.
The build_phase loop bound (c < 3) doesn't match the array size (NUM_CHANNELS = 4). Some elements get their vif; others stay null. The run_phase iterates the full array and crashes on the unfetched element.
Use foreach (vif[c]) in build_phase too — same iteration scheme everywhere. Add an end-of-build_phase assertion: foreach (vif[c]) assert (vif[c] != null) else `` uvm_fatal("NOVIF", ...); ``. Catches the bug at time 0 instead of mid-test, with a clear "channel %d vif is null" message.
Heterogeneous Bundle Forced Into an Array — Type Coercion Failure
WRONG ABSTRACTION// Trying to bundle 3 AXI masters + 1 APB slave into one array
// (it doesn't work — different interface types)
interface generic_bus_if(input logic clk);
logic [31:0] addr;
logic [31:0] data;
logic valid;
// ... not really AXI, not really APB — neither protocol
endinterface
generic_bus_if u_buses [4](.clk); // 4 fake-generic buses
// DUT needs AXI on [0..2] and APB on [3]
// DUT must "decode" which slot is which protocolThe "generic_bus_if" doesn't have the right signals for either protocol. Code on both sides has to translate to/from the generic format, adding error-prone glue. Eventually a developer hardcodes "slot 3 is APB" and the whole abstraction is a leak. Reviewing the diff shows half the code is protocol-translation boilerplate.
Interface arrays assume homogeneous elements. Forcing a heterogeneous set requires fabricating a "common denominator" interface that isn't a real protocol — it has all signals from both protocols and the DUT picks the relevant subset. The result has the worst properties of both.
Keep them as separate types: axi_if u_axi[3](.clk); apb_if u_apb(.clk);. DUT port list: module memory_subsystem(axi_if axi [3], apb_if apb);. Each protocol's interface is full-fidelity; no translation needed. The code is longer at the boundary but every line is honest about what it's doing. This is the right abstraction.
Interview Q&A — 12 Questions on Interface Arrays & Parameterised Interfaces
Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.
Like an array of any other type: interface_type instance_name [N](port_connections);. Example: axi_if u_axi[4](.clk); declares 4 instances of axi_if, each connected to the same clk. Access each element by index: u_axi[0], u_axi[1], etc. Every element has its own independent storage cells for the interface's signals.
Best Practices — Interface Arrays & Parameterised Interfaces Rules
- Use interface arrays for true homogeneous multi-channel structures. Memory controller channels, NoC ports, vector lanes. One protocol, parametric count.
- Parameterise widths in the interface, not in every consumer. Interface owns the contract; consumers accept the typed interface and the widths flow through.
- Source NUM_CHANNELS from the SoC config package. One-line edit changes the channel count everywhere; no hardcoded literals.
- Use
foreachfor every channel-array operation. Loop bounds become structurally correct; off-by-one bugs disappear. - Per-channel scope strings in
config_db::set.$sformatf("uvm_test_top.env.agt[%0d]", c)— never wildcards that collapse N publishes to one. - Don't force heterogeneous interfaces into one array. Use separate arrays per protocol. Common-denominator wrappers leak abstraction.
- Match TB and DUT parameter values via shared package. Width drift between TB array and DUT port is one of the most common multi-channel bugs.
- Per-channel configuration objects. Each agent has its own cfg with its stimulus profile; don't share one global config.
- Per-channel coverage rolling up to a per-channel goal. Aggregate coverage across channels only after each channel reaches its individual goal.
- Save per-channel waveform groups in the repo. Multi-channel debug becomes one-click instead of scrolling through 40 lines.