SystemVerilog——时钟块 Clocking Blocks

预备知识:SystemVerilog Interface

15.0 写在前面

本系列旨在提供准确,流畅,完整的 SystemVerilog 3.1a 语言参考手册的翻译,以供学习者参考。

翻译内容有所选择,但绝不会缺少任何一节,因此如有不解之处可以参考相应原文: SystemVerilog LRM 3.1a

15.1 介绍

一个 clocking 块将“同步于某个特定时钟的信号”集合在一起,让它们的时序清晰可见。

clocking 块是 cycle-based methology 中非常重要的用法,让使用者能够在一个更高的抽象层次上编写 testbench,使 test 可以以 cycle & transaction 为单位编写,不需要再精确到信号的具体时间点。一个 testbench 中可以有多个 clocking 块,每一个都有它自己的一个 clock 再加上任意多个信号。

15.2 Clocking 块声明

Clocking 块的语法如下。只需重点关注 clocking_event, clocking_skew 的含义,结合后面一段示例代码可以方便理解。

clocking_skew 表示“一个信号被采样/被驱动的时间点”距离当前的“时钟事件”有多少个单位时间。输入信号的 clocking_skew(称 input skew)表示输入信号被采样的时间点在“时钟事件”之前多少时间;输出信号的 clocking_skew(称 output skew)表示输出信号被驱动的时间点在“时钟事件”之后多少时间。默认的 input skew 为 1step,默认的 output skew 为 0。 step 是一个特殊的时间单位,不是一种物理时间单位,在 Section18.3 中有具体定义。

如果“时钟事件”不是一个数字,而是指定了一个边沿,那么 clocking_skew 也可以指定为信号的边沿。

点击查看代码
clocking ck1 @(posedge clk);  
    default input #1step output negedge; // legal 
    // outputs driven on the negedge clk
    input ... ;
    output ... ;
endclocking
 
clocking ck2 @(clk); // no edge specified!
    default input #1step output negedge; // legal
    input ... ;
    output ... ;
endclocking 

接下来看下一段写法。= top.mem1.enable指定 enable 信号是来自更高层模块 top.mem1 的信号。output negedge ack;覆盖了 default 中的要求,让 ack 信号被驱动的时间点在时钟下降沿。input #1step addr;覆盖了 default 中的要求,让 addr 信号被采样的时间点在 clock1 上升沿之前一个 step。

点击查看代码
clocking bus @(posedge clock1);
    default input #10ns output #2ns; 
    input data, ready, enable = top.mem1.enable;
    output negedge ack;
    input #1step addr;
endclocking

15.3 Input and output skews

如果“时钟事件”是 clock 的上升沿,图示如下:

skew 必须是一个常量表达式,用参数定义也可以;默认单位是当前作用域的 timescale。

点击查看代码
clocking dram @(clk);
    input #1ps address;
    input #5 output #6 data; //data is an inout signal(?)
endclocking

input skew 为 0 时输入信号在 Observed region 被采样,以防止竞争;output skew 为 0 时输出信号在 NBA(Non Blocking Assignment) region 被非阻塞地赋值。

skew 与过程性延迟语句非常不同,显式地声明 #0 的 skew 不会挂起任何进程,也不会 execute(执行?)或采样 Inactive Region 的值。

15.4 高层次表达式('='的用法)

等号 (=) 可以如前所述用来指定来自更高层模块的信号,也可以用来选择信号的切片或打包一组信号。

clocking mem @(clock);
    input instruction = { opcode, regA, regB[3:1] };
endclocking

15.5 不同 clocking 块中的信号

同一信号可以存在于不同的 clocking 块中。

15.6 clocking 块的作用域和生存时间

一个 clocking 块既是声明也是实例化。一旦声明,clocking 信号就可以通过 clocking 块名和点 (.) 操作符获得。

dom.sig // signal sig in clocking dom

clocking 块只能在 module, interface, program 内部声明,在包含它的 module / interface / program 内部具有静态的作用域和生存时间。

15.7 多个 clocking 块的示例

一个包含两个 clocking 块的 test program。

点击查看代码
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;
    endclocking 
    clocking cd2 @(posedge phi2);
        input #2 output #4ps cmd;
        input enable;
    endclocking 
    initial begin 
        // program begins here
        ...
        // user can access cd1.data , cd2.cmd , etc…
    end 
    assign cmd = enable ? cmd_reg: 'x; 
endprogram
这个 test program 可以在 top 模块中实例化并和 dut (比如cpu,mem) 连接。
点击查看代码
module top;
    logic phi1, phi2;
    wire [8:1] cmd; // cannot be logic (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

15.8 interface 与 clocking 块

用 interface 模块的方式指定 clocking 块可以显著减少连接 testbench 的代码量(比较重写前后的 top 模块),而且 test program 中的 clocking 块中的信号方向与 modport test 中的信号方向是一样的,都是相对于 testbench 而言。这样测试程序可以写在 program 中,它的端口就来自与 clocking 块有关的 interface(initial 块中使用的信号为 cd1.a.data, cd2.b.cmd, …)。
前一段代码可以用 interface 重写如下。

