Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
      • Why Virtual Interfaces Exist
      • Declaring & Binding a vif
      • Driving & Sampling via Clocking Blocks
      • vif Configuration Patterns
      • Multi-Instance & Parameterized vifs
    • Events, Semaphores, Mailboxes and Processes
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Virtual Interfaces: the Class-to-Pins Bridge
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Virtual Interfaces: the Class-to-Pins Bridge
  5. Multi-Instance & Parameterized vifs

Part 2 · OOP for Verification · Intermediate

Multi-Instance & Parameterized vifs

Bind arrays of vifs to multi-port DUTs and handle parameterized interface types.

On this page

    Why this matters

    A two-port DUT needed two agents, and the second port was wired by copy and paste with one index left unchanged. A vif array and one loop remove the copy.

    After this lesson you can: collect interface instances into a vif array, bind one agent per port in a loop, and choose a strategy for parameterized interface widths.

    One loop for N ports

    A 2-port DUT instantiates two interfaces and needs two agents. A vif array turns the binding paths into one loop.

    Static instance arrays are hierarchy, not variables. Collect them into vif variables element by element, then loop downstream.

    bus_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    interface bus_if (input logic clk);
      logic [7:0] data;
      logic       valid;
    endinterface
    
    class agent;                       // one per port: here just a driver
      virtual bus_if vif;
      function new(virtual bus_if vif);
        this.vif = vif;
      endfunction
      task drive(bit [7:0] d);
        @(posedge vif.clk);
        vif.data  <= d;
        vif.valid <= 1'b1;
      endtask
    endclass
    
    class env;
      agent agts[2];
      function new(input virtual bus_if vifs[2]);
        foreach (agts[i])
          agts[i] = new(vifs[i]);      // per-port binding, one loop
      endfunction
    endclass
    
    module tb_ports;
      logic clk = 0;
      always #5 clk = ~clk;
    
      bus_if bif[2] (clk);             // two REAL instances for the 2-port DUT
      virtual bus_if vifs[2];          // dynamic side: an array of references
    
      initial begin
        env e;
        vifs[0] = bif[0];              // collect element by element
        vifs[1] = bif[1];
        e = new(vifs);
        fork
          e.agts[0].drive(8'h11);
          e.agts[1].drive(8'h22);
        join
        #1;
        if (bif[0].data == 8'h11 && bif[1].data == 8'h22)
          $display("PASS: 2 agents built in one loop each drove their own port");
        else $fatal(1, "FAIL: ports got %h and %h", bif[0].data, bif[1].data);
        $finish;
      end
    endmodule
    Terminal
    PASS: 2 agents built in one loop each drove their own port
    Instance arrays are hierarchy; collect them into a vif array, then loop.

    The constructor formal carries an explicit direction. Some compilers reject a bare unpacked-array formal, so input is written out.

    Parameters make types distinct

    A parameterized interface creates one type per parameter value. The matching vif types are equally distinct and never cross-assign.

    The compiler is protecting the widths. A 32-bit driver attached to 64-bit pins would be silently wrong, so the mismatch is a compile error.

    Predict first v32 is a virtual pbus_if #(32). Does v32 = bif64 compile, where bif64 is a pbus_if #(64) instance?

    No. Each parameterization is its own type, so a 32-bit vif cannot point at a 64-bit instance; the compiler protects the widths.

    pbus_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    interface pbus_if #(int WIDTH = 32) (input logic clk);
      logic [WIDTH-1:0] data;
      logic             valid;
    endinterface
    
    module tb_top;
      timeunit 1ns;
      timeprecision 1ps;
      logic clk = 0;
      always #5 clk = ~clk;
      pbus_if #(32) bif32 (clk);
      pbus_if #(64) bif64 (clk);
    
      virtual pbus_if #(32) v32;
      virtual pbus_if #(64) v64;
    
      initial begin
        v32 = bif32;     // OK: types match exactly
        v64 = bif64;
        // v32 = bif64;  // COMPILE ERROR: distinct types
        // v64 = v32;    // COMPILE ERROR: vifs never cross-assign
        if ($bits(v32.data) != 32 || $bits(v64.data) != 64)
          $fatal(1, "FAIL: vif widths differ from 32 and 64");
        $display("PASS: each vif matches its own interface width");
        $finish;
      end
    endmodule
    Terminal
    PASS: each vif matches its own interface width

    Two width strategies

    Typedef-per-width embraces the distinct types. Each driver specialization carries its matching vif, and mismatches fail at compile time.

    The listing below repeats pbus_if so it runs on its own.

    pbus_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    interface pbus_if #(int WIDTH = 32) (input logic clk);
      logic [WIDTH-1:0] data;
      logic             valid;
    endinterface
    
    class driver #(int WIDTH = 32);
      typedef virtual pbus_if #(WIDTH) vif_t;
      vif_t vif;
    
      function new(vif_t vif);
        this.vif = vif;
      endfunction
    
      task drive(bit [WIDTH-1:0] d);
        @(posedge vif.clk);
        vif.data  <= d;   // width checked at compile time; raw drive kept short on purpose
        vif.valid <= 1'b1;
      endtask
    endclass
    
    module tb_top;
      timeunit 1ns;
      timeprecision 1ps;
      logic clk = 0;
      always #5 clk = ~clk;
      pbus_if #(32) bif32 (clk);
      pbus_if #(64) bif64 (clk);
    
      initial begin
        driver #(32) d32;   // declare first, then construct in order
        driver #(64) d64;
        d32 = new(bif32);
        d64 = new(bif64);
        fork
          d32.drive(32'hA5A5_0001);
          d64.drive(64'h1_0000_0002);
        join
        #1;
        if (bif32.data !== 32'hA5A5_0001 || bif64.data !== 64'h1_0000_0002)
          $fatal(1, "FAIL: a driver specialization drove the wrong width or value");
        $display("PASS: 32- and 64-bit drivers each drove their own vif");
        $finish;
      end
    endmodule
    Terminal
    PASS: 32- and 64-bit drivers each drove their own vif

    Max-width-generic shares one interface at the widest port and carries the active width at run time. One agent serves every port, but width checks become manual and upper bits need masking.

    Strategy

    Wins

    Pays

    Typedef per width

    Compile-time width safety

    One class specialization per width

    Max-width generic

    Uniform agents and vif arrays

    Run-time width checks and tie-offs

    Common mistakes

    Common pitfall

    Mixing vifs of different parameterizations in one array is a compile error: they are unrelated types. Choosing max-width-generic and forgetting run-time width checks truncates narrow ports silently.

    Interview

    “Your DUT has 32- and 64-bit ports — one agent or two?” Present the trade-off: type safety per width versus uniform reusable infrastructure. Then pick from the project's width count and sharing needs.

    Check yourself v32 = bif64 fails to compile although both are the same interface. Why?

    Parameterization is part of the type. pbus_if #(32) and pbus_if #(64) are distinct types, so their vifs never cross-assign.

    Check yourself Construction driver #(32) d32 = new(bif32) sits at compilation-unit scope. What breaks?

    At $unit scope, bif32 is not visible: it is declared inside tb_top. Declare the handle in tb_top, either as a module item with an initializer or inside initial.

    Remember

    • Instance arrays are hierarchy; vif arrays are variables you can loop over.

    • Each interface parameterization is its own vif type.

    • Typedef per width gives compile-time safety; one max-width interface gives uniform agents.

    Rule of thumb: collect instances into vifs once, then loop.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Related topics

    • Next topic: Interprocess Communication

    Previous

    53 / 259

    Next: Events, Semaphores, Mailboxes and Processes

    Up next

    Up nextEvents, Semaphores, Mailboxes and Processes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why Virtual Interfaces Exist
    • Declaring & Binding a vif
    • Driving & Sampling via Clocking Blocks