Clocking Blocks
How clocking blocks capture clock timing and synchronisation requirements, sample inputs with precise skews, drive outputs at cycle boundaries, enable race-free testbenches through cycle abstraction, and interact with interfaces and default clocking.
⏱ Introduction
A clocking block assembles a group of signals that share a common clock. It makes their timing explicit, captures input sample points and output drive times, and is the key construct for writing testbenches at the cycle abstraction level — where tests are expressed in terms of cycles and transactions rather than signal waveforms.
Together with the program block, clocking blocks provide three essential capabilities:
- Synchronous events — wait for a clock edge without naming the clock signal directly.
- Input sampling — read the stable, pre-clock value of a signal without worrying about glitches.
- Synchronous drives — write to a signal at a defined time relative to the clock edge.
📄 Clocking Block Declaration
// Basic form clocking cb_name @(posedge clk); default input #1step; // default: sample 1 step before clock default output #2ns; // default: drive 2ns after clock input data, ready, valid; // uses default input skew output ack; // uses default output skew endclocking // Combined default in one line clocking ck1 @(posedge clk); default input #1step output negedge; input ...; output ...; endclocking // Full bus example: mixed skews, hierarchical signal, per-signal override clocking bus @(posedge clock1); default input #10ns output #2ns; input data, ready; input enable = top.mem1.enable; // hierarchical source output negedge ack; // override: drive on negedge input #1step addr; // override: sample 1step before edge endclocking
#1step and the default output skew is #0. The 1step unit samples the signal’s value in the Postponed region of the time step immediately before the clock edge — capturing the fully-settled, glitch-free value.
Signal directions in a clocking block
| Direction | Testbench sees it as | Sampled? | Driven? |
|---|---|---|---|
| input | Read-only from testbench | Yes — at clock edge minus input skew | No |
| output | Write-only from testbench | No | Yes — at clock edge plus output skew |
| inout | Readable and writable | Yes | Yes |
input is a signal the testbench reads — which means the DUT is driving it. A clocking block output is driven by the testbench — the DUT reads it. When connecting through a modport, the DUT’s modport uses reversed directions.
🕐 Input and Output Skews
Input skew places the sample point before the clock edge. Output skew places the drive point after the clock edge. Both are measured in simulation time units relative to the clocking event.
// 1ps input skew: sample 1 picosecond before the clock clocking dram @(clk); input #1ps address; input #5 output #6 data; // per-signal skew for data endclocking // #1step: sample the steady-state value from the previous time step // — always the signal's last value before the clock edge fires clocking safe @(posedge clk); input #1step addr; // guaranteed glitch-free endclocking // #0 input skew: sampled in the Observed region (not Inactive) // #0 output skew: driven as NBA assignment at the clocking event
#1 (which is a real time unit and depends on the timescale), #1step is resolution-independent.
📋 Hierarchical Expressions
Any clocking block signal can map to an arbitrary hierarchical expression — not just a local port. This includes slices, concatenations, and deep hierarchical paths.
// Simple hierarchical signal — uses a remote signal name as the clocking input clocking cd1 @(posedge phi1); input #1step state = top.cpu.state; // local name 'state', source is top.cpu.state endclocking // Concatenation and slices as a single clocking input clocking mem @(clock); input instruction = {opcode, regA, regB[3:1]}; endclocking // Access via: mem.instruction
state) is the local alias. The hierarchical expression after = is where the data actually comes from. Testbench code always uses the local alias: cd1.state — never the full path.
📌 Signals in Multiple Clocking Blocks
The same signal can appear in more than one clocking block. When two clocking blocks share the same clock expression, they share the same synchronisation event — similar to how multiple flip-flops can share a clock. Each clocking block still samples or drives the signal independently according to its own skew.
// signal j appears as output in two different-clock clocking blocks reg j; clocking pe @(posedge clk); output j; endclocking clocking ne @(negedge clk); output j; endclocking // j takes the most recently driven value — enables DDR memory modelling
📌 Scope and Lifetime
- A clocking block is both a declaration and an instance — no separate instantiation step is needed. One copy is created per enclosing module/interface/program instance.
- Clocking block signals are accessed via
clocking_name.signal_name. - Clocking blocks have static lifetime and scope local to their enclosing declaration.
- Clocking blocks cannot be nested.
- They cannot be declared inside functions, tasks, packages, or at the compilation-unit level — only inside a module, interface, or program.
📋 Multiple Clocking Blocks Example
// Two-clock testbench program with two clocking blocks program test( input phi1, input [15:0] data, output logic write, input phi2, inout [8:1] cmd, input enable ); reg [8:1] cmd_reg; clocking cd1 @(posedge phi1); input data; output write; input state = top.cpu.state; // hierarchical endclocking clocking cd2 @(posedge phi2); input #2 output #4ps cmd; input enable; endclocking initial begin // cd1.data — read data via cd1 (sampled at posedge phi1) // cd2.cmd — read/write cmd via cd2 end assign cmd = enable ? cmd_reg : 'x; endprogram // Top level: connect program to DUTs module top; logic phi1, phi2; wire [8:1] cmd; // wire: two bidirectional drivers logic[15:0] data; test main(phi1, data, write, phi2, cmd, enable); cpu cpu1(phi1, data, write); mem mem1(phi2, cmd, enable); endmodule
📋 Interfaces and Clocking Blocks
Combining clocking blocks with interfaces greatly reduces connection boilerplate. Each interface has two modports — one for the testbench (test), one for the DUT (dut) — with reversed directions.
interface bus_A (input clk); logic [15:0] data; logic write; modport test(input data, output write); modport dut (output data, input write); endinterface program test(bus_A.test a); // interface port with modport clocking cd1 @(posedge a.clk); input a.data; output a.write; endclocking // Access: cd1.a.data (long form) // OR: use hierarchical aliases for shorter names clocking cd1b @(posedge a.clk); input data = a.data; // local alias 'data' output write = a.write; // local alias 'write' endclocking // Access: cd1b.data (short form) endprogram module top; logic phi1; bus_A a(phi1); // interface instance test main(a); cpu cpu1(a); endmodule
⚡ Clocking Block Events
The clocking block name itself can be used as an event in @ expressions. It is equivalent to waiting for the clocking block’s own clocking event.
clocking dram @(posedge phi1); inout data; output negedge #1 address; endclocking @(dram); // equivalent to @(posedge phi1) // Useful inside program blocks for clean cycle-based synchronisation initial repeat(10) @(dram); // wait 10 dram cycles
## Cycle Delay — ##
The ## operator delays execution by a specified number of clocking events (cycles) of the default clocking block in scope. It is the cycle-level equivalent of #N for time delays.
// ## N : wait N clock cycles (of the default clocking) ## 5; // wait 5 clocking events ## (j + 1); // wait j+1 cycles (expression evaluated at runtime) ## WAIT_CYCLES; // identifier (parameter or localparam) // Also valid as an intra-assignment delay in synchronous drives ##1 bus.data <= 8'hz; // wait 1 cycle, then drive data to Hi-Z bus.data <= ##2 r; // sample r now, drive to bus.data 2 cycles later
default clocking has been declared for the enclosing module, interface, or program, using ## is a compile error.
📌 Default Clocking
One clocking block per module/interface/program can be declared as the default. The default clocking is used implicitly by all ## cycle delay operations within that scope.
// Form 1: inline — declare and set as default in one step program test(input bit clk, input reg [15:0] data); default clocking bus @(posedge clk); inout data; endclocking initial begin ## 5; // wait 5 posedge clk cycles if (bus.data == 10) ## 1; // wait 1 more cycle if data==10 end endprogram // Form 2: name an existing clocking block as the default module processor; clocking busA @(posedge clk1); ... endclocking clocking busB @(negedge clk2); ... endclocking module cpu(...); default clocking busA; // busA is now the default in cpu initial begin ## 5; // waits 5 posedge clk1 cycles end endmodule endmodule
default clocking more than once in the same program, module, or interface is a compile error. A default clocking is local to its declaring scope — it does not propagate into instantiated sub-modules.
📌 Input Sampling
All clocking block inputs (input or inout) are sampled at the clocking event, adjusted by the input skew:
- Non-zero skew (including
#1step): sampled in the Postponed region of the time step skew time units before the clock edge. - Explicit
#0skew: sampled in the Observed region of the clock-edge time step (avoids the race with Active events but is the same simulation time).
Sampling is immediate — the calling process does not block. When a clocking block signal appears in an expression, it is automatically replaced with the most recently sampled value. For the same signal appearing in multiple clocking blocks, each block samples independently with its own skew.
clocking cb @(posedge clk); input #1step bus_data; // sampled at last Postponed region before posedge clk endclocking initial begin @(cb); // wait for posedge clk $display(cb.bus_data); // reads the sampled value from last cycle end
⚡ Synchronous Events
The @ operator can reference clocking block signals or the clocking block itself for synchronisation. Values used in these event expressions are the sampled (synchronous) values.
// Wait for next change of a specific clocking block signal @(ram_bus.ack_l); // Wait for the next clocking event of ram_bus (posedge/negedge of its clock) @(ram_bus); // Wait for a positive edge of a clocking block input @(posedge ram_bus.enable); // Wait for falling edge of a 1-bit slice — index evaluated at runtime @(negedge dom.sign[a]); // Wait for EITHER posedge dom.sig1 OR any change of dom.sig2 @(posedge dom.sig1 or dom.sig2); // Wait for negedge dom.sig1 or posedge dom.sig2, whichever first @(negedge dom.sig1 or posedge dom.sig2);
← Synchronous Drives
Clocking block outputs are driven using a non-blocking (<=) syntax. The actual signal update happens at the output skew time relative to the clocking event.
// Drive data in the NBA region of the current cycle (skew=0) bus.data[3:0] <= 4'h5; // Wait 1 bus cycle, then drive data to Hi-Z ##1 bus.data <= 8'hz; // Wait 2 default clocking cycles then drive data to 2 ##2; bus.data <= 2; // Intra-assignment: sample r NOW, drive bus.data 2 (bus) cycles later // — process does NOT block bus.data <= ##2 r; // Conflict detection: two drives to same output in same time step // Conflicting bits → X (or 0 for 2-state) pe.nibble <= 4'b0101; pe.nibble <= 4'b0011; // conflict → driven value = 4'b0xx1
Synchronous drives and NBA assignments
Synchronous signal drives are processed as nonblocking assignments. When a clocking block output corresponds to a wire, a continuous-assignment-like driver is created inside the clocking block and updated via NBA. When driving a reg, the value is assigned directly via NBA at the appropriate output skew time.
📋 Quick Reference
Clocking block structure
| Item | Syntax example | Effect |
|---|---|---|
| Declaration | clocking cb @(posedge clk); … endclocking | Creates the clocking block and its instance |
| Default skew | default input #1step output #2ns; | Sets skew for all signals not individually overridden |
| Input signal | input data; | Sampled at clock − input_skew; read as cb.data |
| Output signal | output ack; | Driven at clock + output_skew; written as cb.ack <= |
| Inout signal | inout cmd; | Both sampled and driven |
| Hierarchical alias | input state = top.cpu.state; | Local name maps to remote hierarchical signal |
| Per-signal skew | output negedge ack; | Overrides default for this signal only |
| Default clocking | default clocking cb; | Sets cb as the target of ## cycle delays |
Skew rules
#1step— sample in Postponed region of the time step just before the clock edge. Preferred for inputs.#Nns/#N— real time offset. Input: N units before. Output: N units after.#0— input sampled in Observed region; output driven as NBA at the clock event time.- Skew is a constant expression; can be a parameter.
Synchronous drive forms
cb.sig <= val;— drive at current clock + output skew.##N cb.sig <= val;— wait N clock cycles then drive (blocking form).cb.sig <= ##N val;— sample val now, drive N cycles later (non-blocking intra-assignment).
$exit task.
