Skip to content

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:

SystemVerilog — without interface arrays (painful)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✗ 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.

SystemVerilog — interface array declaration forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endgenerate
  1. apb_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 same pclk and presetn.
  2. 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.
  3. apb[i].slave — apply a modport to a specific element. The modport is appended after the index, exactly as with a non-array interface.
  4. genvar + generate — the standard way to connect N modules to N interface elements with a loop. The generated instances are named gen_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.

SystemVerilog — virtual interface array in a class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 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;
endmodule

Parameterised 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.

Variantpaddrpwdataprdata
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.

SystemVerilog — parameterised interface declaration and usage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 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.

SystemVerilog — parameterised class for parameterised interface
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── 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.

SystemVerilog — SoC TB top combining all techniques
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
 
endmodule

Common Mistakes & How to Fix Them

SystemVerilog — interface array mistakes and fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ 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
end

Quick Reference — Arrays & Parameters at a Glance

SystemVerilog — interface arrays and parameters quick reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

SystemVerilog — scalable multi-channel UVM env
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endclass

Simulation 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.

SystemVerilog — per-element parameter visibility and runtime indexing
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)=32

Waveform 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.

Expected waveform — 4-channel AXI memory controller
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

1

Off-By-One — N-1 Agents Initialised, Last Channel Dead

LOOP BOUND
Buggy Code
TB top — wrong loop bound on vif publish
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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();
end
Symptom

The 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.

Root Cause

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."

Fix

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.

2

config_db Scope String Doesn't Include the Channel Index

SCOPE COLLISION
Buggy Code
Wildcard scope catches ALL agents
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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].
Symptom

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.

Root Cause

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.

Fix

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.

3

Parameter Mismatch Between Interface Array Element and DUT Port

WIDTH DRIFT
Buggy Code
TB and DUT disagree on AW/DW
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 mismatch
Symptom

Elaboration 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.

Root Cause

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.

Fix

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.

4

Virtual Interface Array Element Not Initialised — Channel-N Crash

SPARSE FETCH
Buggy Code
build_phase fetches only first 3 channels
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class 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
endclass
Symptom

Monitor 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.

Root Cause

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.

Fix

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.

5

Heterogeneous Bundle Forced Into an Array — Type Coercion Failure

WRONG ABSTRACTION
Buggy Code
A fake-generic bus is neither AXI nor APB
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 protocol
Symptom

The "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.

Root Cause

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.

Fix

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

  1. Use interface arrays for true homogeneous multi-channel structures. Memory controller channels, NoC ports, vector lanes. One protocol, parametric count.
  2. Parameterise widths in the interface, not in every consumer. Interface owns the contract; consumers accept the typed interface and the widths flow through.
  3. Source NUM_CHANNELS from the SoC config package. One-line edit changes the channel count everywhere; no hardcoded literals.
  4. Use foreach for every channel-array operation. Loop bounds become structurally correct; off-by-one bugs disappear.
  5. Per-channel scope strings in config_db::set. $sformatf("uvm_test_top.env.agt[%0d]", c) — never wildcards that collapse N publishes to one.
  6. Don't force heterogeneous interfaces into one array. Use separate arrays per protocol. Common-denominator wrappers leak abstraction.
  7. 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.
  8. Per-channel configuration objects. Each agent has its own cfg with its stimulus profile; don't share one global config.
  9. Per-channel coverage rolling up to a per-channel goal. Aggregate coverage across channels only after each channel reaches its individual goal.
  10. Save per-channel waveform groups in the repo. Multi-channel debug becomes one-click instead of scrolling through 40 lines.