SystemVerilog · Module 8
Clocking Blocks
Eliminating TB-DUT races at the clock edge: input/output skews, the ##N operator, default clocking, and the canonical Preponed-sample/Re-NBA-drive pattern.
Module 8 · Page 8.3
How clocking blocks synchronise testbench stimulus and sampling to a clock edge, eliminate the setup/hold race condition between TB and DUT, and provide the ##N cycle-based delay operator.
Going deeper? See Clocking Blocks — Deep Dive, SVA, Coverage, Multi-Clock SoC, CDC for the Module 15.2 treatment covering the clocking block's role as the timing foundation of every modern verification stack — anchoring SVA properties, covergroup sampling events, multi-clock-domain VIPs, and CDC verification.
The Problem — TB and DUT Racing at the Clock Edge
Imagine your testbench drives data and your DUT samples it — both triggered by @(posedge clk). At the exact same simulation instant, the TB is writing the signal and the DUT is reading it. Which one happens first? The answer is undefined — it depends on the order the simulator processes the two always blocks in its internal event queue.
This is the setup/hold race condition in the testbench. Your simulation may pass on VCS but fail on Questa — not because the design is wrong, but because the two tools process active-region events in a different order.
// TB: drives data at the SAME posedge clk that the DUT samples it
always @(posedge clk) begin
data = 8'hAB; // ← TB writes in Active region
end
// DUT: samples data at posedge clk — same event, same Active region
always_ff @(posedge clk) begin
reg_data <= data; // ← DUT reads in Active region
// Does it see 0xAB or the OLD value?
// SIMULATOR-DEPENDENT — this is a race.
endWhat is a Clocking Block?
A clocking block is a construct that attaches to a clock edge and precisely controls when the testbench samples inputs (reads from the DUT) and drives outputs (writes to the DUT). It defines:
- The reference clock edge — which clock and which edge (
posedge/negedge) defines the synchronisation point for all signals in the block. - Input skew — how far before the clock edge the TB samples inputs. Default:
#1step— samples in the Preponed region, before any Active-region writes fire. - Output skew — how far after the clock edge the TB drives outputs. Default:
#1— drives 1 time unit after the edge, safely after the DUT has sampled. - Signal list — which signals are driven or sampled through this clocking block. Each signal gets the clocking block's direction-controlled, skew-adjusted access.
| Track | What happens | Race-safe? |
|---|---|---|
input #1step | TB samples before the edge fires — Preponed region | Yes |
output #1 | TB drives 1 time unit after the edge — DUT already sampled | Yes |
No skew (#0) | TB drives/samples at the exact edge — same instant as DUT | No — race |
Declaring a Clocking Block — Full Syntax
interface apb_if (input logic pclk, presetn);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready, pslverr;
// ① ② ③ ④
clocking cb @(posedge pclk);
// ⑤ Default skews: all inputs sampled #1step before edge,
// all outputs driven #1 after edge
default input #1step output #1;
// ⑥ Input signals — TB reads these from the DUT
input prdata, pready, pslverr;
// ⑦ Output signals — TB drives these to the DUT
output paddr, psel, penable, pwrite, pwdata;
endclocking
// ⑧ Modport for testbench: exposes clocking block + essential inputs
modport tb (clocking cb, input pclk, presetn);
endinterface- ①
clockingkeyword — opens the clocking block. It is a construct inside an interface or module, not a separate file. - ②
cb— the clocking block name. Used to access it:bus.cb.paddr <= value,##1cycles advance relative to this block. - ③
@(posedge pclk)— the synchronising clock event. All stimulus and sampling in this block aligns to this edge. Can beposedge,negedge, oredge. - ④ Clocking block body — contains default skews and the signal list. Every signal listed here is direction-controlled and skew-adjusted automatically.
- ⑤
default input #1step output #1— the skew defaults.#1stepfor inputs means "sample in the Preponed region, one time-step before the clock edge" — guaranteed race-free.#1for outputs means "drive 1 time unit after the clock edge". - ⑥
input prdata, pready, pslverr— signals the TB reads from the DUT. From the TB's perspective these are inputs. They will be sampled at#1stepbefore each posedge. - ⑦
output paddr, psel...— signals the TB drives to the DUT. They will be updated#1time unit after each posedge. The DUT samples them on the NEXT clock edge — well after they are stable. - ⑧
modport tb (clocking cb, ...)— exposes the clocking block through the testbench modport. Theclocking cbentry makesbus.cbaccessible inside any module using this modport.
Input and Output Skew — Every Option
The skew values control the precise timing of sampling and driving relative to the clock edge. Understanding them prevents subtle race conditions.
Input skew options
| Skew | Behaviour | Verdict |
|---|---|---|
input #1step | Best practice (default). Samples in the Preponed region — the very first region of the time step, before any Active-region assignments fire. | Race-free |
input #1 | Samples 1 time unit before the clock edge. Works but depends on the timescale. Fragile when timescale changes. | Fragile |
input #0 | Samples at the exact clock edge — same moment as the Active region. Essentially the same as not using a clocking block. | Race — avoid |
Output skew options
| Skew | Behaviour | Verdict |
|---|---|---|
output #1 | Most common. Drives 1 time unit after the clock edge. The DUT has already sampled its inputs at the edge, so the new value is for the next cycle. | Clean |
output #2 | Drives 2 time units after the edge. Used when downstream logic needs the new value to be stable for longer before the next edge. | Specialised |
output #0 | Drives at the exact clock edge — in the Active region, same as the DUT. DUT may or may not see the new value depending on simulator ordering. | Race — avoid |
Rule of thumb: always use default input #1step output #1 unless you have a specific reason to deviate.
Using a Clocking Block — Complete Example
Once a clocking block is declared in the interface, the testbench accesses it via the interface handle. All signal reads and writes go through bus.cb.signal. Use ##N to advance N clock cycles.
program apb_test (apb_if.tb bus);
initial begin
// ── Reset ─────────────────────────────────────────────────────
bus.cb.psel <= 0; // drive via clocking block — 1 unit after next edge
bus.cb.penable <= 0;
bus.cb.pwrite <= 0;
bus.cb.paddr <= 0;
bus.cb.pwdata <= 0;
##5; // wait 5 clock cycles (relative to posedge pclk in cb)
// ── APB Write transaction ─────────────────────────────────────
##1; // advance 1 cycle — setup phase
bus.cb.paddr <= 32'h4000_0000;
bus.cb.pwdata <= 32'h1234_5678;
bus.cb.psel <= 1;
bus.cb.pwrite <= 1;
##1; // enable phase
bus.cb.penable <= 1;
// Wait until DUT signals ready — read via clocking block (race-free)
while (!bus.cb.pready) ##1;
// ── End of transfer ───────────────────────────────────────────
##1;
bus.cb.psel <= 0;
bus.cb.penable <= 0;
bus.cb.pwrite <= 0;
// ── APB Read transaction ──────────────────────────────────────
##2;
bus.cb.paddr <= 32'h4000_0000;
bus.cb.psel <= 1;
bus.cb.pwrite <= 0;
##1;
bus.cb.penable <= 1;
while (!bus.cb.pready) ##1;
// Read the returned data — sampled in Preponed region, guaranteed stable
$display("Read data: 0x%08h", bus.cb.prdata);
##1;
bus.cb.psel <= 0;
bus.cb.penable <= 0;
##5;
$finish;
end
endprogramThe ##N Cycle Delay Operator
##N is SystemVerilog's clock-cycle delay operator. It means "advance N edges of the clocking block's reference clock". Unlike #N (which delays by N time units regardless of the clock), ##N is always an exact number of clock cycles — independent of timescale.
| Syntax | Meaning | Use case |
|---|---|---|
##1 | Advance exactly 1 clock cycle | Move to next clock edge — most common in testbenches |
##N | Advance N clock cycles | Wait N cycles before next action (reset, setup time) |
##0 | Advance to the current clock edge (if not already there) | Rarely used; synchronise without advancing |
##[1:5] | Advance between 1 and 5 cycles (in SVA sequences) | Non-deterministic delay range in assertions |
##[*] | Zero or more cycles (SVA) | Eventually property — flexible timing in assertions |
// Assume: `timescale 1ns/1ps, clock period = 10ns
// ── #N: time-unit delay (fragile, timescale-dependent) ────────────────
#10; // wait 10ns — equals 1 clock cycle ONLY if clock is exactly 10ns
// If clock changes to 8ns, this waits 1.25 cycles — WRONG
// ── ##N: clock-cycle delay (robust, always correct) ───────────────────
##1; // wait exactly 1 posedge of the clocking block's clock
// If clock changes to 8ns, this still waits exactly 1 cycle — CORRECT
##10; // wait 10 clock cycles — completely clock-frequency independent
// ── In a program block with default clocking ──────────────────────────
default clocking cb @(posedge clk);
default input #1step output #1;
input dout;
output din;
endclocking
initial begin
##5; // resolves against default clocking block
cb.din <= 8'hAB; // drive via default clocking block
##1;
$display("%h", cb.dout); // sample via default clocking block
enddefault clocking
When a program or module has a default clocking declaration, all bare ##N delays and clocking-block signal accesses without an explicit block name resolve against that default. This is the cleanest way to write testbenches — you declare the clock once and use ##N everywhere without qualifying it.
program test (apb_if.tb bus);
// Declare the default clocking — all ##N uses this clock
default clocking bus.cb; // refer to the clocking block in the interface
initial begin
// Reset sequence
##2; // 2 cycles — uses bus.cb automatically
bus.cb.psel <= 0;
##3;
// APB write — cycle by cycle
bus.cb.paddr <= 32'h1000;
bus.cb.pwdata <= 32'hFF;
bus.cb.psel <= 1;
bus.cb.pwrite <= 1;
##1;
bus.cb.penable <= 1;
##1 (bus.cb.pready == 1); // advance 1 cycle AND wait for pready
bus.cb.psel <= 0;
bus.cb.penable <= 0;
##5;
$finish;
end
endprogramClocking Block Events — Waiting for the Clock
A clocking block itself is also a valid event. You can wait on it with @(clocking_block_name). This is equivalent to waiting for the next active edge of the clocking block's clock — and is the preferred way to synchronise testbench code to the clock inside a module (as opposed to a program block with ##N).
module monitor (apb_if.tb bus);
initial begin
forever begin
// Wait for next posedge of the clocking block's clock
@(bus.cb);
// At this point inputs are already sampled in the Preponed region
// bus.cb.prdata holds the value from BEFORE this edge — race-free
if (bus.cb.psel && bus.cb.penable && bus.cb.pready)
$display("[MON] APB %s addr=%h data=%h",
bus.cb.pwrite ? "WR" : "RD",
bus.cb.paddr,
bus.cb.pwrite ? bus.cb.pwdata : bus.cb.prdata);
end
end
endmoduleCommon Mistakes & How to Fix Them
-
Using
=(blocking) to drive a clocking block output. Writingbus.cb.psel = 1;instead ofbus.cb.psel <= 1;. The blocking assignment bypasses the output skew mechanism and drives the signal immediately in the Active region — creating a race with the DUT. Fix: always use<=(non-blocking) when writing to clocking block output signals. The clocking block's output skew is only applied when you use<=. -
Forgetting to include the clocking block in the modport. Declaring a
modport tbwithout theclocking cbentry. Inside any module using that modport,bus.cbis simply inaccessible — compile error. Fix: always addclocking cbto the modport used by the testbench:modport tb (clocking cb, input pclk, presetn);. -
Using
#Ninstead of##Nfor clock-cycle delays. Writing#10to wait "one clock cycle" when the clock period is 10ns. This breaks as soon as the clock frequency changes. It also does not synchronise to the clocking block — it is a raw time delay. Fix: use##1(or##N) for all cycle-based delays in testbench code. This is always exactly N clock cycles, regardless of the clock period or timescale. -
Reading a clocking block output instead of an input. The clocking block declares
output paddr(TB drives it) andinput prdata(TB reads it). Trying to readbus.cb.paddrinside the TB reads back the last value you drove — not the DUT's current state. Confusing and misleading. Fix: only read signals declared asinputin the clocking block — those are DUT outputs. For signals the TB drives (outputin the clocking block), you already know the value since you drove it.
Quick Reference — Clocking Block at a Glance
// ── Declare inside an interface ───────────────────────────────────────
clocking cb @(posedge clk);
default input #1step output #1; // always use these defaults
input dut_out_a, dut_out_b; // DUT → TB (TB reads these)
output dut_in_x, dut_in_y; // TB → DUT (TB drives these)
endclocking
// ── Add to modport for testbench ──────────────────────────────────────
modport tb (clocking cb, input clk, rst_n);
// ── In the testbench program ──────────────────────────────────────────
default clocking bus.cb;
// Drive outputs — always use <=
bus.cb.dut_in_x <= 8'hAB;
// Read inputs — always stable (sampled in Preponed)
logic val = bus.cb.dut_out_a;
// Advance clock cycles — always ##N not #N
##1; // 1 cycle
##5; // 5 cycles
##1 (bus.cb.dut_out_b == 1); // 1 cycle AND wait for condition
// Wait for clock event
@(bus.cb); // synchronise to next posedge
// Per-signal skew override (overrides the default for just this signal)
clocking cb2 @(posedge clk);
default input #1step output #1;
output #2 slow_signal; // this one signal uses #2 instead of #1
input fast_sig;
endclockingVerification Usage — Clocking Blocks Are How UVM Drivers and Monitors Talk to the DUT
In any non-trivial UVM testbench, every driver and every monitor accesses bus signals through a clocking block — never bare. The driver drives via the testbench-side clocking block; the monitor samples via a separate monitor-only clocking block that has no drive permission. This isn't optional discipline; it is the canonical pattern. Skipping it means sporadic regression failures and the kind of debug sessions that end with "we added a #1 delay and now it works."
// ── Interface with TB and monitor clocking blocks ───────────────
interface apb_if(input logic clk);
logic [31:0] paddr, pwdata, prdata;
logic psel, penable, pwrite, pready;
// Testbench-side: samples late, drives at NBA
clocking tb_cb @(posedge clk);
default input #1step output #0;
output paddr, pwdata, pwrite, psel, penable;
input prdata, pready;
endclocking
// Monitor-only: samples late, never drives
clocking mon_cb @(posedge clk);
default input #1step;
input paddr, pwdata, prdata, psel, penable, pwrite, pready;
endclocking
modport tb (clocking tb_cb);
modport monitor(clocking mon_cb);
endinterface
// ── UVM driver — every bus access through the clocking block ──
class apb_driver extends uvm_driver;
virtual apb_if.tb vif;
task run_phase(uvm_phase phase);
forever begin
apb_xact tr; seq_item_port.get_next_item(tr);
@(vif.tb_cb); // wait for posedge
vif.tb_cb.paddr <= tr.paddr; // drives land Re-NBA
vif.tb_cb.pwdata <= tr.pwdata;
vif.tb_cb.psel <= 1'b1;
@(vif.tb_cb);
vif.tb_cb.penable <= 1'b1;
while (!vif.tb_cb.pready) @(vif.tb_cb); // sampled Preponed
vif.tb_cb.psel <= 1'b0;
vif.tb_cb.penable <= 1'b0;
seq_item_port.item_done();
end
endtask
endclass
// ── UVM monitor — input-only clocking block ────────────────────
class apb_monitor extends uvm_monitor;
virtual apb_if.monitor vif;
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready) begin
apb_xact obs = apb_xact::type_id::create("obs");
obs.paddr = vif.mon_cb.paddr;
obs.pwdata = vif.mon_cb.pwdata;
ap.write(obs);
end
end
endtask
endclassSimulation Behavior — Where Clocking Block Samples and Drives Land in Time
The clocking block's value is not the syntax — it's the timing semantics. Every read through a clocking block returns the signal's value as of the Preponed region (i.e., before any procedural code or NBA update fires at the current edge). Every drive through a clocking block lands in the Re-NBA region (i.e., after the DUT's NBA updates have settled). Understanding the per-region scheduling unlocks the right mental model for every test you write.
// DUT
module dut(input apb_if.slave apb);
always_ff @(posedge apb.clk) begin
if (apb.psel && apb.penable)
internal_reg <= apb.pwdata; // NBA update
end
endmodule
// TB
module tb;
apb_if u_apb(.clk);
dut u_dut(.apb(u_apb.slave));
initial begin
@(u_apb.tb_cb);
u_apb.tb_cb.pwdata <= 32'hAB;
u_apb.tb_cb.psel <= 1'b1;
u_apb.tb_cb.penable<= 1'b1;
@(u_apb.tb_cb);
// What does the next read see?
$display("after edge: pwdata=%h, internal=%h",
u_apb.tb_cb.pwdata, // sampled Preponed: pre-edge value
u_apb.u_dut.internal_reg); // raw access: post-NBA value
end
endmoduleWaveform Analysis — What Clocking Block Timing Looks Like in Verdi/DVE
Open the waveform viewer on a clocking-block-protected TB. The bus signals look exactly as you'd expect — paddr, psel, etc. drive cleanly relative to the clock. What's invisible in the waveform but visible in the cycle count is the structural race immunity: zero glitches, zero X's, zero "value depends on simulator" results.
With clocking block (input #1step, output #0):
clk _│‾│_│‾│_│‾│_│‾│_│‾│_
paddr -- 100 100 100 -- ← drives land cleanly after NBA
psel 0 1 1 1 1 0
penable 0 0 1 1 1 0
pready 0 0 0 0 1 -- ← stable Preponed sample
internal_reg -- -- -- -- 100 100 ← DUT NBA captures stable value
No glitch on paddr at the clock edge. No race between TB drive and DUT sample.
Cycle count is deterministic across simulator versions.
Without clocking block (raw vif access in driver):
clk _│‾│_│‾│_│‾│_│‾│_│‾│_
paddr X→100 ? ? ? -- ← potential X glitch as TB drives
same time DUT samples
internal_reg -- -- X? 100? -- -- ← either 100 or X depending on scheduler
Test sometimes captures 100, sometimes X. Same source, different simulator
versions or different machine loads produce different results.
Classic sporadic regression failure mode.Industry Insights — Clocking Block Practices in Production Teams
Debugging Academy — 5 Real Clocking Block Bugs
Each lab is a real failure mode pulled from production projects. Buggy code, symptom, root cause, fix.
Raw vif Access Mixed with Clocking Block — Sporadic Race
RACE CONDITION// Driver mostly uses the clocking block, but one access leaked:
task drive_write(int addr, int data);
@(vif.tb_cb);
vif.tb_cb.paddr <= addr;
vif.tb_cb.pwdata <= data;
vif.tb_cb.psel <= 1;
// ❌ Forgot to use tb_cb here:
vif.penable <= 1; // raw access — Active region
@(vif.tb_cb);
while (!vif.tb_cb.pready) @(vif.tb_cb);
endtaskThe test passes on VCS in the morning, fails on Questa in the afternoon — same source, same seed. Sometimes the DUT sees penable=1 in the cycle the TB intended; sometimes it sees it a cycle late. $display traces show the bug "disappearing" when extra prints are added (which slow down execution and change scheduling).
One raw vif.penable assignment landed in the Active region; the other three landed in Re-NBA via the clocking block. The DUT's NBA update of internal state runs between them. Depending on the simulator's scheduler, the DUT may see penable one cycle early or one cycle late.
Every TB-side bus access must go through the clocking block: change vif.penable <= 1; to vif.tb_cb.penable <= 1;. Promote this to a lint rule: any vif.<signal> outside of vif.tb_cb. or vif.mon_cb. in a UVM file is a CI failure. Lint catches this in the next commit instead of in regression three weeks later.
Sampling Skew of 0 — Driver Reads Its Own Just-Driven Value
SAMPLE TIMINGclocking tb_cb @(posedge clk);
default input #0 output #0; // ← input #0 is the bug
output paddr, pwdata, /* ... */;
input prdata, pready;
endclocking
// Driver
task wait_for_ready();
while (!vif.tb_cb.pready) @(vif.tb_cb);
// Sometimes sees pready=1 immediately even though DUT hasn't asserted it yet
endtaskDriver exits its wait_for_ready wait state on the same edge it drove psel/penable — DUT hasn't even seen the transaction yet, but TB believes it's complete. Subsequent transactions queue up too fast; the bus shows back-to-back transfers when the DUT never asserted pready for the first one.
input #0 samples in the Active region — at the same instant as the edge fires. The driver's just-driven values are still "visible" because their NBA updates and the DUT's NBA updates haven't run yet. The standard, race-free default is input #1step, which samples in the Preponed region (immediately before the edge), guaranteeing the sample is the stable pre-edge value.
default input #1step output #0;
// Reads see the pre-edge value (Preponed); drives land after NBA (Re-NBA).Almost every production interface uses exactly this combination. Deviations need an explicit justification and a code-review note explaining why.
##N with Default Clocking Block from Another Bus
DEFAULT DRIFT// Two buses with different clocks
apb_if u_apb(.clk(apb_clk)); // 50 MHz
axi_if u_axi(.clk(axi_clk)); // 200 MHz
module tb;
default clocking u_apb.tb_cb; // ← APB is the default
initial run_test();
endmodule
// In an AXI driver task:
task axi_burst_4cycles();
##4; // ← intended 4 AXI cycles (20 ns); actually 4 APB cycles (80 ns)!
end_transaction();
endtaskAXI bursts complete in 80 ns instead of 20 ns. AXI throughput tests report 4× lower bandwidth than the spec. The test code looks "obviously right" because ##4 reads as "4 cycles" — but it's 4 cycles of the wrong clock.
##N resolves to "N cycles of the default clocking block." With a top-level default clocking u_apb.tb_cb, every unqualified ##N in any file uses APB's clock — even inside AXI driver code, which expected AXI's clock.
// In the AXI driver, use explicit clocking-block events:
repeat (4) @(vif.tb_cb);Never use default clocking in a multi-bus testbench. Use it only at the test top level and never in reusable agent code.
Clocking Block Drive After Reset Released — Captures Stale Value
RESET RACE// Reset is asynchronous; TB drives signals just before/during reset
initial begin
apb.tb_cb.psel <= 0;
apb.tb_cb.penable <= 0;
rst_n = 0;
#50ns;
rst_n = 1; // release reset
@(apb.tb_cb);
apb.tb_cb.paddr <= 32'h100; // first real transaction
apb.tb_cb.pwdata <= 32'hAB;
apb.tb_cb.psel <= 1;
// ... DUT sometimes captures uninitialised internal_reg before this drive
endFirst post-reset transaction sometimes succeeds, sometimes shows the DUT capturing X or a previous value in internal_reg. The bug looks like a DUT reset issue but is actually a TB scheduling order during the reset release.
Reset is asynchronous (raw rst_n = 0;), but the TB's first drive is via the clocking block (Re-NBA region). The DUT's first post-reset clock edge may fire before the clocking-block drive lands — the DUT sees the bus in its reset-default state (X for output signals without explicit reset).
apb.tb_cb.psel <= 0;
apb.tb_cb.penable <= 0;
// ... reset release ...
repeat (2) @(vif.tb_cb); // let Re-NBA drives propagate
// now begin first real transactionDrive initial bus values through the clocking block before releasing reset, and use repeat (2) @(vif.tb_cb); after the reset release before the first real transaction. Alternative: keep the bus idle (driven to known reset values) for the entire reset period — many TB libraries provide a drive_idle() task for this exact purpose.
Clocking Block in Synthesisable File — Synthesis Drops It Silently
SIM/SYNTH GAP// Interface shipped to synthesis with the clocking block visible:
interface apb_if(input logic clk);
logic [31:0] paddr, pwdata;
logic psel, penable;
clocking tb_cb @(posedge clk);
default input #1step output #0;
output paddr, pwdata, psel, penable;
endclocking
modport tb(clocking tb_cb);
endinterface
// Some RTL module accidentally references the clocking block:
module bad_rtl(apb_if.tb apb);
always_ff @(posedge apb.clk)
apb.tb_cb.paddr <= some_addr; // ← references clocking block!
endmoduleSimulation works fine. Synthesis runs with a buried warning: "clocking block reference ignored." The synthesised netlist has no driver for paddr from this module. Silicon validation finds the RTL never asserts the address bus.
Clocking blocks are simulation-only constructs. Any RTL module that references one via vif.cb.signal has nothing to drive once synthesis strips the clocking block. The warning lives in a 50000-line log; nobody sees it.
Two layered defences. (1) RTL modules never connect via the tb modport — they use master/slave modports that don't have clocking blocks. (2) Wrap clocking blocks in `ifndef SYNTHESIS so synthesis never even sees them. (3) Lint rule: any RTL module accessing vif.<clocking_block> is a CI failure. Once enforced, the bug class is structurally eliminated.
Interview Q&A — 12 Questions on Clocking Blocks
Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.
The testbench-vs-DUT race at the active clock edge. Without clocking blocks, TB procedural assigns and DUT NBA updates run in implementation-defined order in the Active region; the test may see different values on different simulators or different runs. Clocking blocks specify when the TB samples (Preponed: before any edge-triggered work) and when the TB drives (Re-NBA: after DUT NBA updates). Race-free by construction.
Best Practices — Clocking Block Rules to Walk Away With
- Every protocol interface has at least one clocking block. No exceptions for "slow" or "simple" buses — retrofit cost is always higher than initial cost.
- Use
default input #1step output #0. The canonical race-free skew. Deviations need explicit code-review justification. - Two clocking blocks per interface:
tb_cb(driver) andmon_cb(monitor-only). Drivers attach via tb modport; monitors via monitor modport. Roles enforced structurally. - Lint rule: no bare
vif.<signal>in UVM files. Every access throughvif.tb_cb.orvif.mon_cb.. CI fails on violation. - Never mix
##Nand@(posedge clk)in the same task. Pick clocking-block-relative or raw-event timing; don't combine. - Avoid
default clockingin multi-bus testbenches.##Nsilently uses the wrong clock. Reusable components qualify every reference. - Drive idle values through the clocking block during reset. Prevents first-post-reset transactions from seeing X on the bus.
- Guard clocking blocks behind
`ifndef SYNTHESISor keep them in TB-only files. Synthesis silently strips them; RTL accidentally referencing them silently drops drives. - Sample DUT internals via clocking blocks too. Hierarchical-reference reads in monitors and scoreboards have the same race exposure; clocking-block-protect them as well.
- Document the clocking-block contract in the interface header. Sampled signals, driven signals, skew values, intended consumers. The next reader needs the contract, not a forensic read of the source.