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
    • SystemVerilog Data Types
    • Arrays: Packed, Dynamic, Queues & Associative
    • Procedural Code & Scheduling
    • Operators, Expressions & Width Rules
    • Packages, Typedefs & Compilation
    • Interfaces, Modports & Clocking Blocks
      • Interface Basics
      • Modports
      • Clocking Blocks
      • Interface BFM
  • 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
Loading lesson

‹Interfaces, Modports & Clocking Blocks
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Language Foundations
  4. Interfaces, Modports & Clocking Blocks
  5. Interface BFM

Part 1 · Language Foundations · Intermediate

Interface BFM

Parameterize the bus bundle, move pin choreography into tasks, and call them from a directed test.

On this page

    Why this matters

    Every test re-wrote the same valid/ready handshake, each slightly differently, and one of them dropped the last byte. Written once inside the interface, the handshake is right everywhere.

    After this lesson you can: parameterize an interface, put the pin-level handshake in automatic tasks, and drive a DUT from a directed test through them.

    Parameters become part of the type

    Real buses come in families: one protocol at several widths, with payloads that change per project. An interface takes value parameters for widths and parameter type for payloads, exactly like a module.

    Each parameterization elaborates into its own type. bus_if#(32) and bus_if#(64) are different types, so code that names the interface type, such as a class's virtual interface handle in the OOP part, must name the exact specialization. A module port is different: a plain bus_if or bus_if.dut port takes the parameters of whichever instance is connected.

    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
    interface bus_if #(
      parameter int  ADDR_W = 32,
      parameter int  DATA_W = 64,
      parameter type meta_t = logic [3:0]    // type parameter
    )(
      input logic clk
    );
      timeunit 1ns; timeprecision 1ps;
      logic              req, gnt;
      logic [ADDR_W-1:0] addr;
      logic [DATA_W-1:0] wdata, rdata;
      meta_t             meta;
    
      // generate: structure follows the parameters
      generate
        if (DATA_W > 32) begin : g_wide
          logic [DATA_W/8-1:0] byte_en;     // byte enables only when wide
        end
      endgenerate
    
      clocking cb @(posedge clk);
        default input #1step output #2;
        output req, addr, wdata, meta;
        input  gnt, rdata;
      endclocking
    endinterface

    Lock the widths together

    The DUT repeats the same width parameters. Feed both sides from one package of constants, so the two sets cannot diverge at any instantiation.

    An interface port cannot take parameter values. A plain bus_if.dut port inherits the connected instance's widths, not the defaults, so lock the two sides with an elaboration check that compares $bits(bus.addr) with the DUT's own parameter.

    The next listing is a sketch and is not run: its dut has no body and it relies on a bus_if with a dut modport, like the one in the Testbench section.

    dut.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
    // not-run: sketch; needs a bus_if with a dut modport (see the Testbench listing)
    package soc_cfg_pkg;
      parameter int ADDR_W = 40;
      parameter int DATA_W = 128;
    endpackage
    
    module tb_top;
    
      timeunit 1ns; timeprecision 1ps;
      import soc_cfg_pkg::*;
      logic clk = 0;  always #5 clk = ~clk;
    
      // one source of truth for BOTH sides:
      bus_if #(.ADDR_W(ADDR_W), .DATA_W(DATA_W)) bus (.clk(clk));
    
      dut #(
        .ADDR_W (ADDR_W),         // same package constants:
        .DATA_W (DATA_W)          // interface and DUT cannot diverge
      ) u_dut (.bus(bus));
    endmodule
    
    // A plain interface port: the widths come from the connected instance.
    module dut #(
      parameter int ADDR_W = 32,
      parameter int DATA_W = 64
    )(
      bus_if.dut bus
    );
      // lock-step check: elaboration fails if the instance and the DUT disagree
      if ($bits(bus.addr) != ADDR_W || $bits(bus.wdata) != DATA_W)
        $error("bus_if widths do not match the dut parameters");
    endmodule

    Exact specialization matters for handles: a virtual bus_if #(...) must name the exact widths of the instance it points at. That rule belongs to the virtual interfaces topic.

    Pin choreography in automatic tasks

    Tasks inside the interface see the bus signals natively. Pack the drive, wait, and sample sequence once, and every caller shares the same timing-correct choreography.

    Declare every task automatic. A static task shares one set of locals across concurrent calls, so two callers corrupt each other.

    The read and write tasks sit in the interface at the top of the Testbench listing below, where they run.

    What goes in the interface

    The interface owns the pin-accurate, mechanical layer: drive and sample tasks written against the clocking block. Directed tests call these tasks directly. Class-based drivers, in the OOP part, call the same tasks through a virtual interface and add sequencing, randomization and error injection on top.

    Interview

    “Why put the handshake in a task inside the interface?” Every caller then shares one timing-correct implementation that sits beside the pins, and it works before any class environment exists.

    Check yourself Two agents share one interface task declared without automatic. What breaks?

    Both calls share one set of locals, so they corrupt each other. Declare interface tasks automatic.

    Testbench

    The BFM interface, a small memory DUT, and a directed test that writes a word and reads it back through the tasks.

    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
    interface bus_if #(parameter int ADDR_W = 32, parameter int DATA_W = 64) (input logic clk, input logic rst_n);
      timeunit 1ns; timeprecision 1ps;
      logic req, gnt, we;
      logic [ADDR_W-1:0] addr;
      logic [DATA_W-1:0] wdata, rdata;
    
      clocking cb @(posedge clk);
        default input #1step output #2;
        output req, we, addr, wdata;
        input  gnt, rdata;
      endclocking
      modport dut (input req, we, addr, wdata, output gnt, rdata, input clk, rst_n);
      modport tb (clocking cb, input clk, rst_n);
    
      // BFM method: full handshake, written once (note the synced deassert)
      task automatic read(input logic [ADDR_W-1:0] a, output logic [DATA_W-1:0] d);
        @(cb);
        cb.req <= 1'b1; cb.we <= 1'b0; cb.addr <= a;
        @(cb iff cb.gnt);                  // grant seen: the DUT has the address
        @(cb);                             // one more cycle: the DUT places rdata
        d = cb.rdata;
        @(cb);
        cb.req <= 1'b0;
      endtask
    
      task automatic write(input logic [ADDR_W-1:0] a, input logic [DATA_W-1:0] d);
        @(cb);
        cb.req <= 1'b1; cb.we <= 1'b1; cb.addr <= a; cb.wdata <= d;
        @(cb iff cb.gnt);
        @(cb);
        cb.req <= 1'b0;
      endtask
    endinterface
    
    module dut #(parameter int ADDR_W = 32, parameter int DATA_W = 64)
      (bus_if.dut bus);                            // widths come from the instance
      logic [DATA_W-1:0] mem [4];
      assign bus.gnt = bus.req;                    // grant follows request
      always_ff @(posedge bus.clk or negedge bus.rst_n) begin
        if (!bus.rst_n) bus.rdata <= '0;
        else if (bus.req && bus.we) mem[bus.addr[1:0]] <= bus.wdata;
        else if (bus.req && !bus.we) bus.rdata <= mem[bus.addr[1:0]];
      end
    endmodule
    
    module tb_top;
      timeunit 1ns; timeprecision 1ps;
      logic clk = 0, rst_n = 0;
      always #5 clk = ~clk;
      bus_if #(.ADDR_W(32), .DATA_W(64)) bus (.clk(clk), .rst_n(rst_n));
      dut #(.ADDR_W(32), .DATA_W(64)) u_dut (.bus(bus));
    
      // directed bring-up test: no classes, full protocol correctness
      initial begin : smoke
        logic [63:0] d;
        int errors = 0;
        repeat (4) @(posedge clk);
        rst_n <= 1'b1;                            // nonblocking: no race with the DUT flop
        bus.write(32'h1000, 64'hDEAD_BEEF_CAFE_F00D);
        bus.read(32'h1000, d);
        if (d !== 64'hDEAD_BEEF_CAFE_F00D) errors++;
        if (errors != 0) $fatal(1, "FAIL: readback differed");
        $display("PASS: write then read returned the written word");
        $finish;
      end
    endmodule
    Terminal
    PASS: write then read returned the written word

    Project step

    Give pkt_if a clocking block for each role and two tasks: send_pkt drives a queue of bytes with valid/ready and marks the last one, and recv_pkt collects bytes until last. Wrap the v0 fifo as pkt_fifo with a pkt_if on each side and move two packets through it.

    One way to write it. This is PacketFIFO's first full bench: the OOP part replaces the direct task calls with classes.

    pkt_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
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    // pkt_if with a clocking block per role and the two BFM tasks.
    interface pkt_if (input logic clk, input logic rst_n);
      timeunit 1ns; timeprecision 1ps;
      logic       valid, ready, last;
      logic [7:0] data;
      modport src (output valid, data, last, input ready, clk, rst_n);
      modport snk (input valid, data, last, clk, rst_n, output ready);
    
      clocking src_cb @(posedge clk);     // the testbench as a source
        default input #1step output #2;
        output valid, data, last;
        input  ready;
      endclocking
      clocking snk_cb @(posedge clk);     // the testbench as a sink
        default input #1step output #2;
        output ready;
        input  valid, data, last;
      endclocking
    
      task automatic send_pkt(input logic [7:0] bytes [$]);
        @(src_cb);
        foreach (bytes[i]) begin
          src_cb.valid <= 1'b1;
          src_cb.data  <= bytes[i];
          src_cb.last  <= (i == bytes.size() - 1);
          @(src_cb iff src_cb.ready);     // this edge took the byte
        end
        src_cb.valid <= 1'b0;
        src_cb.last  <= 1'b0;
      endtask
    
      task automatic recv_pkt(output logic [7:0] bytes [$]);
        bytes = {};
        @(snk_cb);
        snk_cb.ready <= 1'b1;
        do begin
          @(snk_cb iff snk_cb.valid);     // this edge delivered a byte
          bytes.push_back(snk_cb.data);
        end while (!snk_cb.last);
        snk_cb.ready <= 1'b0;
      endtask
    endinterface
    
    // DEPTH is a power of two of at least 2, so PTR_W is at least 1.
    module fifo #(parameter int DEPTH = 16, parameter int DATA_W = 32) (
      input  logic              clk, rst_n, wr_en, rd_en,
      input  logic [DATA_W-1:0] wdata,
      output logic [DATA_W-1:0] rdata,
      output logic              full, empty
    );
      timeunit 1ns; timeprecision 1ps;
      localparam int PTR_W = $clog2(DEPTH);
      logic [DATA_W-1:0] mem [DEPTH];
      logic [PTR_W:0] wr_ptr, rd_ptr;
      assign empty = (wr_ptr == rd_ptr);
      assign full  = (wr_ptr[PTR_W] != rd_ptr[PTR_W]) &&
                     (wr_ptr[PTR_W-1:0] == rd_ptr[PTR_W-1:0]);
      assign rdata = mem[rd_ptr[PTR_W-1:0]];
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          wr_ptr <= '0;
          rd_ptr <= '0;
        end else begin
          if (wr_en && !full) begin
            mem[wr_ptr[PTR_W-1:0]] <= wdata;
            wr_ptr <= wr_ptr + 1;
          end
          if (rd_en && !empty) rd_ptr <= rd_ptr + 1;
        end
      end
    endmodule
    
    // PacketFIFO v0 with pkt_if on both sides.
    module pkt_fifo (pkt_if.snk in, pkt_if.src out);
      timeunit 1ns; timeprecision 1ps;
      logic       full, empty;
      logic [8:0] rdata;
      fifo #(.DEPTH(4), .DATA_W(9)) u_f (
        .clk(in.clk), .rst_n(in.rst_n),
        .wr_en(in.valid), .rd_en(out.ready),
        .wdata({in.last, in.data}), .rdata(rdata), .full(full), .empty(empty));
      assign in.ready  = !full;
      assign out.valid = !empty;
      assign {out.last, out.data} = rdata;
    endmodule
    
    Terminal
    packet 1: '{'h22, 'ha5, 'h3c, 'hbb}
    packet 2: '{'h51, 'h7, 'h56}
    PASS: send_pkt and recv_pkt moved both packets through PacketFIFO v0 unchanged

    Remember

    • Each parameterization of an interface is its own type.

    • Tasks inside the interface see the signals and its clocking blocks directly.

    • Make every interface task automatic.

    Rule of thumb: write the handshake once, beside the pins.

    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. Icarus 13 cannot run these benches: it rejects interface ports and clocking blocks.

    The PacketFIFO bench was also run with a planted bug (the read pointer stepping by 2); it failed with both packets wrong, and its watchdog stops the run if bytes never arrive.

    Related topics

    • Next part: Object-Oriented Programming

    Previous

    34 / 259

    Next: Object-Oriented SystemVerilog for Verification

    Up next

    Up nextObject-Oriented SystemVerilog for Verification

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Interface Basics
    • Modports