Clocking Blocks and Modports

Clocking Blocks in SystemVerilog

A clocking block in System Verilog is a construct that simplifies and synchronizes signal interactions with a clock edge. It provides a clean way to specify the timing relationship between input and output signals in a testbench. Additionally, the clocking skew feature within clocking blocks provides fine-grained control over signal sampling and driving relative to a clock edge.

Clocking skew defines when signals are sampled or driven relative to the clock edge. This feature is critical for ensuring timing correctness in designs where the sampling and driving windows need precise alignment.

Why Use Clocking Blocks?

  1. Synchronization: Ensures inputs are sampled and outputs are driven at the correct clock edges.
  2. Timing Isolation: Abstracts timing details, reducing the risk of race conditions in testbenches.
  3. Readability: Improves clarity by defining input and output signals explicitly.

Syntax of Clocking Blocks:

clocking cb @(posedge clk);
default input #2 output #1;
    input  data_in;
    output data_out;
    inout  bidir_signal;
endclocking

Here:

  • @(posedge clk) specifies the clock edge at which signals are sampled or driven.
  • input signals are sampled at the clock edge.
  • output signals are driven after the clock edge.
  • inout signals can be both sampled and driven.
  • Input Skew: The input signals will be sampled 2 times unit before the clock edge.
  • Output Skew: Delays the driving of output signals by a specified time relative to the clock edge. Here it is 1 time unit.

By default, the value of input and output skew is 0 time unit if it is not specified. The input and output skew must be parameter or constant.

Modports in SystemVerilog

A modport in System Verilog defines the access rules for an interface. It specifies which signals are inputs, outputs, or inouts for a particular module, providing fine-grained control over how different modules interact with the interface.

Why Use Modports?

  1. Direction Control: Clearly defines the direction (input, output, inout) of signals for each module accessing the interface.
  2. Role Separation: Enables different modules to use the same interface with different roles.
  3. Code Clarity: Improves readability by explicitly stating signal directions.

Syntax of Modports:

interface simple_if;
    logic a, b, c;

    // Define modports
    modport master (input a, output b, inout c);
    modport slave  (output a, input b, inout c);
endinterface

Here:

  • The master modport allows a module to read a, write b, and read/write c.
  • The slave modport reverses the roles, allowing a module to write a, read b, and read/write c.

Combining Clocking Blocks and Modports

Clocking blocks and modports can be used together to create highly modular and synchronized verification environments.

Example:

  1. Clocking block and modport in Interface and connecting interface to module
interface bus_if(input logic clk);
    // Signals in the interface
    logic [7:0] data;
    logic valid;
    logic ready;

    // Clocking block for the testbench
    clocking tb_cb @(posedge clk);
        output data, valid;   // Testbench drives these signals
        input ready;          // DUT drives this signal
    endclocking

    // Clocking block for the DUT
    clocking dut_cb @(posedge clk);
        input data, valid;    // DUT samples these signals
        output ready;         // DUT drives this signal
    endclocking

    // Modports to define roles
    modport DUT (clocking dut_cb); // DUT uses dut_cb clocking block
    modport TB (clocking tb_cb);  // Testbench uses tb_cb clocking block
endinterface

module dut(bus_if.DUT vif);
  
  always @(vif.dut_cb) begin
    if (vif.dut_cb.valid) begin
      vif.dut_cb.ready <= 1; // Ready when valid is asserted
    end else begin
      vif.dut_cb.ready <= 0; // Not ready otherwise
    end
  end
  
endmodule

2. Testbench

module testbench;
    logic clk;
    bus_if vif(clk); // Instantiate the interface and connect to clk

    // DUT instantiation using the modport
    dut u_dut(.vif(vif.DUT));

    // Clock generation
    initial begin
        clk = 0;
        forever #5 clk = ~clk; // 10ns clock period
    end
  
  // Testbench Class
    class tb_class;
        virtual bus_if.TB vif; // Use the TB modport of the interface

        function new(virtual bus_if.TB vif);
            this.vif = vif;
        endfunction

        // Task to drive transactions
        task run_test();
            vif.tb_cb.valid <= 0;
            vif.tb_cb.data <= 0;
            #10;

            vif.tb_cb.valid <= 1; // Assert valid
            vif.tb_cb.data <= 8'hA5; // Send data
            #10;

            vif.tb_cb.valid <= 0; // Deassert valid
            vif.tb_cb.data <= 0;
            #10;
        endtask
    endclass

    // Testbench Initialization
    initial begin
      tb_class tb = new(vif.TB); // Pass the TB modport to the testbench class
        tb.run_test();

        // End simulation
        $finish;
    end

    // Monitor the interface signals
    initial begin
        $monitor("Time: %0t | valid: %b | ready: %b | data: %h", 
                 $time, vif.tb_cb.valid, vif.tb_cb.ready, vif.tb_cb.data);
    end 
endmodule

Output:

Time:    0 | valid: 0 | ready: 0 | data: 00
Time:   10 | valid: 1 | ready: 1 | data: A5
Time:   20 | valid: 0 | ready: 0 | data: 00