第13章 其他重要主题

13.1异步FIFO断言

谈到写断言,异步FIFO(与同步FIFO相比)是一个困难的命题。 Read和Write时钟是异步的,这意味着要检查的最重要属性是从写入到读取时钟的数据传输。其他断言是检查fifo_full,fifo_empty等条件。

首先我们介绍一下异步FIFO的设计。 有点复杂,但你不需要了解细节。 接下来我们看到一个我设计了一些断言的测试平台。是的,您可以在设计(RTL)(但不推荐),测试平台(如本例中),systemverilog interface,systemverilog program和文件中拥有断言(这是“bind”发挥作用的时候(这是强烈推荐))。

这个FIFO设计和测试平台可在Springer服务器上使用。

13.1.1 FIFO Design

module asynchronous_fifo (
                          // Outputs
                          fifo_out, full, empty,
                          // Inputs
                          wclk, wclk_reset_n, write_en,
                          rclk, rclk_reset_n, read_en,
                          fifo_in
                          );
   parameter D_WIDTH = 20;
   parameter D_DEPTH = 4;
   parameter A_WIDTH = 2;
   input wclk_reset_n;
   input rclk_reset_n;
   input wclk;
   input rclk;
   input write_en;
   input read_en;
   input [D_WIDTH-1:0] fifo_in;
   output [D_WIDTH-1:0] fifo_out;
   output               full;
   output               empty;
   reg [D_WIDTH-1:0]    reg_mem[0:D_DEPTH-1];
   reg [A_WIDTH:0]      wr_ptr;
   reg [A_WIDTH:0]      wr_ptr_gray;
   reg [A_WIDTH:0]      wr_ptr_gray_rclk_q;
   reg [A_WIDTH:0]      wr_ptr_gray_rclk_q2;
   reg [A_WIDTH:0]      rd_ptr;
   reg [A_WIDTH:0]      rd_ptr_gray;
   reg [A_WIDTH:0]      rd_ptr_gray_wclk_q;
   reg [A_WIDTH:0]      rd_ptr_gray_wclk_q2;
   reg                  full;
   reg                  empty;
   wire [A_WIDTH:0]     nxt_wr_ptr;
   wire [A_WIDTH:0]     nxt_rd_ptr;
   wire [A_WIDTH:0]     nxt_wr_ptr_gray;
   wire [A_WIDTH:0]     nxt_rd_ptr_gray;
   wire [A_WIDTH-1:0]   wr_addr;
   wire [A_WIDTH-1:0]   rd_addr;
   wire                 full_d;
   wire                 empty_d;
   assign wr_addr = wr_ptr[A_WIDTH-1:0];
   assign rd_addr = rd_ptr[A_WIDTH-1:0];
   always @ (posedge wclk) begin
      if (write_en) reg_mem[wr_addr] <= #`FF_DLY fifo_in;
   end

   assign fifo_out = reg_mem[rd_addr];

   always @ (posedge wclk or negedge wclk_reset_n) begin
      if (!wclk_reset_n) begin
         wr_ptr <= #`FF_DLY {A_WIDTH+1{1’b0}};
         wr_ptr_gray <= #`FF_DLY {A_WIDTH+1{1’b0}};
      end else begin
         wr_ptr <= #`FF_DLY nxt_wr_ptr;
         wr_ptr_gray <= #`FF_DLY nxt_wr_ptr_gray;
      end
   end

   assign nxt_wr_ptr = (write_en) ? wr_ptr+1 : wr_ptr;
   assign nxt_wr_ptr_gray = ((nxt_wr_ptr>>1) ^ nxt_wr_ptr);

   always @ (posedge rclk or negedge rclk_reset_n) begin
      if (!rclk_reset_n) begin
         rd_ptr <= #`FF_DLY {A_WIDTH+1{1’b0}};
         rd_ptr_gray <= #`FF_DLY {A_WIDTH+1{1’b0}};
      end else begin
         rd_ptr <= #`FF_DLY nxt_rd_ptr;
         rd_ptr_gray <= #`FF_DLY nxt_rd_ptr_gray;
      end
   end

   assign nxt_rd_ptr = (read_en) ? rd_ptr+1 : rd_ptr;
   assign nxt_rd_ptr_gray = (nxt_rd_ptr>>1) ^ nxt_rd_ptr;
   
   // check full
   always @ (posedge wclk or negedge wclk_reset_n) begin
      if (!wclk_reset_n)
        {rd_ptr_gray_wclk_q2, rd_ptr_gray_wclk_q} <= #`FF_DLY
                                                     {{A_WIDTH+1{1’b0}}, {A_WIDTH+1{1’b0}}};
      else
        {rd_ptr_gray_wclk_q2, rd_ptr_gray_wclk_q} <= #`FF_DLY {rd_ptr_gray_wclk_q, rd_ptr_gray};
   end

   assign full_d = (nxt_wr_ptr_gray == {~rd_ptr_gray_wclk_q2[A_WIDTH: A_WIDTH-1], rd_ptr_gray_wclk_q2[A_WIDTH-2:0]});

   always @ (posedge wclk or negedge wclk_reset_n) begin
      if (!wclk_reset_n)
        full <= #`FF_DLY 1’b0;
      else
        full <= #`FF_DLY full_d;
   end

   // check empty
   always @ (posedge rclk or negedge rclk_reset_n) begin
      if (!rclk_reset_n)
        {wr_ptr_gray_rclk_q2, wr_ptr_gray_rclk_q} <= #`FF_DLY
                                                     {{A_WIDTH+1{1’b0}}, {A_WIDTH+1{1’b0}}};
      else
        {wr_ptr_gray_rclk_q2, wr_ptr_gray_rclk_q} <= #`FF_DLY
                                                     {wr_ptr_gray_rclk_q, wr_ptr_gray};
   end

   assign empty_d = (nxt_rd_ptr_gray == wr_ptr_gray_rclk_q2);

   always @ (posedge rclk or negedge rclk_reset_n)begin
      if (!rclk_reset_n)
        empty <= #`FF_DLY 1’b1;
      else
        empty <= #`FF_DLY empty_d;
   end
