The SystemVerilog clocking block solves one of the most dangerous problems in simulation: the race condition between the testbench and the DUT at the clock edge. Without it, your testbench may occasionally drive a signal at the same time the DUT samples it — and which one "wins" depends on non-deterministic simulation scheduling. Clocking blocks make this deterministic, correct, and professional-grade.
Consider this classic testbench code:
// DANGEROUS — race condition with the DUT initial begin @(posedge clk); // wait for the clock edge data_in <= 8'hA5; // drive signal AT the same edge valid <= 1'b1; // ...just like the DUT samples it end
The problem: in SystemVerilog simulation, all processes that trigger on posedge clk are scheduled at the same simulation time. The order they execute is non-deterministic — it depends on the simulator's internal scheduling. Sometimes the testbench drives data_in before the DUT's always_ff samples it (the testbench "wins" — this test appears to pass). Sometimes the DUT samples before the testbench drives (the DUT sees the old value — the test appears to fail). The same test passes on Monday and fails on Friday. This is a race condition.
A clocking block declares two things explicitly:
#1step (one delta cycle before). The testbench reads the stable, settled value of the signal before the edge — guaranteed to capture the last complete cycle's value.#1step. The testbench writes the signal after the DUT has already clocked on this edge — so the DUT only sees the new value on the next cycle.This is exactly how real hardware works: setup time (sample before edge) and clock-to-Q delay (output appears after edge). Clocking blocks make simulation match real synchronous design practice.
clocking cb_name @(posedge clk); default input #1step output #1step; input signal_a; // sampled 1step before posedge output signal_b; // driven 1step after posedge input #2ns signal_c; // custom: sampled 2ns before edge output #3ns signal_d; // custom: driven 3ns after edge endclocking
To drive a signal through a clocking block, use the clocking block name: cb.data <= value; — always with non-blocking assignment (<=). The signal will actually update at the output skew time after the next clock edge. To advance one clock cycle: ##1; (two hash symbols, then N cycles).
To read a signal: value = cb.data; (simple reference). The value you get is the one captured at the input skew before the last clock edge. To wait for a clock edge: @(cb); — this waits for the clocking block's triggering event (the posedge of its clock).
The ##N operator means "wait N clocking block cycles" — it is a synchronous delay counted in clock cycles, not time. ##1 advances one clock cycle. ##0 means "at the current clock edge" (used in concurrent assertions). You can chain them: ##3 cb.valid <= 1; ##1 cb.valid <= 0; drives valid for exactly one cycle, 3 cycles after the current time.
| Syntax | Meaning |
|---|---|
| @(cb) | Wait for one clock cycle (clocking block edge) |
| ##1 | Advance one clock cycle of the default clocking block |
| ##N | Advance N clock cycles |
| cb.signal <= val | Drive signal at output skew after next edge |
| val = cb.signal | Read signal sampled at input skew before last edge |
| @(posedge cb.signal) | Wait for clocking-block-sampled posedge of signal |
Best practice is to put the clocking block inside the interface. This co-locates all timing information with the signal declarations and makes it available to any testbench that instantiates the interface via a virtual interface handle. The clocking block becomes part of the interface contract between the DUT and the testbench.
A modport that includes the clocking block looks like: modport TB_CB (clocking cb_name, output rst_n); — the testbench modport grants access to the clocking block and any non-clocked signals (like async reset).
// ============================================================
// clk_block_demo.sv — Clocking block: race-free TB demo
// EcrioniX · SV Verification Course · Day 6
// ============================================================
// ---- Interface with clocking block ----
interface sync_if (input logic clk);
logic rst_n;
logic [7:0] data;
logic valid;
logic ready;
logic [7:0] result;
logic result_valid;
// ---- Clocking block: testbench perspective ----
// input skew : sample 1step BEFORE posedge (see settled value)
// output skew: drive 1step AFTER posedge (DUT sees new value next cycle)
clocking cb @(posedge clk);
default input #1step output #1step;
// From TB view: data/valid are outputs (TB drives them)
output data;
output valid;
// From TB view: ready/result are inputs (TB reads them)
input ready;
input result;
input result_valid;
endclocking
// Modport for testbench: uses the clocking block + async rst_n
modport TB_MP (clocking cb, output rst_n);
// Modport for DUT: direct signal access (synchronous flip-flops in DUT)
modport DUT_MP (input clk, rst_n, data, valid, output ready, result, result_valid);
endinterface : sync_if
// ---- Testbench using the clocking block ----
module clk_block_demo;
logic clk = 0;
always #5 clk = ~clk; // 100 MHz
sync_if bus (clk);
// ---- Simulate a DUT: simple pass-through with 2-cycle latency ----
logic [7:0] pipe1, pipe2;
always_ff @(posedge clk or negedge bus.rst_n) begin
if (!bus.rst_n) begin
pipe1 <= 0; pipe2 <= 0;
bus.ready <= 0;
bus.result <= 0;
bus.result_valid <= 0;
end else begin
bus.ready <= 1;
pipe1 <= bus.valid ? bus.data : pipe1;
pipe2 <= pipe1;
bus.result <= pipe2;
bus.result_valid <= bus.valid;
end
end
// ---- Testbench: drive through clocking block ----
initial begin
// Async reset (not through clocking block — it's async)
bus.rst_n = 0;
bus.cb.data <= 0;
bus.cb.valid <= 0;
repeat(3) @(bus.cb); // wait 3 clocking block cycles
bus.rst_n = 1;
@(bus.cb);
// ---- CORRECT: drive through clocking block ----
// The <= assignment updates AFTER the edge (output skew)
// The DUT will see these values in the NEXT cycle
$display("[T=%0t] Driving 8'hA5 through clocking block", $time);
bus.cb.data <= 8'hA5;
bus.cb.valid <= 1'b1;
@(bus.cb); // advance 1 cycle — data is now stable to DUT
bus.cb.data <= 8'hB6;
@(bus.cb);
bus.cb.valid <= 1'b0;
// Wait and sample result
##3; // advance 3 more cycles (##N = N clocking events)
$display("[T=%0t] result = 8'h%0h, result_valid = %b",
$time, bus.cb.result, bus.cb.result_valid);
// ---- Demonstrate reading sampled value (input skew) ----
@(bus.cb);
// bus.cb.ready is sampled 1step BEFORE the posedge — stable settled value
if (bus.cb.ready)
$display("[T=%0t] DUT is ready (sampled with input skew)", $time);
// ---- ##N delays ----
$display("[T=%0t] Waiting 5 cycles with ##5...", $time);
##5;
$display("[T=%0t] Done. No race conditions — simulation is deterministic.", $time);
$finish;
end
// ---- Demonstrate what WRONG code looks like (without clocking block) ----
// DO NOT DO THIS:
// @(posedge clk); // same time as DUT flip-flops
// bus.data <= 8'hA5; // RACE: DUT may or may not see this yet
// WITH clocking block:
// @(bus.cb); // wait for edge
// bus.cb.data <= 8'hA5; // drives 1step AFTER edge — safe, deterministic
endmodule : clk_block_demoAlways use non-blocking assignment (<=) when driving through a clocking block: cb.data <= value. Using blocking assignment (=) bypasses the output skew mechanism and can reintroduce race conditions. The non-blocking form schedules the update for the NBA (non-blocking assignment) region, which respects the output skew timing.
Industry-grade verification environments — particularly those built on UVM — always use clocking blocks because:
The IEEE 1800 standard specifically added clocking blocks to address these simulation correctness problems. After Day 5 (interfaces) and Day 6 (clocking blocks), your testbench infrastructure is at an industry-professional level — you're ready for Day 7 (program blocks) and then OOP classes in Phase 2.
@(posedge clk) races with the DUT — non-deterministic, intermittent failures.#1step) and output skew (drive after edge — default #1step).cb.signal <= value (non-blocking — always).val = cb.signal (reads the pre-edge captured value).@(cb) = one cycle; ##N = N cycles.A clocking block is a named group of signals with explicit sampling (input skew) and drive (output skew) timing relative to a clock edge. It eliminates the testbench-vs-DUT race condition by ensuring the TB samples signals before the edge and drives signals after the edge — so they never compete at the same simulation time.
Input skew: how far before the clock edge the testbench samples a signal (default #1step — one delta cycle before posedge). Output skew: how far after the clock edge the testbench drives a signal (default #1step — one delta cycle after posedge). Custom skew: input #2ns data; samples 2 ns before the edge. The DUT sees the TB's new value only in the next clock cycle.
By moving the TB's drive to after the clock edge (output skew), the DUT has already latched the current cycle's values before the TB changes the signal. By sampling before the edge (input skew), the TB reads the fully-settled previous-cycle value, not an in-flight update. Together these ensure TB and DUT never race for the same signal at the same time.
##1 is a cycle delay counted in clocking block edges — "advance one clock cycle." It is used inside processes that operate in clocking block context (after @(cb)). ##3 waits 3 cycles. In concurrent assertions (SVA), ##1 means "exactly one cycle later." It is completely different from #1 (a 1ns/1-unit time delay).