Part 1 · Language Foundations · Intermediate

Tasks & Functions in Interfaces

BFM-style drive and sample tasks inside interfaces, and an honest comparison with class-based BFMs.

The interface as a bus functional model

Because an interface is a scope containing the bus signals, tasks and functions declared inside it manipulate those signals with no hierarchy crossing and no handle plumbing — the signals are simply in scope. Pack the protocol's pin-level choreography ( drive a request, wait for grant, sample data ) into interface tasks and the interface becomes a self-contained BFM: callers say bus.read(addr, data) and never touch a pin. Every caller — class-based driver, directed test, even a quick bring-up stimulus module — exercises the same timing-correct sequence, so pin choreography is written once and debugged once. Methods should drive and sample exclusively through the clocking block so the race-free guarantees of the previous lesson apply to every transaction.

systemverilog
interface bus_if (input logic clk, input logic rst_n);
  logic        req, gnt, we;
  logic [31:0] addr;
  logic [63:0] wdata, rdata;

  clocking cb @(posedge clk);
    default input #1step output #2;
    output req, we, addr, wdata;
    input  gnt, rdata;
  endclocking

  // BFM method: full handshake, written once
  task automatic read(input  logic [31:0] a,
                      output logic [63:0] d);
    @(cb);
    cb.req  <= 1'b1;  cb.we <= 1'b0;  cb.addr <= a;
    @(cb iff cb.gnt);                  // wait for grant, race-free
    d = cb.rdata;
    cb.req  <= 1'b0;
  endtask

  task automatic write(input logic [31:0] a, input logic [63:0] d);
    @(cb);
    cb.req <= 1'b1;  cb.we <= 1'b1;  cb.addr <= a;  cb.wdata <= d;
    @(cb iff cb.gnt);
    cb.req <= 1'b0;
  endtask

  function automatic bit busy();
    return req && !gnt;
  endfunction
endinterface

Calling interface methods

From a module, methods are called through the interface port; from a class, through a virtual interface handle (next lesson). Declare interface tasks automatic — a static task has one shared set of argument and local-variable storage, so two concurrent calls (two agents sharing a BFM, or a fork) corrupt each other's state in ways that debug as data mysteriously changing mid-task. Methods can also be exported through modports with import task read entries, restricting which views may call which methods.

systemverilog
// directed bring-up test: no classes, full protocol correctness
module smoke_test (bus_if bus);
  logic [63:0] d;
  initial begin
    wait (bus.rst_n);
    bus.write(32'h1000, 64'hDEAD_BEEF_CAFE_F00D);
    bus.read (32'h1000, d);
    if (d !== 64'hDEAD_BEEF_CAFE_F00D)
      $error("readback mismatch: %h", d);
    else
      $display("smoke test PASS");
  end
endmodule
diagram
WHERE THE PROTOCOL KNOWLEDGE LIVES

  class-based BFM                interface BFM
  ───────────────                ─────────────
  class driver;                  interface bus_if;
    virtual bus_if vif;            task read(...);   ← pin choreography
    task drive_pins(); ←──┐        task write(...);     lives WITH the pins
  endclass              │      endinterface
                        │            ▲
  pin wiggling reaches  │            │ bus.read(a,d)
  signals via vif.cb.x ─┘       any caller: class via vif,
  (handle indirection)          module, directed test

Interface BFM vs class BFM — the honest trade-off

Strengths of interface methods

  • Co-located with the signals and clocking block — no handle indirection inside the choreography.

  • Callable from static modules and directed tests — usable before any class environment exists (bring-up, gate-level).

  • One timing-correct implementation shared by every caller; emulation/acceleration flows favor this split (pins in static world).

  • Natural home alongside the protocol assertions that police the same handshake.

Where class BFMs win

  • No inheritance: an interface task cannot be extended or overridden for an error-injecting variant — classes use factory overrides.

  • No dynamic construction or randomization inside — interfaces are elaboration-time static objects.

  • Configuration and policy (delays, error rates) live awkwardly in interface variables vs clean class config objects.

  • UVM sequencing, phasing, and TLM connect to classes, not interfaces.

Mature methodology splits the difference: the interface owns the pin-accurate, protocol-mechanical layer (drive/sample tasks against the clocking block), and the class driver owns the intelligent layer (sequencing, randomization, error injection, configuration) and calls down into the interface methods through its virtual interface handle. The interview-ready summary: interface methods give reuse across static and dynamic worlds; class BFMs give extensibility — production environments layer the second on top of the first.

Key takeaways

  • Interface tasks see the bus signals natively — pack the pin choreography there, once.

  • Drive and sample inside methods exclusively through the clocking block to inherit race freedom.

  • Always declare interface tasks automatic — static-lifetime tasks corrupt concurrent calls.

  • Layer the worlds: interface = mechanical pin layer; class driver = brains that calls it via vif.

Common pitfalls

  • Static (non-automatic) interface tasks called concurrently — shared locals silently corrupt both calls.

  • Bypassing the clocking block inside a method (raw signal writes) — reintroduces races the cb was added to kill.

  • Burying test policy (delays, error injection) in interface variables — belongs in class configuration.

  • Expecting to override an interface task per-test like a class method — interfaces have no inheritance.