Clocking Blocks and Program Blocks
Every construct in this curriculum so far has been about what to write. This page is about a subtler, easy-to-miss problem: when, exactly, testbench code and DUT (design-under-test) code each see a signal change, relative to a clock edge — and why getting that wrong causes bugs that only show up as rare, hard-to-reproduce simulation mismatches, not consistent failures.
The race condition SystemVerilog testbenches are exposed to
At a posedge clk, both the DUT's always_ff blocks and a testbench's driver code (in an initial/always block) may be scheduled to react in the very same simulation time step. Which one actually executes first, at that exact instant, is determined by simulator-internal scheduling rules a test writer shouldn't have to reason about — and if a driver happens to update a DUT input in the same time step the DUT samples it, whether the DUT sees the old or new value can depend on scheduling order rather than the design's actual intent. This is the "race" — not a bug in the DUT or the testbench individually, but an ambiguity in exactly when a signal's new value becomes visible to each side.
Clocking blocks: giving the testbench a well-defined view of time
interface dut_if (input logic clk);
logic [7:0] data_in;
logic valid;
logic [7:0] data_out;
clocking cb @(posedge clk);
output data_in, valid; // testbench drives these, skewed to avoid the race
input data_out; // testbench samples this, skewed to avoid the race
endclocking
endinterface
task drive(bit [7:0] value);
dut_vif.cb.data_in <= value; // driven through the clocking block, not the raw signal
dut_vif.cb.valid <= 1;
@(dut_vif.cb); // wait for the clocking block's own clock event
endtask
A clocking block declares, per signal, whether the testbench drives it (output) or samples it (input), relative to a named clock edge — and automatically applies a small, simulator-managed timing skew (driving slightly after the edge, sampling slightly before it) that avoids the race entirely, without the test writer having to hand-tune delay values to dodge it. Accessing a signal through the clocking block (dut_vif.cb.data_in, not the raw dut_vif.data_in) is what gets this protection — bypassing the clocking block and touching the raw interface signal directly reintroduces the exact race it exists to prevent.
Two mechanical points worth being explicit about: a clocking-block output signal must always be driven with the non-blocking synchronous drive operator (cb.data_in <= value;, never cb.data_in = value;) — the assignment's right-hand side is evaluated immediately, but the drive itself is scheduled for the clocking block's next clock event, which is what lets the drive land at the correct post-edge skew instead of racing the edge. And while a clocking block is conventionally declared inside an interface (as it is throughout this curriculum, since that's the natural meeting point between testbench and DUT), the LRM also allows one to be declared directly inside a module or a program block.
program blocks: a testbench-only compilation unit
program automatic test(dut_if.tb dut_vif);
initial begin
dut_vif.cb.data_in <= 8'hA5;
dut_vif.cb.valid <= 1;
@(dut_vif.cb);
$display("dut responded: %0h", dut_vif.cb.data_out);
end
endprogram
A program block is, structurally, similar to a module — but it's specifically intended to hold testbench code, not DUT logic, and historically existed to give the testbench its own well-defined execution ordering relative to the DUT's modules, reducing race exposure at the block-scheduling level, separately from what a clocking block does at the individual-signal level. In modern SystemVerilog testbenches (especially UVM-based ones), the program block itself is used less often than clocking blocks — most of what it originally solved is now handled by disciplined use of clocking blocks plus always_ff in the DUT — but it's still worth recognizing on sight in existing codebases, and understanding why it exists (testbench/DUT race avoidance) rather than treating it as unexplained legacy syntax.
Mechanically, this ordering comes from the simulation event scheduler's regions within one time step: a module's nonblocking (<=) updates land in the Active region, while statements inside a program block execute in the later Reactive region — by the time a program block's code runs, every module's Active-region updates for that time step have already completed, so the testbench is guaranteed to see the DUT's fully-settled values for that edge rather than racing to see them mid-update.
The practical takeaway
Declare a clocking block inside any interface connected between a testbench and a DUT, and drive/sample every signal through it (vif.cb.signal, not vif.signal directly) from testbench code. This one habit removes the most common source of "the testbench works most of the time but occasionally sees the wrong value" bugs — races that are maddening to debug precisely because they don't reproduce consistently.
Skew values and the ##N cycle-delay operator
interface dut_if (input logic clk);
logic [7:0] data_in;
logic data_out;
default clocking cb @(posedge clk);
default input #1step output #2;
output data_in;
input data_out;
endclocking
endinterface
task drive_and_wait(bit [7:0] value);
dut_vif.cb.data_in <= value;
##2; // advance 2 clocking-block cycles, no need to name cb explicitly
endtask
The skew values shown earlier aren't automatic magic — they're a default (#1step for input, meaning sampled in the Preponed region just before the edge so the prior cycle's settled value is read; a small positive delay like #2 for output, meaning driven after the edge so the DUT has already latched that cycle's inputs) that a clocking block can also state explicitly with default input ... output ..., or override per-signal. Marking a clocking block default (as default clocking cb @(...)) lets the rest of the testbench use the terse ##N cycle-delay operator (##2 = advance 2 clock cycles) without repeating @(dut_vif.cb) or naming which clocking block it refers to — ##N always resolves to whichever clocking block was declared default in that scope.
What's next
Every language mechanic this curriculum set out to cover — data types, procedural/modular constructs, OOP, randomization, concurrency, assertions, coverage, and now the clocking discipline that ties a class-based testbench safely to a DUT — is now in place. The closing page is a capstone: a small, complete, class-based, interface-driven, randomized, self-checking testbench for the full adder, using a clocking block, an assertion, and a covergroup together in one example.