A testbench that drives DUT inputs with blocking assignments right at @(posedge clk) races the DUT flops that sample them. Both run on the same edge, and either may run first, so the DUT captures the old value or the new one depending on simulator ordering. Reading a nonblocking flop output at the edge is not a race: it always returns the old value, because the update waits for the NBA region.
A clocking block fixes both directions by construction. Inputs sample with an input skew; the default #1step means the value just before the edge, untouched by anything the edge triggers.
Outputs drive with an output skew after the edge. They land like real stimulus arriving between clocks and never collide with same-edge DUT sampling.
q changes at this clock edge. Testbench code samples q with input skew #1step, and other code with skew #0. Which one sees the old value?The #1step sample sees the old value, from just before the edge. The #0 sample is taken after this edge's updates, so it sees the new value: one cycle earlier than the DUT's own flops would.
A zero input skew samples in the Observed region, after this edge's flops have updated, so the bench sees the new value and is off by one cycle. A fixed time skew couples the bench to a clock period. 1step is one time-precision unit before the edge: the last stable value of the previous cycle, independent of the period.
The interface declares the block once; testbench code then drives and samples through bus.cb. Many codebases also export the block through a modport (modport tb (clocking cb)); this course uses it on the interface instance instead.
interface bus_if (input logic clk);
timeunit 1ns; timeprecision 1ps;
logic req, gnt;
logic [31:0] addr;
logic [63:0] rdata;
clocking cb @(posedge clk);
default input #1step output #2;
output req, addr; // testbench drives these (through cb)
input gnt, rdata; // testbench samples these (through cb)
endclocking
endinterfaceIdiom | Meaning |
|---|---|
| Wait for the clocking event itself, cleaner than a raw edge. |
| The sampled pre-edge value, stable even mid-cycle. |
| A drive landing after the edge, seen cleanly at the next one. |
Raw | The bypass path. Never drive it beside the clocking block. |
The waveform shows the handshake's timing: the requester drives req 1 ns after an edge, like a small output skew, and the one-flop responder captures it at the next edge.
Slot 0: req is 0 and gnt is 0. The bus is idle.
Slot 1: req is 1, driven just after the slot-1 edge (output skew). gnt is still 0: the flop has not seen it yet.
Slot 2: the slot-2 edge captures req, so gnt rises. req drops just after that edge.
Slot 3: req is 0 and gnt is 0. The grant drops one cycle after the request.
Drive and sample each signal through exactly one path. A raw drive beside a clocking drive reintroduces the race and the two drives fight.
The ##N operator counts N events of the scope's default clocking. With no default clocking in scope, there is nothing to count, so this lesson waits with explicit clocking events instead.
repeat (2) @(bus.cb) waits two clocking events with no extra declaration. The testbench below uses it.
input #1step sample, and why not #0?1step samples the Preponed value just before the edge. A zero skew samples in the Observed region, after this edge's flops have updated, so the bench would see the new value one cycle early.
bus.cb.req and raw bus.req. What breaks?Both paths. The drives conflict and the sampling race returns. Keep one path per signal.
default clocking is declared. What does ##2 mean?Nothing legal: the operator counts events of the scope's default clocking, and none exists. Wait with explicit @(bus.cb) instead.
A one-flop grant, four request pulses driven through bus.cb, and a count of the grants sampled through it.
interface bus_if (input logic clk);
timeunit 1ns; timeprecision 1ps;
logic req, gnt;
clocking cb @(posedge clk);
default input #1step output #2;
output req; // the testbench drives req through cb
input gnt; // and samples gnt through cb
endclocking
modport dut (input req, output gnt, input clk);
endinterface
module dut (bus_if.dut bus);
timeunit 1ns; timeprecision 1ps;
always_ff @(posedge bus.clk)
bus.gnt <= bus.req; // one-flop grant
endmodule
module tb_top;
timeunit 1ns; timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
bus_if bus (.clk(clk));
dut u_dut (.bus(bus));
int seen = 0;
initial begin
bus.cb.req <= 1'b0;
repeat (2) @(bus.cb); // count clocking events, not nanoseconds
for (int i = 0; i < 4; i++) begin
bus.cb.req <= 1'b1; // lands 2 ns after this edge
@(bus.cb); // the DUT captures req at this edge
bus.cb.req <= 1'b0;
@(bus.cb); // sampled just before this edge: gnt is 1
if (bus.cb.gnt === 1'b1) seen++;
$display("pulse %0d at %0d ns: sampled gnt=%b", i, $time, bus.cb.gnt);
end
if (seen == 4) $display("PASS: clocking-block sampling counted all 4 grants");
else $fatal(1, "FAIL: counted %0d of 4 grants", seen);
$finish;
end
endmodulepulse 0 at 35 ns: sampled gnt=1
pulse 1 at 55 ns: sampled gnt=1
pulse 2 at 75 ns: sampled gnt=1
pulse 3 at 95 ns: sampled gnt=1
PASS: clocking-block sampling counted all 4 grantsEvery drive goes through bus.cb, so it lands 2 ns after the edge and never at the DUT's sampling instant, and every read is the pre-edge sample.
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 this bench: it has no clocking blocks.
Verilator 5.048 does not accept a clocking block reached through a modport, so the lesson uses the block on the interface instance.
The waveform comes from a plain-Verilog model of the same handshake, drawn with Icarus 13.