Global training solutions for engineers creating the world's electronics

SystemVerilog Clocking Tutorial

Clocking blocks have been introduced in SystemVerilog to address the problem of specifying the timing and synchronisation requirements of a design in a testbench.

A clocking block is a set of signals synchronised on a particular clock. It basically separates the time related details from the structural, functional and procedural elements of a testbench. It helps the designer develop testbenches in terms of transactions and cycles. Clocking blocks can only be declared inside a module, interface or program.

First Example

Here is a simple example to illustrate how SystemVerilog’s clocking construct works. Consider a loadable, up/down binary counter:
module COUNTER (input Clock, Reset, Enable, Load, UpDn,
                input [7:0] Data, output reg[7:0] Q);
  always @(posedge Clock or posedge Reset)
    if (Reset)
      Q <= 0;
    else
      if (Enable)
        if (Load)
          Q <= Data;
        else
          if (UpDn)
            Q <= Q + 1;
          else
            Q <= Q - 1;
endmodule
The testbench to test this counter, without using the clocking construct, might look like this:
module Test_Counter;
  timeunit 1ns;

  reg Clock = 0, Reset, Enable, Load, UpDn;

  reg [7:0] Data;
  wire [7:0] Q;
  reg OK;

  // Clock generator
  always
  begin
    #5 Clock = 1;
    #5 Clock = 0;
  end

  // Test stimulus
  initial
  begin
    Enable = 0;
    Load = 0;
    UpDn = 1;
    Reset = 1;
    #10; // Should be reset
    Reset = 0;
    #10; // Should do nothing - not enabled
   Enable = 1;    #20; // Should count up to 2
    UpDn = 0;
    #40; // Should count downto 254
    UpDn = 1;

    // etc. ...
  end

  // Instance the device-under-test
  COUNTER G1 (Clock, Reset, Enable, Load, UpDn, Data, Q);

  // Check the results
  initial
  begin
    OK = 1;
    #9;
    if (Q !== 8'b00000000)
      OK = 0;
    #10;
    if (Q !== 8'b00000000)
      OK = 0;
    #20;
    if (Q !== 8'b00000010)
      OK = 0;
    #40;
    if (Q !== 8'b11111110)
      OK = 0;
    // etc. ...
  end
endmodule
 The testbench using clocking will look like this:
module Test_Counter_w_clocking;
  timeunit 1ns;

  reg Clock = 0, Reset, Enable, Load, UpDn;
  reg [7:0] Data;
  wire [7:0] Q;

  // Clock generator
  always
  begin
    #5 Clock = 1;
    #5 Clock = 0;
  end

  // Test program
  program test_counter;
    // SystemVerilog "clocking block"
    // Clocking outputs are DUT inputs and vice versa
    clocking cb_counter @(posedge Clock);
      default input #1step output #4;
      output negedge Reset;
      output Enable, Load, UpDn, Data;
      input Q;
    endclocking

    // Apply the test stimulus
    initial begin

      // Set all inputs at the beginning   
      Enable = 0;           
      Load = 0;
      UpDn = 1;
      Reset = 1;

      // Will be applied on negedge of clock!
      ##1 cb_counter.Reset  <= 0;
     // Will be applied 4ns after the clock!
     ##1 cb_counter.Enable <= 1;
      ##2 cb_counter.UpDn   <= 0;
      ##4 cb_counter.UpDn   <= 1;
      // etc. ...     
    end

    // Check the results - could combine with stimulus block
    initial begin
      ##1  
      // Sampled 1ps (or whatever the precision is) before posedge clock
      ##1 assert (cb_counter.Q == 8'b00000000);
      ##1 assert (cb_counter.Q == 8'b00000000);
     ##2 assert (cb_counter.Q == 8'b00000010);
      ##4 assert (cb_counter.Q == 8'b11111110);
      // etc. ...     
    end

    // Simulation stops automatically when both initials have been completed

  endprogram

  // Instance the counter
  COUNTER G1 (Clock, Reset, Enable, Load, UpDn, Data, Q);

  // Instance the test program - not required, because program will be
  // instanced implicitly.
  // test_COUNTER T1 ();
endmodule
There are a few important things to note: the testbench is implemented as a module, with a nested program that contains the clocking block (the full explanation of the advantages of implementing a testbench using a program can be found in the program article). Program blocks can be nested within modules or interfaces. This way multiple co-operating programs can share variables local to the scope. Nested programs with no ports or top-level programs that are not explicitly instantiated are implicitly instantiated once. Implicitly instantiated programs have the same instance and declaration name.

 

The clocking construct is both the declaration and the instance of that declaration. Note that the signal directions in the clocking block within the testbench are with respect to the testbench. So Q is an output of COUNTER, but a clocking input. Note also that widths are not declared in the clocking block, just the directions.

 

The signals in the clocking block cb_counter are synchronised on the posedge of Clock, and by default all signals have a 4ns output (drive) skew and a #1step input (sample) skew. The skew determines how many time units away from the clock event a signal is to be sampled or driven. Input skews are implicitly negative (i.e. they always refer to a time before the clock), whereas output skews always refer to a time after the clock.

 

An input skew of #1step indicates that the value read by the active edge of the clock is always the last value of the signal immediately before the corresponding clock edge. A step is the time precision.

 

The ## operator is used in the testbench to delay execution by a specified number of clocking events, or clock cycles.

Clocking Block Drives

Clocking block outputs and inputs can be used to drive values onto their corresponding signals, at a certain clocking event and with the specified skew. An important point to note is that a drive does not change the clock block input of an input signal.  This is because reading the input always yields the last sampled value, and not the driven value.

 

Synchronous signal drives are processed as nonblocking assignments. If multiple synchronous drives are applied to the same clocking block output or inout at the same simulation time, a run-time error is issued and the conflicting bits are set to X for 4-state ports or 0 for 2-state ports.

 

Here are some examples using the driving signals from the clocking block cb:
cb.Data[2:0] <= 3'h2;    // Drive 3-bit slice of Data in current cycle
##1 cb.Data <= 8'hz;         // Wait 1 Clk cycle and then drive Data
##2 cb.Data[1] <= 1;         // Wait 2 cycles, then drive bit 1 of Data
cb.Data <= ##1 Int_Data; // Remember the value of Int_Data, and then
                         // drive Data 1 Clk cycle later
cb.Data[7:4] <= 4'b0101;
cb.Data[7:4] <= 4'b0011; // Error: driven value of Data[7:4] is 4’b0xx1

Clocking Blocks and Interfaces

This is an example presenting multiple clocking blocks using interfaces. A clocking block can use an interface to reduce the amount of code needed to connect the testbench.

 

The interface signals will have the same direction as specified in the clocking block when viewed from the testbench side (e.g. modport TestR), and reversed when viewed from the DUT (i.e. modport Ram). The signal directions in the clocking block within the testbench are with respect to the testbench, while a modport declaration can describe either direction (i.e. the testbench or the design under test). To illustrate we will implement two busses, with different clocks, and a testbench separated from the top level. The testbench is implemented as a program.
// Interface definitions
interface DataBus (input Clock);
  logic [7:0] Addr, Data;
  modport TestR (inout Addr, inout Data);
  modport Ram (inout Addr, inout Data);
endinterface

interface CtrlBus (input Clock);
  logic RWn;
  // RWn is output, as it is in the clocking block
  modport TestR (output RWn);
  // RWn is input, reversed than in the clocking block
  modport Ram (input RWn);
endinterface

// Testbench defined as a program, with two clocking blocks
program TestRAM (DataBus.TestR DataInt,
                 CtrlBus.TestR CtrlInt);
  clocking cb1 @(posedge DataInt.Clock);
    inout #5ns DataInt.Data;
    inout #2ns DataInt.Addr;
  endclocking

  clocking cb2 @(posedge CtrlInt.Clock);  
   output #10;
    output RWn = CtrlInt.RWn;  //  Hierarchical expression
  endclocking

  initial begin
    cb2.RWn = 0;
    cb1.DataInt.Data = 1;
    ...
  end
endprogram

module RAM (DataBus.Ram DataInt, CtrlBus.Ram CtrlInt);
  logic [7:0] mem[0:255];

  always @*
    if (CtrlInt.RWn)
      DataInt.Data = mem[DataInt.Addr];
    else
      mem[DataInt.Addr] = DataInt.Data;
endmodule

module Top;
  logic Clk1, Clk2;

  // Instance the interfaces
  DataBus TheDataBus(.Clock(Clk1));
  CtrlBus TheCtrlBus(.Clock(Clk2));

  RAM TheRAM (.DataBus.Ram(TheDataBus.Ram),
              .CtrlBus.Ram(TheCtrlBus.Ram)); // Connect them
  TestRAM TheTest (.DataBus.TestR(TheDataBus.TestR),
                   .CtrlBus.TestR(TheCtrlBus.TestR));
endmodule

Clocking block events

The clocking event of a clocking block can be accessed directly by using the clocking block name, e.g. @(cb) is equivalent to @(posedge Clk). Individual signals from the clocking block can be accessed using the clocking block name and the dot (.) operator. All events are synchronised to the clocking block.

 

Here are some other examples of synchronisation statements:
// Wait for the next change of Data signal from the cb clocking block
@(cb.Data);

// Wait for positive edge of signal cb.Ack
@(posedge cb.Ack);

// Wait for posedge of signal cb.Ack or negedge of cb.Req
@(posedge cb.Ack or negedge cb.Req);

// Wait for the next change of bit 2 of cb.Data
@(cb.Data[2]);
// Wait for the next change of the specified slice
@(cb.Data[7:5]);