SYSTEMVERILOG SERIES · SV-15

SystemVerilog Series — SV-15: Clocking Blocks — VLSI Trainers
SystemVerilog Series · SV-15

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
Default skews: if not overridden, the default input skew is #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

DirectionTestbench sees it asSampled?Driven?
inputRead-only from testbenchYes — at clock edge minus input skewNo
outputWrite-only from testbenchNoYes — at clock edge plus output skew
inoutReadable and writableYesYes
Directions are from the testbench’s perspective, not the DUT. A clocking block 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.

clock
Input sampled here — before the clock edge by input skew
Output driven here — after the clock edge by output skew
// 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
#1step is the preferred input skew for testbench signals. It conceptually samples in the Preponed region — before any Active events in the clock-edge time step — giving you the clean, settled value from the previous cycle. Unlike #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
Local alias vs remote source. The name you declare inside the clocking block (e.g. 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
## requires a default clocking in scope. If no 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
Only one default per scope. Declaring 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 #0 skew: 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
Reading an inout clocking block signal always returns the sampled value, not the driven value. Driving a signal via a clocking block output does not immediately change what you read back through the clocking block input side. The input reads the last sample taken at the clocking event — not the value being driven.

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

ItemSyntax exampleEffect
Declarationclocking cb @(posedge clk); … endclockingCreates the clocking block and its instance
Default skewdefault input #1step output #2ns;Sets skew for all signals not individually overridden
Input signalinput data;Sampled at clock − input_skew; read as cb.data
Output signaloutput ack;Driven at clock + output_skew; written as cb.ack <=
Inout signalinout cmd;Both sampled and driven
Hierarchical aliasinput state = top.cpu.state;Local name maps to remote hierarchical signal
Per-signal skewoutput negedge ack;Overrides default for this signal only
Default clockingdefault 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).
Coming next: SV-16 covers the Program Block — its purpose as a testbench container, how it schedules in the Reactive region to eliminate design-testbench races, multiple program instances, and the $exit task.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top