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.
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 definitionInterface with clocking blocks, accessed through a vif
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
endinterfaceRace-free driver through the vif
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
endclassSemantics 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.