endmodule

13.1.2 FIFO TB and Assertions

module test_asynchronous_fifo
  (
   fifo_out, full, empty,
   wclk, wclk_reset_n, write_en,
   rclk, rclk_reset_n, read_en,
   fifo_in
   );
   parameter D_WIDTH = 20;
   parameter D_DEPTH = 4;
   parameter A_WIDTH = 2;
   output wclk_reset_n;
   output rclk_reset_n;
   output wclk;
   output rclk;
   output write_en;
   output read_en;
   output [D_WIDTH-1:0] fifo_in;
   logic                wclk_reset_n;
   logic                rclk_reset_n;
   logic                wclk;
   logic                rclk;
   logic                write_en;
   logic                read_en;
   logic [D_WIDTH-1:0]  fifo_in;
   input [D_WIDTH-1:0]  fifo_out;
   input                full;
   input                empty;
   asynchronous_fifo aff1
     (
      fifo_out, full, empty,
      wclk, wclk_reset_n, write_en,
      rclk, rclk_reset_n, read_en,
      fifo_in
      );
   /*
    下列属性检查,看看如果FIFO是满的,wr_ptr不会改变。 
    这个断言也可以用其他方式编写(例如使用'空'条件)。 请尝试一下,看看结果是否与此断言相符
    */

   property check_full;
      @ (posedge wclk) disable iff (!wclk_rstn)
        (full) |=> @ (posedge wclk) aff1.wr_ptr == $past(aff1.wr_ptr);
   endproperty
   cfull : assert property (check_full) else $display($stime,"%m Check wr_ptr full FAIL");
   cfullc : cover property (check_full) $display($stime,"%m Check wr_ptr full PASS");
   /*
    下列属性检查,看看如果FIFO是空的,rd_ptr不会改变。 
    '空'意味着rd_ptr保持与它在上一个clk的值相同 - 从而保证rd_ptr没有改变。 
    但请注意,我们正在使用!$ isunknown并将它作为表达式传递给$past。 
    为什么? 如果FIFO为空,则过去的rd_ptr可能为'X'。 所以我们确保rd_ptr与过去的值相同,并且它不是未知的。 
    这个断言也可以用其他方式编写(例如使用“full”条件或不使用$ past)。 请尝试一下,看看结果是否与此断言相符。
    */

   property check_empty;
      @ (posedge rclk) disable iff (!rclk_rstn)
        (empty) |=> @ (posedge rclk)
          if (!$isunknown($past(aff1.rd_ptr)))
            (aff1.rd_ptr == $past(aff1.rd_ptr));
   endproperty
   cempty : assert property (check_empty) else $display($stime,"%m Check rd_ptr empty FAIL");
   cemptyc : cover property (check_empty) $display($stime,"%m Check rd_ptr empty PASS");

   /*------------------------------------------
    ASYNCHRONOUS DATA TRANSFER CHECK
    -------------------------------------------*/
   /*
    This is a very important assertion for an asynchronous FIFO. Check that the
    data that is written at a wr_ptr is the same data that is read when rd_ptr reaches that
    wr_ptr. Simple! Let us look at the assertion step by step
    */
   /*
      这是一个非常重要异步FIFO断言。检查在wr_ptr处写入的数据与在rd_ptr达到wr_ptr时读取的数据是否相同。
    简单吗!让我们一步一步看看这个断言。
      在断言中,data_check属性检查以查看FIFO未满。如果是这样,请将wr_ptr保存到局部变量'ptr'中,
    并将来自fifo的数据保存到局部变量'data'中并显示,以便我们可以轻松看到断言在仿真过程中的进展情况。
      如果左侧表达式(|=>)为真,那么右侧表达式说明rd_ptr的第一个匹配与wr_ptr相同(注意wr_ptr存储在本地变量ptr中),
    即读取数据与写入数据相同(注意写入数据存储在局部变量中)。
      序列rd_detect(ptr)用作first_match的表达式。它说,从现在开始直到永远等到你检测到一个读,
    它的rd_ptr等于wr_ptr(它存储在先前的局部变量'ptr'中)。请注意,由于我们有wr_clk和rd_clk,因此它们是多时钟属性。
    所以,在这个属性中,我们看到了
        多时钟属性(wclk和rclk)
        使用局部变量进行存储/比较
        first_match
        将一个子程序(这里是一个$display)附加到表达式以进行有效的调试
    有效地使用##0来创建序列中的重叠条件尝试用不同的方式来写出断言。参考不同的运算符,采样值函数等,看看你是否可以写出等价的断言。
    不仅有一种写断言的方法。但是确实有一条正确的道路和一条错误的道路。你会通过练习来获得。
    */

   sequence rd_detect(ptr);
      ##[0:$] (read_en && !empty && (aff1.rd_ptr == ptr));
   endsequence

   property data_check(wrptr);
      integer           ptr, data;
      @ (posedge wclk) disable iff (!wclk_reset_n || !rclk_reset_n)
        (write_en && !full, ptr=wrptr, data=fifo_in, $display($stime,"\t Assertion Disp wr_ptr=%h data=%h",aff1.wr_ptr,fifo_in))
        |=> @ (negedge rclk) first_match(rd_detect(ptr), $display($stime," Assertion Disp FIRST_MATCH ptr=%h Compare data=%h fifo_out=%h", ptr, data, fifo_out)) ##0 (fifo_out === data);
   endproperty
   dcheck : assert property (data_check(aff1.wr_ptr)) else $display($stime,"FAIL: DATA CHECK");
   dcheckc : cover property (data_check(aff1.wr_ptr)) $display($stime,"PASS: DATA CHECK");

   /*------------------------------------------
    If FULL -> NOT EMPTY Check
    -------------------------------------------*/
   /*
   以下属性是不言自明的。但请注意,这也是一个多时钟属性,由于写入和读取时钟是不同的时钟,所以我们必须使用不重叠的运算符| =>。
   这是一个互斥属性。你可以用什么不同的方式写这个断言?
   */

   property full_empty;
      @ (posedge wclk) disable iff (!wclk_reset_n)
        @ (posedge wclk) (full) |=> @ (posedge rclk) (!empty);
   endproperty
   few :assert property (full_empty) else $display($stime,"FAIL: Full and Empty BOTH asserted");
   cfew : cover property (full_empty) $display($stime,"PASS: Full and Empty check");

   /*------------------------------------------
    If EMPTY -> NOT FULL Check
    -------------------------------------------*/
   /*
   以下属性是不言自明的。但请注意,这也是一个多时钟属性,由于写入和读取时钟是不同的时钟,所以我们必须使用不重叠的运算符| =>。       
   这是一个互斥属性。你可以用什么不同的方式写这个断言?
   */
   property empty_full;
      @ (posedge wclk) disable iff (!wclk_reset_n)
        @ (posedge rclk) (empty) |=> @ (posedge wclk) (!full);
   endproperty

   efw : assert property (full_empty) else $display($stime, "FAIL: Full and Empty BOTH asserted");
   /*------------------------------------------
    rclk_reset_n Check on rclk
    -------------------------------------------*/
   /*
   以下属性检查在复位FIFO时,空指针是否为高(即空)
   */

   property reset_n_rclk;
      @ (posedge rclk) !rclk_reset_n |-> empty;
   endproperty

   reset_nrclkA: assert property (reset_n_rclk) else $display($stime,"FAIL: FIFO not empty during rclk_reset_n");
   reset_nrclkC: cover property (reset_n_rclk) $display($stime,"PASS: FIFO empty during rclk_reset_n");

   /*------------------------------------------
    wclk_reset_n Check on wclk
    -------------------------------------------*/
   /*
   以下属性检查,在复位FIFO时,FIFO未满。 FIFO只能在复位期间变为空,而不是满。
   */

   property reset_n_wclk;
      @ (posedge wclk) !wclk_reset_n |-> !full;
   endproperty
   reset_nwclkA: assert property (reset_n_wclk) else $display($stime, "FAIL: FIFo FULL during wclk_reset_n");
   reset_nwclkC: cover property (reset_n_wclk) $display($stime,"PASS: FIFO FULL during rclk_wstn");

   /*
    以下断言很重要。我们正在检查我们自己的测试平台!是的,这很重要。    
    这是一个简单的测试平台,但想法是,随着您的测试平台开发复杂的代码,您很可能会犯错误。那么为什么不使用断言来捕获这些错误。
    例如,在第一个断言中,我们检查如果FIFO已满,我们不会继续写入,因为这个特定的FIFO没有回送信号。    
    同样,第二个断言检查是否看到FIFO是空的,我们不会继续读FIFO!
   */
   
   /*-----------------------------------------
   Checks for the Test-bench
   -------------------------------------------*/
   property check_full_write_en;
      @ (posedge wclk) disable iff (!wclk_reset_n)
        full |-> !write_en;
   endproperty
   check_full_write_enA : assert property (check_full_write_en) else $display($stime,"%m FAIL: check_full_write_en");

   check_full_write_enC : cover property (check_full_write_en) $display($stime,"%m PASS: check_full_write_en");
   property check_empty_read_en;
      @ (posedge rclk) disable iff (!rclk_reset_n)
        empty |-> !read_en;
   endproperty
   check_empty_read_enA: assert property (check_full_write_en) else $display($stime,"FAIL: %m check_full_write_en");
   check_empty_read_enC: cover property (check_full_write_en) $display($stime,"PASS: %m check_full_write_en");
   integer              i, seed1, wclk_width, rclk_width, loopcount, base;
   /*
   以下是您熟悉的常规Verilog测试平台代码
   */
   initial begin
      loopcount = 50;
      seed1 = 12345;
      wclk = 1’b1; write_en=1’b1;
      rclk = 1’b0; read_en=1;
      fork
         begin wclk_reset_n = 1’b0; #100; wclk_reset_n = 1’b1; write_en=1’b1; end
         begin rclk_reset_n = 1’b0; #100; rclk_reset_n = 1’b1; read_en=1’b0; end
         for (i=0; i<loopcount; i++) begin
            //rclk_width = ({$random} % 40) + 5;
            //wclk_width = ({$random} % 40) + 5;
            rclk_width = 40; wclk_width = 40;
            //rclk_width = 10; wclk_width = 40;
            //rclk_width = 40; wclk_width = 10;

            $display($stime,"wclk_width=%0d rclk_width=%0d",wclk_width,rclk_width);
            #1000#i;
         end
      join
      $finish(2);
   end
   always #rclk_width rclk=!rclk;
   always #wclk_width wclk=!wclk;
   always @ (wclk,rclk,write_en,read_en,full,empty,aff1.wr_ptr,aff1.rd_ptr,fifo_in,fifo_out) begin
      $strobe($stime,"\t\twclk=%b rclk=%b write_en=%b read_en=%b full=%b empty=%b wr_ptr=%d rd_ptr=%d fifo_in=%h fifo_out=%h", 
              wclk,rclk,write_en,read_en,full,empty,aff1.wr_ptr,aff1.rd_ptr,fifo_in,fifo_out);
   end
   always @ (negedge wclk) begin
      if (rclk_reset_n) begin
         if (!full) begin
            write_en=1’b1;
            fifo_in = $random({seed1});
         end
         else
           write_en=1’b0;
      end
   end
   always @ (posedge rclk) begin
      #2;
      if (rclk_reset_n) begin
         if (!empty) begin
            read_en=1’b1;
            //$strobe($stime,"READ: Pointer=%h data=%h",aff1.rd_ptr,fifo_out);
         end
         else
           read_en=1’b0;
      end
   end
   always @ (wclk_reset_n) $strobe($stime,"\twclk_reset_n=%b",wclk_reset_n);
   always @ (rclk_reset_n) $strobe($stime,"\trclk_reset_n=%b",rclk_reset_n);
