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
  • 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
    • Reusable Testbench Architecture
      • Policy Classes
      • Abstract BFM
      • Parameterized Interfaces
      • Interface Classes
      • Layered Stimulus
    • Checking Out-of-Order Traffic
    • Assertion Depth
    • Coverage Modelling for Real Features
    • Capstone: Out-of-Order Memory Controller
    • Principal Judgment
Loading lesson

‹Reusable Testbench Architecture
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Reusable Testbench Architecture
  5. Parameterized Interfaces

Part 9 · Principal-Level Verification · Advanced

Parameterized Interfaces

Why virtual bus_if #(W) spreads through an environment, and how typedef specializations, config objects and elaboration-time width checks keep it under control.

On this page

    Why the parameter spreads

    A virtual interface type includes the interface's parameter values. virtual bus_if #(8) and virtual bus_if #(32) are different types, and one cannot be assigned to the other. Verilator rejects such an assignment at compile time with a message that the right-hand side is a different interface specialization.

    So any class that stores the handle must name a specialization. Write it generically and the class needs its own parameter, and so does every class that holds that class. The width ends up on the driver, monitor, agent, environment, config object and test.

    Diagram
    test #(W) ─► env #(W) ─► agent #(W) ─► driver #(W) ─► virtual bus_if #(W)
                                         └──► monitor #(W) ─► virtual bus_if #(W)
      One width change = every #(W) in the chain must agree, or the build fails.

    Four ways to contain it

    Technique

    What it fixes

    What remains

    typedef per specialization

    Widths are spelled once: typedef virtual bus_if #(.W(32)) bus32_vif;

    Classes still carry #(W)

    Config object carrying the vif

    One object per bus instance; agents receive a cfg, not loose handles

    The cfg class is parameterized

    Max-width interface plus runtime width

    One interface type, W_MAX bits, a runtime active width

    Unused upper bits, and every check must mask

    Abstract BFM (previous lesson)

    The environment holds no vif and no parameter

    An API to design and maintain

    Parameterized classes are the right tool when the width changes data types inside the class, for example a typedef bit [W-1:0] data_t that the driver uses. The cost is that every specialization is a separate class type, so a queue cannot hold drivers of different widths.

    Runnable example: two specializations, one guard

    Two memories, 8 and 32 bits. A typedef names each virtual interface specialization once. A parameterized config object carries the handle and its name, and a parameterized driver writes four patterns that light the top bit and the top byte, then reads them back.

    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    interface bus_if #(parameter int W = 8) (input logic clk);
      // Elaboration-time guard: reported while the design is built.
      if (W % 8 != 0 || W > 64) begin : g_bad_width
        $error("bus_if: W must be a multiple of 8 and at most 64");
      end
      // Run-time backstop, in case the build was told to treat errors as warnings.
      initial if (W % 8 != 0 || W > 64) $fatal(1, "FAIL: bus_if built with W=%0d", W);
      logic         en, we;
      logic [7:0]   addr;
      logic [W-1:0] wdata, rdata;
    endinterface
    
    module mem #(parameter int W = 8) (
      input  logic clk, en, we,
      input  logic [7:0] addr,
      input  logic [W-1:0] wdata,
      output logic [W-1:0] rdata
    );
      logic [W-1:0] m [256];
      always_ff @(posedge clk)
        if (en) begin
          if (we) m[addr] <= wdata;
          else    rdata   <= m[addr];
        end
    endmodule
    
    // One typedef per specialization: the only place the widths are spelled out.
    typedef virtual bus_if #(.W(8))  bus8_vif;
    typedef virtual bus_if #(.W(32)) bus32_vif;
    
    // The config object carries the handle; W travels with the type.
    class bus_cfg #(int W = 8);
      virtual bus_if #(.W(W)) vif;
      string name;
      function new(string name, virtual bus_if #(.W(W)) vif);
        this.name = name; this.vif = vif;
      endfunction
    endclass
    
    class bus_driver #(int W = 8);
      typedef bit [W-1:0] data_t;
      bus_cfg #(W) cfg;
      int errors;
      function new(bus_cfg #(W) cfg); this.cfg = cfg; endfunction
      task write(bit [7:0] a, data_t d);
        @(negedge cfg.vif.clk);
        cfg.vif.en <= 1; cfg.vif.we <= 1; cfg.vif.addr <= a; cfg.vif.wdata <= d;
        @(negedge cfg.vif.clk);
        cfg.vif.en <= 0; cfg.vif.we <= 0;
      endtask
      task read(bit [7:0] a, output data_t d);
        @(negedge cfg.vif.clk);
        cfg.vif.en <= 1; cfg.vif.we <= 0; cfg.vif.addr <= a;
        @(negedge cfg.vif.clk);
        cfg.vif.en <= 0;
        d = cfg.vif.rdata;
      endtask
      // Patterns that light the top byte catch any width truncation on the path.
      task run();
        data_t pat [4] = '{'1, {1'b1, {(W-1){1'b0}}}, data_t'(64'hA5A5_5A5A_C3C3_3C3C), '0};
        data_t got;
        foreach (pat[i]) write(8'(i), pat[i]);
        foreach (pat[i]) begin
          read(8'(i), got);
          if (got !== pat[i]) begin
            errors++;
            $display("%s addr %0d: got %h exp %h", cfg.name, i, got, pat[i]);
          end
        end
      endtask
    endclass
    
    typedef bus_driver #(8)  drv8_t;
    typedef bus_driver #(32) drv32_t;
    

    The patterns are chosen to expose truncation: all ones, only the top bit, and a mixed pattern. A path that drops upper bits passes a random test with small values more often than you would expect.

    Mutant run. Instantiating the 32-bit memory with mem #(.W(8)), a parameter that was not propagated, still compiles: Verilator reports the port width mismatch as a WIDTHTRUNC warning, and the course runner's -Wno-fatal lets the build continue. The bench fails with 3 mismatches, for example got 0000003c exp c3c33c3c.

    Elaboration-time width checks

    The $error inside the generate if is an elaboration system task. The LRM says that an elaboration-time $error lets elaboration continue so that further errors are reported, but no simulation runs. Tools differ in how strictly they apply that:

    Tool (as tested here)

    bus_if #(.W(12))

    Icarus 13

    Elaboration error, no simulation. Accepts only a single string argument for elaboration tasks.

    Verilator 5.048, default

    USERERROR warning, which stops the build because warnings are fatal by default.

    Verilator 5.048 with -Wno-fatal

    The warning is printed and the simulation still runs.

    The course runner uses -Wno-fatal, which is common in real flows too. That is why the interface also has a run-time $fatal backstop. With W set to 12, Verilator prints the elaboration message and the run then ends with FAIL: bus_if built with W=12 at time 0.

    Common pitfall

    Trusting an elaboration $error in a flow that downgrades errors or warnings. Check what your simulator does with elaboration tasks under your regression switches, and add a time-0 run-time check if the build can continue.

    Interview

    Likely question: “Why can’t I assign virtual bus_if #(16) to a virtual bus_if #(8) variable, and how do you live with it?” Answer: parameter values are part of the virtual interface type. Contain it with one typedef per specialization and a config object that carries the handle, or remove it from the environment with an abstract BFM.

    Check yourself A team uses a 64-bit interface for all variants and passes the active width in a config field. Name one benefit and one risk.

    Benefit: one interface type, so no parameter spreads through the classes. Risk: upper bits that the DUT does not drive are still visible, so every driver, monitor and scoreboard must mask to the active width, and a missed mask shows up as spurious mismatches or hidden bugs.

    Check yourself Why does the bench store the vif in a config object instead of passing it straight to the driver?

    The config object groups everything that describes one bus instance (handle, name, later timeouts or modes). Agents then take one argument that can grow without changing constructor signatures, and the vif type is named in one place.

    Related topics

    • Previous: Abstract BFM

    • Next: Interface Classes

    • Multi-Instance & Parameterized vifs (Part 4)

    • Parameterized Classes

    Previous

    234 / 259

    Next: Interface Classes

    Up next

    Up nextInterface Classes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Policy Classes
    • Layered Stimulus