点击查看代码
interface bus_A (input clk);
    logic [15:0] data;
    logic write;
    modport test (input data, output write);
    modport dut (output data, input write);
endinterface 
interface bus_B (input clk);
    logic [8:1] cmd;
    logic enable;
    modport test (input enable);
    modport dut (output enable);
endinterface

program test( bus_A.test a, bus_B.test b );
    clocking cd1 @(posedge a.clk);
        input a.data;
        output a.write;
        inout state = top.cpu.state;
    endclocking 
    clocking cd2 @(posedge b.clk);
        input #2 output #4ps b.cmd;
        input b.enable;
    endclocking 
    initial begin 
        // program begins here
        ...
        // user can access cd1.a.data , cd2.b.cmd , etc…
    end 
endprogram
点击查看代码
module top;
    logic phi1, phi2;
    bus_A a(phi1);
    bus_B b(phi2);
    test main( a, b );
    cpu cpu1( a );
    mem mem1( b );
endmodule `
可以利用 clocking 块的等号(=)减少信号名的长度。这样获得与 clocking 块有关的 interface 信号时只需要写 cd1.data, cd2.cmd, …
点击查看代码
clocking cd1 @(posedge a.clk);
    input data = a.data;
    output write = a.write;
    inout state = top.cpu.state;
endclocking 
clocking cd2 @(posedge b.clk);
    input #2 output #4ps cmd = b.cmd;
    input enable = b.enable;
endclocking

15.9 clocking 块事件

可以通过 clocking 块的名称直接获取它的“时钟事件”,也就是说如有以下 clocking 块,@(dram);等价于 @(posedge phi1);

clocking dram @(posedge phi1);
    inout data;
    output negedge #1 address;
endclocking

15.10 周期延迟:##

## 操作符可以用来执行指定数量的“时钟事件”或时钟周期的延迟。语法如下:

一个周期的长度由默认 clocking 块决定。如果当前 module / interface / program 没有指定默认 clocking 块,编译器会报一个错误。

## 5; // wait 5 cycles (clocking events) using the default clocking 
## (j + 1); // wait j+1 cycles (clocking events) using the default clocking

15.11 默认 clocking 块

在一个 module/interface/program 内部可以指定有且仅有一个 clocking 块作为默认 clocking 块,用来执行所有周期延迟的操作。语法如下:

在声明 clocking 块时指定它为默认:

点击查看代码
program test( input bit clk, input reg [15:0] data ); 
    default clocking bus @(posedge clk);
        inout data;
    endclocking 
    initial begin 
        ## 5;
        if ( bus.data == 10 )
        ## 1;
        else 
        ...
    end 
endprogram
或将已存在的 clocking 块指定为默认,这里的 busA 只在 cpu 模块有效:
点击查看代码
module processor ... 
    clocking busA @(posedge clk1); ... endclocking 
    clocking busB @(negedge clk2); ... endclocking 
    module cpu( interface y ); 
        default clocking busA ;
        initial begin 
        ## 5; // use busA => (posedge clk1)
        ...
        end 
    endmodule 
endmodule

15.13 同步事件

显式的同步通过事件控制操作符(@)完成。这些 进行同步事件控制的信号 所取的值是 在各自所关联的“时钟事件”下被采样的值,称同步值(synchronous values)。一些同步语句的示例如下:

点击查看代码
@(ram_bus.ack_l); //等到 ack_1 信号的下一次变化
@(ram_bus); //等到 clocking 块 ram_bus 的下一个时钟事件
@(negedge dom.sign[a]); //等到 dom.sign[a] 的下降沿,a 在运行时间取值.
@(posedge dom.sig1 or dom.sig2); //等到 dom.sig1 的上升沿或者 dom.sig2 的下一次变化

15.14 同步驱动

clocking 块的输出(outputinout)将值驱动到它们所关联的信号上,驱动的时刻在“时钟事件”加上 output skew 的时刻。同步驱动的语法与赋值语句很像:


clockvar_expression(<= 左边)是被驱动的信号,可以是clocking 块输出的片选,但不能是打包的({})。

cycle_delay 中 ## 后面的表达式(称 event_count)指定语句执行之前必须延迟几个“时钟事件”(即周期),这里的周期是信号所在 clocking 块的周期而不是默认 clocking 块的周期。

bus.data[3:0] <= 4’h5; // 用当前周期的 NBA region 的值驱动
##1 bus.data <= 8’hz;  // 等待 1 (bus) cycle 后驱动
##2; bus.data <= 2;    // 等待 2 default clocking cycles 后驱动
bus.data <= ##2 r;     // 记录 r 的值然后等待 2 (bus) cycles 后驱动
posted @ 2024-10-28 14:10  nancylunan  阅读(2130)  评论(0)    收藏  举报