endmodule

13.2 在程序代码中嵌入并发断言


                                        图13.1 在程序代码中嵌入并发断言

是的,你确实可以从程序块中断言一个属性(即并发属性)。请注意,属性和序列本身是在程序块之外声明的。

那么立即声明和在程序块中嵌入并发断言之间有什么不同?立即断言是'断言',而不是'断言属性',它是非时间域'断言'。相反,嵌入在过程块中的并发断言是通常的并发断言“断言属性”(即它可以是时域的)。换句话说,程序代码中嵌入的即时断言将在零时间内完成,而并发断言可能会或可能不会在零时间内完成。但是程序代码中的并发断言是阻塞还是非阻塞?坚持这个想法一段时间。

重申,嵌入程序代码的直接声明必须在零时间内完成。嵌入在程序代码中的并发断言可能会或可能不会在零时间内完成。

图13.1指出,你想触发一个断言的'条件'已经在行为上建模了,并且不需要在一个断言中(作为先行词)被复制。该示例显示了两种断言属性的方式。 'ifdef P显示了我们在本书中看到的属性的常规方式。 'else显示从always块断言的相同属性。但是请注意,在程序块中,断言前面是'if(bState == CycleStart)'条件。换句话说,可能已经用行为代码中的条件调节断言。如果该属性在程序块之外“被断言”(正如我们在本书中所做的那样),您将不得不在程序块中复制该属性中的先行条件。

