Part 2 · OOP for Verification · Intermediate

Driving & Sampling via Clocking Blocks

vif.cb.sig drive and sample semantics, @(vif.cb) synchronization idioms, and race-free driver/monitor code.

Why raw vif.sig access races

Driving vif.addr directly at @(posedge vif.clk) puts your testbench process in the same simulation time step as the DUT's flip-flops. Whether the DUT sees the old or new value depends on process scheduling order — a race that may differ between simulators and even between runs. A clocking block inside the interface removes the race by construction: it defines when signals are sampled (input skew, e.g. 1step before the edge) and when drives take effect (output skew, e.g. a delay after the edge). Class code that goes through vif.cb inherits these guarantees automatically.

diagram
CLOCKING BLOCK TIMING (default #1step input, #2ns output)

            sample inputs here           outputs take effect here
                 │                          │
   ──────────────▼──────────┬──────────────▼──────────────────►
                       posedge clk      clk + 2ns            time
                 ▲          │
       #1step before edge   │
       = settled values     │
       from PREVIOUS cycle  └─ DUT flops capture at the edge,
                                always seeing LAST cycle's drive
                                 no race, by definition

Interface with clocking blocks, accessed through a vif

systemverilog
interface bus_if (input logic clk);
  logic [31:0] addr;
  logic [31:0] rdata;
  logic        valid;
  logic        ready;

  // Driver's view: drives addr/valid, samples ready/rdata
  clocking drv_cb @(posedge clk);
    default input #1step output #2ns;
    output addr, valid;
    input  ready, rdata;
  endclocking

  // Monitor's view: samples everything, drives nothing
  clocking mon_cb @(posedge clk);
    default input #1step;
    input addr, valid, ready, rdata;
  endclocking
endinterface

Race-free driver through the vif

systemverilog
class driver;
  virtual bus_if vif;

  function new(virtual bus_if vif);
    this.vif = vif;
  endfunction

  task drive_one(bit [31:0] a);
    @(vif.drv_cb);                  // sync to the clocking event
    vif.drv_cb.addr  <= a;          // clocked drive — applied with
    vif.drv_cb.valid <= 1'b1;       // output skew, race-free
    @(vif.drv_cb iff vif.drv_cb.ready);  // wait for sampled ready
    vif.drv_cb.valid <= 1'b0;
  endtask
endclass

class monitor;
  virtual bus_if vif;

  task run();
    forever begin
      @(vif.mon_cb iff (vif.mon_cb.valid && vif.mon_cb.ready));
      // sampled values: stable, pre-edge, identical on every simulator
      $display("[%0t] saw addr=%0h data=%0h",
               $time, vif.mon_cb.addr, vif.mon_cb.rdata);
    end
  endtask
endclass

Semantics and idioms to internalize

The rules

  • @(vif.cb) waits for the clocking event — the standard sync point; prefer it over @(posedge vif.clk) in class code.

  • vif.cb.sig <= value is a clocked drive: scheduled, applied with the output skew. Use nonblocking <= for clocking drives.

  • Reading vif.cb.sig returns the value sampled at the input skew — last cycle's settled value, not the live wire.

  • Reading the raw vif.sig still gives the live (possibly mid-transition) value — mixing raw and cb access on the same signal reintroduces the race.

  • Direction split per clocking block (drv_cb outputs vs mon_cb all-inputs) documents and enforces each component's role.

Interview angle: 'why does the monitor through mon_cb see a value one cycle behind the wire?' Because input skew samples before the edge, the cb view is the pre-edge settled value — which is exactly what the DUT's flops captured. That is not a bug; it is the definition of race-free sampling. A second favorite: 'driver writes vif.valid directly and the DUT misses the first beat on one simulator only' — the fix is moving the drive onto vif.drv_cb.valid.

Key takeaways

  • Clocking blocks define sample and drive instants; class code inherits them through vif.cb.

  • Sync with @(vif.cb), drive with vif.cb.sig <= value, sample by reading vif.cb.sig.

  • cb reads return pre-edge sampled values — the same values DUT flops captured, by design.

  • Separate driver and monitor clocking blocks encode direction roles in the interface itself.

Common pitfalls

  • Driving raw vif.sig from class code — works in one simulator, races in another.

  • Mixing vif.cb.sig and vif.sig access on the same signal in one process — skews disagree, debug nightmare.

  • Using blocking = on clocking drives — clocking outputs expect nonblocking semantics.

  • Expecting vif.cb.sig reads to show the live wire — they show the input-skew sample, one settled value behind.