让我们把注意力转回到嵌入式并发断言是阻塞的还是非阻塞的。当你从程序代码中发出并发断言并且它没有在零时间内完成时会发生什么?并发断言之后的过程代码会发生什么?它会停止,直到并发断言完成(阻塞)?或者,以下代码是否会继续与并发断言(非阻塞)并行执行?这就是图13.2所解释的。


                           图13.2 程序代码中的并发断言是非阻塞的

有两个属性'pr1'和'pr2'。他们都“消耗”时间,即他们促使时间往前走。程序块'always @(popsedge clk)一个接一个地断言这两个属性,两者之间没有任何时间间隔。这段代码将如何执行?程序代码将遇到'assert property(pr1 ..)'并将其解除。 'pr1'将通过查找cstart来开始评估,并随后跟进。换句话说,'pr1'正在等待在时间域发生的事情。但是启动它的程序代码不会等待'pr1'完成。它将转向下一个声明,即“声明属性(pr2 ..)”并将其解除。所以,现在你已经有了正在执行的'pr1',并且'pr2'只是被并行执行并且程序代码移动到其他依次执行的代码。

简而言之,程序代码中的并发断言是非阻塞的。

如仿真日志中所示,在时间10,(@ posedge clk),我们触发'pr1'。同时(因为下一个语句是'assert property(pr2 ...)'),我们激发'pr2'。在时刻30,'cstart == 1'和'aStrob == 0'。这意味着'pr1'和'pr2'的先行条件已经满足。在时刻50处,'Wenb == 0'完成属性'pr2',并且该属性在模拟日志中如时刻50所示那样通过。因此,您首先注意到的是,即使'pr1'先被触发,'pr2'依然先完成。换句话说,因为这两个属性都是非阻塞的并且在它们自己的并行线程上执行,所以它们之间或'pr1','pr2'和过程代码之间没有时间关系。遵循同样的思路,看看为什么'pr1'和'pr2'在时刻90同时通过。


                        图13.3 在程序代码中嵌入并发断言 - 进一步的细微差别

请参阅程序块中推断断言的时钟边沿的规则。另外,图13.3中还记录了语义的其他细微差别。总之,嵌入式断言的时钟推断来自'always'块边缘灵敏度。它不是从程序代码中嵌入的任何其他时域条件(边沿或电平)推导出的。

13.3 调用子程序

 将子程序附加到表达式是一个很好的功能,对调试工作和其他应用程序非常有用。例如,如果您想知道何时以复杂的顺序执行表达式(这只是一个例子),您可以将Verilog任务“附加”到表达式并打印您感兴趣的条件。 13.4解释了这种情况。

如图所示,可以将子例程附加到表达式,序列或序列中的表达式或子序列。请注意,附加的子程序将在它所附加的表达式或序列完成时执行。例如,图13.4最上面的例子中的(not(cde,tdisp1))表示当'cde'达到其真实结论时'tdisp1'将被执行。否则,它不会执行。

图13.5进一步解释了子程序何时执行。首先,在图的顶部,您会注意到我们'附加'了一个局部变量以及一个子例程($ display)。由于$ rose(ptrue)是子程序附加的序列,所以只有当$ rose(ptrue)为真时才会执行。同样,该图的下一部分显示(pout ==(local_data?5),$ display(...)),其中(pout==(local_data?5))是$ display子例程所附加的表达式。同样,只有当(pout ==(local_data?5))为真时,子程序$ display才会执行。


                                                 图13.4调用子程序

 您还会注意到,该图中的$display(大部分)显示局部变量。这是$ display作为子程序的重要用途之一,因为局部变量不能从动作块(通过或失败)访问。

 

                                                        图13.5 调用子程序 - 进一步的细微差别

 在图13.6中,子程序是一个Verilog的'task'-lvar_seq_trigger,它又包含一个$display。但是我不希望你逃避这个想法,即你可以附加的唯一子程序就是$display某个东西的那个子程序!您可以使用子例程(例如)收集覆盖范围信息(使用覆盖组和覆盖点)。当我们讨论功能覆盖时,我们会看到这个例子。由于附加的子程序可以是任务,因此可以考虑许多可能的应用程序。

 

                                    图13.6调用子程序和局部变量 - 应用程序

 将“任务”附加到表达式后的想法是,除了不能访问调用“任务”的序列的局部变量外,您可以执行Verilog允许您在“任务”中执行的任何操作。但你确实可以将一个局部变量作为参数传递给'task',如图13.6所示。

'序列lvar_seq'有一个局部变量'local_data'。这个局部变量作为实际参数传递给'lvar_seq_trigger'。 '任务lvar_seq_trigger'反过来使用它作为输入'ldata'并显示它。这是一种将局部变量传递给附加子程序的方法。

但请注意,您不能从附加的子例程(例如,任务lvar_seq_trigger)分层访问变量(本地或非本地变量)。这在图中显示为ERROR。在这里,我们试图通过使用'lvar_seq.pin'来分层访问'sequence lvar_seq'中的变量'pin'。

请注意,您调用子程序的断言不会等待子程序的完成或从子程序中取返回值。

最后,传递给子例程的参数必须按值或引用传递('ref'或'const ref')。

 13.4 序列作为形式参数

 

                                              图13.7 作为形式参数的序列

 SystemVerilog断言确实很强大,从这个特性中可以看出。您可以将整个序列作为实际参数传递给属性或其他序列。一个显而易见的优点是您可以将不同属性中的序列重用为属性的形式参数。其中一个例子是如图13.7所示的重置序列。重置序列通常用作不同属性的先行词。写一次,并将其作为实际参数传递给不同的属性。这就是具有可观察性和可调试性的可重用性。传递给属性的序列(显然)可以用于左侧或者右侧表达式(|->, |=>)。

 13.5 序列作为前项

 由于序列可以作为实际参数传递,因此有很多优点。我们在图13.7中看到了一个。这是另一个。在这里,我们定义一个简单的序列'seq'并将其传递给属性's_rc1'。在此属性中,我们使用'seq'(即属性中的c_seq)作为前项(图13.8)。


                                                 图13.8 序列作为先行词

与任何先行词一样,该属性将等待前项为真,然后执行结果。现在,对于许多运算符(例如'throughout'),我们发现运算符的LHS和RHS对断言失败负有同样的责任。如果运算符的任何一方未能通过,那么序列/属性失败。这里需要注意的重要一点是,在'throughout'的情况下,运算符被用于后项而不是前项。任何因此而失败的事件都会导致属性失败。在这里,一个序列被用作'前项'的意义是,即使前面的序列失败,该属性不会失败。相反,该属性只是等待序列'c_seq'最终成为真,之后才会执行结果。这是有道理的,因为只有当前项被抽样为真时,才会被评估。

故事的最后一点是,无论你在前项表达式中有什么,它都不会导致失败。前项表达式的工作是评估其表达式/序列,并将其抽样为真,然后评估后项表达式。

 13.6 敏感列表中的序列


                                         图13.9 程序块敏感列表中的序列

 让我们进一步使用'序列'。你猜怎么着?您可以在敏感信号列表中使用序列进行事件控制,也可以在初始块中将其用作显式边沿敏感控制事件。您可以非常有效地使用此功能,因为在SVA中设计时域条件比使用行为Verilog容易得多。您可以将某些条件设计为一个序列,然后在行为Verilog代码中使用它,如图13.9和13.10所示 。

图13.9显示'always'块等待序列'sr1'完成并简单显示其PASS结果。你能弄清楚为什么没有FAIL报告?当'gnt'在2个钟后没有跟随'req'时,‘always @ (sr1)’会被触发吗?请尝试看看会发生什么。

图13.10中的序列说明@(posedge clk)如果$ rose(read)被采样为高电平(边沿),应该至少有一个readC(读完成)。 'initial'块等待这个序列完成(使用@(ReadComplete),然后发出下一个读取。

正如你所看到的,这个特性在设计你的SystemVerilog验证平台代码时,使用序列功能非常强大。


                                         图13.10“敏感”列表中的序列

 13.7 建立一个计数器


                                             图13.11 使用局部变量构建计数器

 好的,对一个序列这足够了(至少在一段时间内)。让我们看看我们如何有效地使用局部变量和连续重复操作符来构建一些东西!

让我们建立一个计数器。为什么?有很多应用程序可以使用这个例子。例如,您希望确保网络上的传入数据包在有效负载达到最大阈值(计数)时生成中断。

属性checkCounter声明名为“LCount”的本​​地“int”。它等待'startCount'的上升沿,在这个上升沿,它将'initCount'存储到'LCount'中(initCount在程序代码中的其他地方定义)。

然后它等待1个时钟并将LCount增加1,并在每个时钟继续这样做,直到LCount达到maxCount。连续重复运算符[* 0:$]进行计数。换句话说,'LCount = LCount+1“在每个posedge clk重复,直到达到”LCount == maxCount“。一旦达到maxCount,前项表达式就意味着在同一时钟(重叠的含义)intr被断言。

快速注意:正如您已经注意到的,在本书中,我没有给出大型应用程序,然后描述一套有限的SVA功能,而是选择用简单的应用程序来描述每个运算符,以便您清楚地理解运算符的工作原理,并用运算符的特性来设计您的断言。

 13.8 时钟延迟:如果您想要可变的时钟延迟?

 

                                            图13.12 变量延迟-问题陈述

 图13.11构建了一个可用于创建可变延迟模型的计数器。请注意,SVA只允许其延迟运算符具有恒定的固定延迟。所以,图 13.12和13.13的例子提供了一个解决这个限制的策略。

图13.12描述了一个典型的规范。我们需要根据读取队列中'read'的位置来检查可变延迟。换句话说,如果'read'在队列的末尾,'read'将以最大延迟完成。相反,如果它在队列的开始处,它将以最小延迟完成。或者在中间的任何地方都有一个延迟。

由于延迟(或延迟范围)运算符不允许变量的延迟,因此如何用一个通用断言对此进行建模?您不想为每个固定延迟创建单独的断言。这是问题陈述。现在让我们看看如何解决这个问题。考虑这个例子作为一个想法生成器。

 

                                               图13.13 可变延迟-解决方案

 图13.13中的概念与建立计算器相同。这里不是递增局部变量,而是递减它直到它达到零。在这个应用程序中,readLatency是在你的程序代码中定义的,它根据Read队列中Read的位置而改变。

当这个属性的read_latecncy_check被断言时,它会在assertion($ fell(rd_))时将readLatency赋值给Ldelay,并在每个posedge clk中对其进行递减,直到它达到0。

(1, Ldelay = Ldelay-1)[*0:$] ##1 (Ldelay==0)

 那么在(1,Ldelay = Ldelay-1)中为什么需要'1'?回想一下,当赋值语句附加到表达式时,我们可以分配给一个局部变量。由于我们没有任何明确的表达式,所以我们只需使用'always true'作为表达式。

您可以继续将readLatency从基于读取队列中'read'位置的过程代码更改,并使用相同的属性来检查读取队列中读取的不同延迟。

这个简单的例子虽然简单,但是功能强大。

 13.9 如果'动作块'是阻塞的?

我们已经看到,属性的断言('assert property')允许你由两个“动作”块。一个在属性通过时触发,另一个在失败时触发。

此动作块可以包含SystemVerilog支持的任何过程代码。程序块可以具有时域“延迟”(例如@(posedge cc)或“wait sig”等)。那是当你需要小心衡量后果的时候。如果在这个模块没有“延迟”,就很简单。''assert property''没有任何延迟地触发块; 该块在0时间内执行; 返回并且该属性随其执行一起移动。但是如果动作块有延迟,这里会发生什么。

图13.14左上方的属性表示'''assert property(pr1)else failtask''。如果该属性失败,则调用一个名为“failtask”的任务。 'failtask'等待4 @(posedge clk)并从任务中返回。 4时钟延迟仅是时间延迟的一个例子。


                                                   图13.14阻塞动作块

现在让我们看看仿真日志。在时刻30,req= 1,所以属性pr1移动。在时刻70,gnt = 0,这是一个失败条件,因为当时gnt应该是'1'。由于失败,在70时刻(失败时)调用'failtask'。 'failtask'等待4个posedge clks,它们在时间90,110,130,150处显示在日志文件中。当'failtask'等待其时钟完成时,在110时刻,req再次变高。因此,该属性开始执行,并期望'gnt'高在时刻150. 再次gnt是'0',所以属性应该失败。但它不!为什么?因为在150时刻,'failtask'仍然完成了第四个时钟的等待。由于第四个时钟还没有结束,那么在150时间内出现'gnt'失败时,失败就会被抑制,因为第一次调用'failtask'还没有结束。

问题是,如果你调用一个程序块没有在'0'时间内完成,并且如果属性前提的下一个触发引起另一个通过/失败,与通过/失败关联的动作块的下一个触发就不会发生。所以,在你的动作块中使用时间要小心。

图13.15仿真日志突出了同一点。在动作块中有一个消耗时间的例子,另一个例子没有。如图13.15所示,LHS和RHS的属性是相同的,除了LHS动作块调用消耗时间(4个时钟)的'failtask'。另一方面,RHS调用'failtask',但它不会消耗任何时间。

两个仿真日志中的'req'条件也相同。但是RHS显示两个'FROM failtask - 0'的调用,而LHS日志只显示一个调用(如上面图13.14所述),因为动作块是'阻塞'的,并且不允许重新进入已经执行的动作块。


                                                         图13.15阻塞与非阻塞动作块

13.9 属性​​中的多重蕴涵运算符。要特别小心…

 你可以在一个属性有多重蕴涵吗?当然可以。但是,您需要非常认真地理解属性中多重蕴涵(听起来很有趣)的含义。让我们看一个例子并理解它是如何工作的。

 

                                              图13.16属性中的多重蕴涵

在图13.16中,属性mclocks(乍一看)看起来非常温和。但请密切关注,你会看到两个含义。 @(posedge clk)如果'a'为真,暗示'bSeq'## 1 c,这意味着'dSeq'。一个前项表达式的结果用作另一个后项表达式的前项表达式。

现在,让我们看看放在日志。在时刻175,a = 1,所以属性开始评估并暗示bSeq ## 1 c。在时刻185'bSeq'匹配,所以现在属性寻找## 1 c。在时刻195,c不等于'1',但属性不失败。哇!原因?请注意,'bSeq ## 1 c'现在是'dSeq'的前项,我们知道前面没有匹配,结果不会被评估,属性也不会失败。即使'bSeq## 1'是一个后项,这似乎也适用,但它也是一个前项表达式。语言异常?并非如此,但这些属性的行为并不直观。由于'bSeq ## 1 c'不匹配,整个属性被丢弃,并且属性再次等待'a== 1'重新开始。

混淆?嗯,是的。因此,请不要使用这种多重蕴涵的属性,除非你确定这是你想要的。我见过工程师使用它,因为逻辑看起来很直观,但行为不是。

 13.11序列中的子序列

 一个序列可以嵌入另一个序列中。嵌入的序列可以被称为子序列。图13.17显示序列'abc'被嵌入序列'abcRule'中。如果子序列没有自己的显式时钟,则嵌入的子序列从父序列推断时钟。

 

                                            图13.17序列中的子序列-时钟推断

 另外,如图13.18所示,一个序列既可以用作前项表达式,也可以用作后项表达式。如图所示,前项是序列's1',这意味着序列's2'是随之而来的。在这里,每个序列都有自己的显式时钟。然而,如果情况并非如此,那么子序列会从属性s1tos2继承(推断)时钟。

指出这种用法的原因是为了再次强调,最好将属性分解为更小的序列,然后建立更大的整体属性。序列越小,调试性和可控性越好。

                                                      图13.18序列中的子序列

13.12 循环依赖


                                                 图13.19循环依赖

 如图13.19所示,确实可以在属性之间存在循环依赖关系,但不在序列之间。但请注意,属性之间的循环依赖关系只是属性的后项,而不是前项。

此功能的用途是什么?如果要检查状态机的两种状态之间的连续切换,可以使用此属性。此示例中显示的属性将永远不会完成,直到仿真结束。

还要注意,你不能做像''c |=>d ## 1 e ## 1 p1''这样的东西。不能将另一个属性用作循环依赖的子序列。 你会得到以下错误。
**Error:massert.v(42):'##'表达式的RHS中有非法SVA属性值。

 13.13 精化主题...

 

                                                图13.20 对主题进行细化

 13.14 仿真性能效率


                                                     图13.21仿真性能效率

 在图13.21中,顶部的属性rdyProtocol说如果rdy是真的,那么你必须得到一个rdyAck。我们设计了使用恒定延迟范围。没有什么不对,但是(从仿真结果中可以看出),基于无限范围的设计比不使用这种范围的设计运行得慢。这并不意味着禁止使用##[1:$],但如果你能找到更好的方法来解决这个问题,你将会获得更好的仿真效率。 图13.21的底部显示了替代方式。 它使用'goto'运算符,它模拟相同的行为,即在rdy之后至少会有1个rdyAck。

 13.15这是一个空虚的世界!咦?

 本节可能已经在书中提前提到很多了,但我不希望读者从困惑中解脱出来。一旦你通过这个例子,你会明白为什么'蕴涵'操作符在断言中几乎是必须的。为便于理解,下面的示例图有详细的注释。因此他们没有很多文字。开始了。

 13.15.1并发断言 - 没有蕴涵

 让我们来看图13.22。

在属性pr1中没有蕴涵运算符。如图13.22所示,属性'pr1'解释为‘在clk的上升沿req应该为真,2个时钟后gnt应该为真’。请注意,我们没有在属性中使用蕴涵运算符。因此,请仔细阅读属性。它并没有说如果req是真的,该属性应该检查gnt。它只是说''req'在posedge clk上是真实的,后面的两个clks是gnt是正确的。因此,REQ的每个时钟都不是真的,属性FAIL。那是我们真正想要的吗?我不这么认为。这就是蕴涵运算符进入我们视线的地方。

更重要的是,你注意到在90时刻,属性PASSes以及FAILs!惊人!该属性通过,因为在时刻50,req = 1,因此该属性在90处寻找gnt = 1。它在90处发现gnt =1,所以它通过。但是因为90的req = 0,它也失败了。再次令人震惊!

我的建议是,除非你完全确定你在做什么,否则不要使用没有蕴涵的属性。继续往下读。


                                            图13.22无蕴含运算符的断言

 13.15.2并发断言 - 带有蕴涵

 好吧,所以我们决定添加一个蕴涵运算符,如‘@(posedge clk)req |-> ## 2 gnt; 这样我们就不会得到错误的失败。可是等等!仔细查看图13.23中的模拟日志。现在,只要req = 0,断言就会通过。这是怎么回事?

一切似乎都OK。如果前项表达式不是真的,那么后项不会被触发。但是,如果前项表达式不是真的,属性PASS,动作块会触发并告诉我们属性PASSes。请继续阅读。


                                                              图13.23 断言导致空PASS

 13.15.3 空PASS。什么?

我们在前项表达式失败但属性获得PASS的原因是,根据LRM''如果前面的sequence_expr没有匹配,那么对蕴涵的评估就会空洞地成功并返回true'。因此,只要你看到'req'低,你会得到一个'空'的PASS,触发PASS动作块,我们得到PASS显示。

好的,那么解决方案是什么?为什么我们没有看到这种行为,直到现在我们已经经历了所有的例子?继续阅读...

13.15.4 并发断言 - 带'cover'

 有两种方法可以克服我们不想要的这种行为。一种是简单地忽略属性'assert'中的PASS动作块,即,根本没有PASS动作块。那样如果有空的PASS,我们的日志不会被误导性的PASS消息所混淆。请注意,忽略所谓的空PASS是无害的。这是显而易见的解决方案。但是如果你想知道什么时候PASSes?这就是'cover'进入我们视线的地方。

 

                                       图13.24带“cover”断言,为了提示真PASS

 'cover'的解决方案让我们看到该属性是否确实被覆盖(即被执行)。 'cover'不具有空PASS属性。它在断言结束时表示它是否被覆盖。当它被覆盖时,它会触发一个PASS操作块。在此操作块中,您可以放置​​一个$display语句来指示该属性已被覆盖或已通过。

请注意,'cover'没有FAIL动作块,并且没有空PASS属性。

通过这种方式,使用'assert'和'cover',我们有一种方法来编写一个断言,给出我们所需的FAIL和PASS指示,而没有任何其他消息。请仔细阅读图13.24中的仿真日志以查看属性的行为。还要注意在$display任务中使用%m。这个很好的旧Verilog功能显示了断言的整个路径。这是在两个单独的作用域中区分具有相同名称的两个属性的一种方法。

 13.16 空序列

在图13.25中,我们使用连续运算符'*',但使用'0'重复[* 0]。换句话说,我们说'b'不应该重复。换句话说,即使它是属性的一部分,这意味着'b'根本不存在(空)。因此,''b [* 0] ## 1!a''仅仅意味着检查空序列'b',1个时钟后检查'!a'。因为,空序列并不意味着什么,所以我们基本上在$ rose(a)1个时钟之后检查'!a'。


                                                         图13.25 空匹配[* 0]

下面的例子是为了完整而给出的。将它们视为参考材料。

图13.26确实很有趣。 LRM提供了关于如何解释(seq ##n empty)和(empty ##n seq)两个规则的规则,两个规则都是n 0>。解释在图13.26的顶部注明。为了清楚说明,让我们来看看这个例子。属性'ab'表示如果前项表达式'z'为真,则后续序列'sc1'应该执行。序列'sc1'表示当'z'为真时'a'为真;那么(根据LRM规则(seq ## n empty == seq ##(n-1)'true),该序列可以被解读为'a'为真;然后'b'可能不是真的(即空 - 不存在)或者将继续重复,直到c == 1。

让我们看看仿真日志,看看新的定义是否成立。


                                                    图13.26 空匹配-示例

 在时刻30,z = 1,所以属性同时寻找'a'=1。 'a'= 1,'b'也等于'1',这并不重要,因为'b'可以具有零匹配,只要在最后'b'后1个时钟处有c == 1 '或'a'。在我们的情况下,从30开始,我们确实有c == 1在50,这是'a == 1'以及'b == 1'之后的一个时钟。因此,属性通过。

在时刻70,z = 1,a = 1但b = 0。这是空的匹配。因此,该属性在最后一个'a'之后查找c == 1。它发现,在时刻90,属性通过。

 在时刻110处,z = 1,a = 1,b = 0。下一个时钟在130,a =0,c也等于'0' - 但'b'= 1。正如我们前面看到的,'b'可能不匹配,或者可能永远持续匹配直到c == 1。由于在时刻130处c ==0,属性继续查找b == 1直到c == 1。发生在时刻150(b == 1)和190(c == 1)并且属性通过。

在时刻250,z = 1,a = 1和b = 1。下一个时钟在270,c仍然是零,所以属性继续看到b保持'1'。在时刻290,b == 1,所以我们继续前进。但是在时刻310,'b'没有保持断言,'c'也不等于'1'。 'c'应该是'1'(以满b[*0:$] ##1 c)。都不会发生,属性失败。

图13.27中的例子可以有效地用于当你想检查是否发生了某个序列而另一个永远不会发生的情况。您也可以使用非零连续重复操作符来完成此操作,但[* 0]或[= 0]使它更容易。例如,在以下示例中,

@ (posedge clk) a |=> b[=0];

 意味着在'a'是真的,一个时钟后,'b'永远不会发生。换句话说,'b'应该永远被否定(零)。阅读/解读这个属性非常简单。行为显示在图13.27中的仿真日志中。

 那么,你会如何写这个属性呢?

@ (posedge clk) a |=> !b[*1:$];

 同样的意思。一旦'a'是真的,从下一钟开始'b'应该永远保持'!b'连续。

 

                                                    图13.27空匹配示例-II

 以下纯粹是参考资料。把它放在你的后兜里。在下雨天它会有用的!(图13.28)。


                                             图13.28 空序列-更多规则

猜你喜欢

转载自blog.csdn.net/zhajio/article/details/80229405