SV 第四章 连接设计和测试平台

发布时间 2023-08-06 00:56:00作者: MinxJ

SystemVerilog验证

4 连接设计和测试平台

验证需要几个步骤,生成输入激励,捕获输出响应,决定对错和衡量进度。首先需要将一个合适的测试平台连接到设计上。测试平台包裹着设计(DUT,Design Under Test),发送激励并捕获设计的输出。

4.1 将测试平台和设计分开

理想的开发过程中设计和验证是分开的,真实的开发过程中,有限预算的情况下,可能要求你同时干两份活。

之前了解到,在SV中改进了reg,新名字为logic,logic可以像wire一样用来连接块,但是不能由多个端口驱动,如果需要多个端口来驱动,就需要使用wire。

4.2 接口

逻辑设计变得越来越复杂,所以不同块之间的通信也很复杂,需要分割成独立的实体,SV中使用接口来连接块与块之间的通信,接口可以看作一捆只能的连线,包含了连接、同步、升值两个或者更多块之间的通信功能,连接了设计和测试平台。

接口相当将连接和通信两者独立出来,单独作为一个块,其中的特性还有很多,在这里举一个简单的例子。

Testbench <<Interface>> Arbiter

首先是接口定义:

interface arb_if (input bit clk);
    logic [1:0] grant, request;
    logic rst;
endinterface

然后是优先级仲裁器:

module arbiter (arb_if arbif);
    always @(posedge arbif.clk or posedge arbif.rst) begin
        if (arbif.rst)
            arbif.grant <= 2'd00;
        else if (arbif.request >= 2'b10)
            arbif.grant <= 2'b10;
        else
            arbif.grant <= arbif.request;
    end
endmodule

然后是Test文件编写:

module test (arb_if arbif);
    initial begin
        #10 arbif.rst = 1'b1;
        #10 arbif.rst = 1'b0;

        #10 arbif.request = 2'b00;
        #10 $display("Request is : %b , Grant is : %b", arbif.request, arbif.grant);

        #10 arbif.request = 2'b01;
        #10 $display("Request is : %b , Grant is : %b", arbif.request, arbif.grant);

        #10 arbif.request = 2'b10;
        #10 $display("Request is : %b , Grant is : %b", arbif.request, arbif.grant);
        
        #10 arbif.request = 2'b11;
        #10 $display("Request is : %b , Grant is : %b", arbif.request, arbif.grant);
        
        $finish;
    end
endmodule

最后是顶层文件:

module top;

    bit clk;
    always #5 clk = ~clk;

    arb_if  arbif(clk);
    arbiter abt(arbif);
    test    tl(arbif);

endmodule : top

以下是仿真结果:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

Request is : 00 , Grant is : 00
Request is : 01 , Grant is : 01
Request is : 10 , Grant is : 10
Request is : 11 , Grant is : 10
$finish called from file "./test.sv", line 18.
$finish at simulation time               100000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 100000 ps
CPU Time:      0.190 seconds;       Data structure size:   0.2Mb
Sat Jul 29 18:06:27 2023

以上例子用于理解接口的基本连接属性的使用,测试和设计很草率。

如果不能对符合Verilog-2001的就带马上进行修改,那么其中的端口也可以改为接口,将接口中的信号直接连接到每个端口上。如下:

module top;

    bit clk;
    always #5 clk = ~clk;

    arb_if  arbif(clk);
    arbiter abt(.grant (arbif.grant),
                .request (arbif.request),
                .clk (arbif.clk),
                .rst (arbif.rst));
    test    tl(arbif);

endmodule : top

上面的例子使用点对点且无方向的来凝结方式,但是很多通信接口中都有着数据的传输方向,这种情况可以使用接口中的modport,将信号分组,并指定方向。例如在很多通信协议中,分为Master组和Slave组,并在其中指定信号的方向,使其单独连接主机和从机。

使用modport分组可以对上面的例子做如下修改:

interface arb_if (input bit clk);
    logic [1:0] grant, request;
    logic rst;
    
    modport TEST (output request, rst,
                input grant, clk);
    modport DUT (input request, rst, clk,
                output grant);
    modport MONITOR (input request, grant, rst, clk);

endinterface

同样的,在其他模块中定义接口的时候需要做如下改动:

module test (arb_if.TEST arbif);
    ···
endmodule

module arbiter (arb_if.DUT arbif);
    ···
endmodule

这样就完成了对于端口的指定方向和分组。

这次在接口定义中,加入了monitor的分组,我们可以利用monitor来作为一个总线监视的模块。增加以下模块:

module monitor (arb_if.MONITOR arbif);
    
    always @(posedge arbif.request[0]) begin
        $display("[%0t]: request[0] asserted", $time);
        @(posedge arbif.grant[0]);
        $display("[%0t]: grant[0] asserted", $time);
    end

    always @(posedge arbif.request[1]) begin
        $display("[%0t]: request[1] asserted", $time);
        @(posedge arbif.grant[1]);
        $display("[%0t]: grant[1] asserted", $time);
    end
endmodule

当然需要在top模块中实例化并连接,打印信息如下:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

Request is : 00 , Grant is : 00
[50000]: request[0] asserted
[55000]: grant[0] asserted
Request is : 01 , Grant is : 01
[70000]: request[1] asserted
[75000]: grant[1] asserted
Request is : 10 , Grant is : 10
[90000]: request[0] asserted
Request is : 11 , Grant is : 10
$finish called from file "./test.sv", line 18.
$finish at simulation time               150000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 150000 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Sat Jul 29 19:07:57 2023

此时对打印信息不是很理解,当作一个练习,疑问保留。

综上所述,使用接口的优缺点如下:

优点

  • 接口方便了设计的重用,比如多个模块之间的信号传递,使用特定的协议,这个时候只需要设计出接口,就可以反复重用了。
  • 接口也可以代替原来需要在模块或者程序中反复声明的一系列信号,减少了连接过程中出现错误的可能性。
  • 增加一个信号的时候,只需要声明一次,不需要再更高的层次中声明,减少了错误发生的概率
  • modport允许将一个模块很方便地将接口中的一系列信号捆绑在一起,可以指定信号的方向,以便工具进行检查。

劣势

  • 对于点对点的链接,使用modport的接口面熟和使用信号列表的端口一样冗长
  • 必须使用信号名和接口名,可能会使得代码变得冗长
  • 如果接口是不会被重用的,那么写一个接口的工作量要比直接端口连线要更多的工作量
  • 连接两个不同的接口很困难,如果一个新的接口包含了现有的接口的所有信号,并新增了一些信号,那么就需要拆分出独立的信号,并正确驱动

4.3 激励时序

测试平台和设计之间的时序必须密切配合,包括驱动信号的时机、竞争冒险、数值冲突等等。在SV中有一些继续结构可以帮助控制通信中的时序问题。

上面的例子,对于接口只是做了连接的作用,但是实际上接口是可以使用时钟块来指定同步信号相对于时钟的时序的,时钟块中的任何信号都将同步驱动和采样。

一个接口可以包含多个时钟块,因为每个块中都只有一个时钟的表达式,所以对应一个时钟域。典型的时钟表达式如@(posedge clk)定义了单时钟沿,而@(clk)定义了DDR时钟(双数据率)。

一旦定义了时钟块,测试平台就可以使用@arbif.cb表达式等待时钟,不需要确切的时钟信号和边沿,这样,即使改变了时钟块中的时钟或边沿,也不需要修改测试平台的代码。

对于之前的例子,代码做如下改动:

首先是在interface里面引入clocking块:

interface arb_if (input bit clk);
    logic [1:0] grant, request;
    logic rst;

    clocking cb @(posedge clk);
        output request;
        input grant;
    endclocking
    
    modport TEST (clocking cb, output rst);
    modport DUT (input request, rst, clk,
                output grant);
    modport MONITOR (input request, grant, rst, clk);

endinterface

然后针对测试文件,做如下修改:

module test (arb_if.TEST arbif);
    initial begin
        arbif.cb.request <= 2'b01;
        $display ("[%0t\t]: Drove req = 01", $time);
        repeat (2) @arbif.cb;
        if(arbif.cb.grant != 2'b01)
            $display ("[%0t\t]: al:grant != 2'b01", $time);
        $finish;
    end
endmodule

如果想在驱动一个信号前,等待两个时钟周期,可以使用:repeat(2)@bus.cb;或者使用周期延时:##2。后一种只能在时钟块中作为驱动信号的前缀来使用,因为它需要知道以哪个时钟为基准做延时。

然后得到的仿真输出为:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

[0		]: Drove req = 01
[5000	]: request[0] asserted
[15000	]: grant[0] asserted
$finish called from file "./test.sv", line 8.
$finish at simulation time                15000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 15000 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Sun Jul 30 09:51:05 2023

在TEST modport中,将request和grant视为同步信号,时钟模块cb声明了块中的信号在时钟上升沿有效,信号方向是相对于modport的,在modport中被使用,因此,request是TEST modport的输出信号,grant是输入信号。

在VMM验证方法学中,更倾向于把信号定义成wire,因为wire可以被多个信号驱动,增加了代码的重用性,而logic重点在于易用。

logic的缺点在于只能被一个信号驱动,如果被多个信号驱动则会报错。如果测试平台在接口中使用过程赋值语句驱动一个异步信号,那么就必须要logic,如果时钟块中的信号时钟是同步的,那么可以定义为logic或wire。

在测试时,测试平台往往需要模仿测试仪的行为,例如在DUT中,存储单元在始终有效边沿锁存输入信号,然后由存储单元输出,通过逻辑块到达下一个存储单元,这中间的时延必须要与一个时钟周期,所以测试平台需要在满足协议时序的情况下,尽可能晚地采样。

设计和测试平台都用module封装后,它们之间可能会出现竞争状态。这个问题的根源在于设计和测试平台的事件混合在同一个时间片内,我们希望存在一种方法可以在时间轴上分开这些事件,就像分开代码那样。在SV中,将测试平台的代码封装在程序块(Program Block)之中,值得注意的是,程序块不能有任何的层次级别,例如模块的实例,接口等等。

这里是一个比较完整的调度语义:(去除了其他语言接口的部分,例如C)

  • Preponed:这个区域中的数值是上一个时间片中的最终稳定的值,采样断言变量。

  • Active:断言采样完成后进入本区域,只执行阻塞赋值语句,连续赋值语句,非阻塞赋值中“< =”右边的计算,源于计算以及调用系统函数(如$display),不同线程中执行与许不确定

  • Inactive:只有在线程被加上#0延迟才会进入该区域,该操作会延缓线程的操作事件,可以用于对事件执行的先后顺序进行调度。

  • NBA:把非阻塞赋值右边的数值赋给左侧。

    (以上三个基本上是为了DUT准备的,在Verilog就已经有了)

  • Observed:根据Preponed区域采样的断言的值,来评估断言中的属性是否成立,属性评估在任何一个时间段中,只发生一次。

  • Reactive:评估后,对断言表达式中的代码进行操作,看是否成功,还会执行Program中的连续赋值、阻塞赋值,非阻塞赋值右边的计算,顺序同样不确定。

  • Re-Inactive:执行Pragram块中的#0延时阻塞赋值

  • Re-NBA:Program中的非阻塞赋值左侧更新

  • Postponed:代表本时钟片中最终稳定的数值

在SV中还引入了#1step的概念,时钟块默认的输入偏移就是#1step,输出偏移是0,也就是当前时间片还未进行任何操作时采样,即和断言采样在一个区域。

以下是简化的调度语义:

区域名 行为
Active 仿真模块中的设计代码
Observed 执行System Verilog的断言
Reactive 执行程序中的测试平台的部分
Postponed 为测试平台的输入采样信号
pre=>start: Form previous time slot
next=>end: To next time slot
A=>operation: Active (design)
O=>operation: Observed (assertions)
R=>operation: Reactive (testbench)
P=>operation: Postponed (sample)
OC=>condition: Active events
RC=>condition: Active events

pre(right)->A(bottom)->O(right)->OC
OC(yes,right)->A
OC(no,bottom)->R(right)->RC
RC(yes,right)->A
RC(no,bottom)->P(right)->next

可以注意到的是,时间并不是单向向前流动的,在Observed和Reactive区域的事件可以触发本时钟周期内Active区域中的进一步的设计事件。

关于设计和测试的竞争,下面举一个例子来说:

先设计一个DUT:

module dut (input bit clk,
            input bit[2:0] a,
            output bit[2:0] b);
    
    always @(posedge clk) begin
        b <= a;
        $display("%0t\t: B is %0d", $time, b);
    end

endmodule

然后设计一个Test文件:(包含激励和监测)

module test (input bit clk,
            output bit[2:0] a,
            input bit[2:0] b);
    
    bit[2:0] r;

    initial begin
        #40 $finish;
    end

    initial begin
        repeat (4) @(posedge clk) begin
            a <= 1;
            $display("%0t\t: A is %0d", $time, a);
        end
    end

    initial begin
        repeat (4) @(posedge clk) begin
            r <= b;
            $display("%0t\t: Result is %0d", $time, r);
        end
    end

endmodule

最后在顶层模块中连接:

module top();
    
    bit clk;

    always #5 clk = ~clk;

    bit[2:0] a;
    bit[2:0] b;

    dut dut0(clk, a, b);
    test test0 (clk, a, b);

endmodule

得到的仿真结果如下:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

5000	: B is 0
5000	: A is 0
5000	: Result is 0
15000	: B is 0
15000	: A is 1
15000	: Result is 0
25000	: B is 1
25000	: A is 1
25000	: Result is 0
35000	: B is 1
35000	: A is 1
35000	: Result is 1
$finish called from file "./test.sv", line 8.
$finish at simulation time                40000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 40000 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Sun Jul 30 21:37:07 2023

这里使用的非阻塞赋值,非阻塞赋值是在NBA中进行的,这样得到的仿真结果是可预测的,符合预期的。

如果这里的非阻塞赋值全部换成阻塞赋值:(如下地方)

module dut (input bit clk,
            input bit[2:0] a,
            output bit[2:0] b);
    ...
    b = a;
    ...
endmodule

module test (input bit clk,
            output bit[2:0] a,
            input bit[2:0] b);
    ...
    a = 1;
    ...
    r = b;
    ...
endmodule

得到的仿真结果为:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

5000	: B is 0
5000	: A is 1
5000	: Result is 0
15000	: B is 1
15000	: A is 1
15000	: Result is 1
25000	: B is 1
25000	: A is 1
25000	: Result is 1
35000	: B is 1
35000	: A is 1
35000	: Result is 1
$finish called from file "./test.sv", line 8.
$finish at simulation time                40000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 40000 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Sun Jul 30 21:49:11 2023

这个时候就出现了竞争冒险,两个initial是并行的,我们没办法确定是哪一个阻塞语句先执行,不同的编译器可能得到不同的结果,这种不可预测不是我们希望的。

当我们需要使用阻塞语句的时候,但是设计和测试之间可能会存在竞争冒险,这时候应该使用Program块。以下是改动:

program test (input bit clk,
            output bit[2:0] a,
            input bit[2:0] b);
    
    bit[2:0] r;

    initial begin
        #40 $finish;
    end

    initial begin
        repeat (4) @(posedge clk) begin
            a = 1;
            $display("%0t\t: A is %0d", $time, a);
        end
    end

    initial begin
        repeat (4) @(posedge clk) begin
            r = b;
            $display("%0t\t: Result is %0d", $time, r);
        end
    end

endprogram

以下是仿真结果:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

5000	: B is 0
5000	: A is 1
5000	: Result is 0
15000	: B is 1
15000	: A is 1
15000	: Result is 1
25000	: B is 1
25000	: A is 1
25000	: Result is 1
35000	: B is 1
35000	: A is 1
35000	: Result is 1
$finish called from file "./test.sv", line 8.
$finish at simulation time                40000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 40000 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Sun Jul 30 21:58:10 2023

可以看到,这样的情况下,由于Program在Re-active区域执行的。

所以,当带时钟Program块给模块发送数据,模块不会立刻知道,直到下一个时钟片才能看到,这是符合发送数据的组件的预期。

当模块给带有时钟的Program块发送数据的时候,Program能够立即检测到。

4.4 接口的驱动和采样

测试平台需要驱动和采样设计信号,主要通过带有时钟块的接口做到的,异步信号通过接口没有任何演示,而时钟块中的信号将会得到同步。

在时钟块中应当使用同步驱动,即使用“< =”操作符来驱动。(测试平台在Reactive区域执行,而设计代码在Active区域执行)。

busif.cb.request <= 1; 		// 同步驱动
busif.cb.cmd <= cmd_buf;	// 同步驱动

如果测试平台在时钟有效边沿驱动同步接口信号,那么值会立刻传递到设计中(输出延迟#0)。如果在有效边沿之后驱动,那么直到下一个有效边沿才会被设计捕获。

在Verilog-1995中,如果你想驱动一个双向信号,你可能需要使用reg来连接wire。在SV中可以直接定义双向信号。例:

interface master_if (input bit clk);
    wire [7:0] data;	// 双向信号
    
    clocking cb @(posedge clk);
        inout data;
    endclocking
    
    modport TEST (clocking cb);
endinterface

在SV的程序(Program)中可以使用initial块,但不允许使用always块。由于一个测试平台的执行过程是经过初始化、驱动和响应设计行为等步骤后结束仿真的,如果写了always块,这个块可能从仿真的一开始就会在每一个时钟的上升沿触发执行,仿真将永远不会结束,必须得调用$exit来发出程序结束的信号。

如果你确实需要一个always块,你可以使用initial forever来完成同样的事情

值得注意的是,时钟发生器设计部分联系很紧密,而验证更关心的是在正确的时钟周期内提供正确的值,而不是纳秒级的延时和时钟的相对位移。

因此,时钟发生器应该在一个模块中,而不是在Program中。以下是一个错误例子:

program bad_generator (output bit clk, out_sig);
    initial
        forever #5 clk <= ~clk;
    initial
        forever @(posedge clk)
            out_sig <= ~out_sig;
endprogram

4.5 将这些模块连接起来

在SV中连接端口的时候可以使用快捷符号".*"(隐式端口连接),能够自动在当前级别自动连接实例的端口到具体信号,只要端口和信号的名字和数据类型相同。

如果你的模块的端口中含有接口,如果接口并没有连接,那么SV不会让你的编译通过的。因为,端口包含单个信号的模块或者程序块,即使不被例化也可以被编译,编译器会自动连线,将他们连接在对于的信号上,如果端口中含有接口,编译器将会很难办,如果接口中含有时钟块那就更难办了。

4.6 顶层作用域

有时候会需要在仿真的过程中,创建程序或者模块之外的对象,以便参与仿真的所有对象都可以访问他们。在Verilog中可以利用宏来快约模块边界,经常用来创建全局变量。

SV中引入了比那一单元,它是一起编译的源文件的一个组合。即,在任何“module、macromodule、interface、program、package、primitive”的边界之外的区域,叫做编译单元作用域,也成为$unit。

有个问题是,这回引起混淆,在VCS中,它同时编译所有的SV代码,所以$unit是全局的。但是在Design Compile一次编译一个模块或者一组模块,这时$unit可能只包含一个或者几个文件的内容,这导致了$unit不可移植。

简单的说,在SV中如果把顶层top看作层数为1,那么$unit所在的层数就为第0层,在SV中可以用$root来表示这一层级,就像是Linux里面的”/“来表示根目录,所以当我们需要来引用编译单元作用域的变量的时候,可以通过$root来帮助定位,以下为例子:

module top;
    bit clk;
endmodule

program automatic test;
    ...
    initial begin
        $display("clk = %b", $root.top.clk);	// 绝对引用
    end
    ...
endprogram

4.7 程序与模块交互

程序块可以读写模块中的所有信号,可以调用模块中的例程,但是模块却看不到程序块。因为测试平台需要访问和控制设计,但是设计却独立于测试平台的任何东西。

在测试平台中使用函数从DUT获取信息是一个好办法,如果DUT代码改变了,测试平台就可能错误地解释数值。

4.8 SystemVerilog 断言

断言,从字面上来说就是对一件事情下结论,断定某一件事情。本质上也是这样的,利用断言去断定设计的行为,对设计的属性做一个描述,在这个过程中,断言的对象是DUT,内容是属性。对于一个设计来说,我们给一个固定的输入,应该能得到一个固定的,可以预料到的输出,那么使用断言去描述设计的这种属性,当设计得到的输出出乎意料的时候,出乎断言的预料,那么断言就失败了,就会报错。这样的情况下,我们就可以知道,我们的设计没有达到我们预期的结果。

举个例子来说,如果我们的设计是一个半加器,当我们给1和1的时候,正常情况下,我们应该得到的输出是0,那么我就去断言此时输出为0,如果断言失败,说明设计的功能和预计有差别,就可以说明设计存在问题。

在使用断言之前,我们可能会使用if-else语句来实现同样的效果,代码如下:

program test (input bit clk,
            output bit[2:0] a,
            input bit[2:0] b);
    
    initial begin
        forever @(posedge clk) begin
            a = b + 3'd1;

            // if-else
            if (b % 2 == 0)
                $display("Passed %0d\tis odd number", b);
            else

                $error("Failed %0d\tis odd number", b);
        end
    end

    initial begin
        #100 $finish;
    end


endprogram

仿真结果如下:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

Passed 0	is odd number
Error: "./test.sv", 14: top.test0: at time 15000 ps
Failed 1	is odd number
Passed 2	is odd number
Error: "./test.sv", 14: top.test0: at time 35000 ps
Failed 3	is odd number
Passed 4	is odd number
Error: "./test.sv", 14: top.test0: at time 55000 ps
Failed 5	is odd number
Passed 6	is odd number
Error: "./test.sv", 14: top.test0: at time 75000 ps
Failed 7	is odd number
Passed 0	is odd number
Error: "./test.sv", 14: top.test0: at time 95000 ps
Failed 1	is odd number
$finish called from file "./test.sv", line 19.
$finish at simulation time               100000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 100000 ps
CPU Time:      0.190 seconds;       Data structure size:   0.2Mb
Sat Aug  5 11:38:04 2023

现在,我们可以用断言来实现,以下是直接断言的例子:

program test (input bit clk,
            output bit[2:0] a,
            input bit[2:0] b);
    
    initial begin
        forever @(posedge clk) begin
            a = b + 3'd1;

            // assert
            al : assert (b % 2 == 0);
        end
    end

    initial begin
        #100 $finish;
    end
endprogram

以下是仿真结果:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

"./test.sv", 10: top.test0.al: started at 15000ps failed at 15000ps
	Offending '((b % 2) == 0)'
"./test.sv", 10: top.test0.al: started at 35000ps failed at 35000ps
	Offending '((b % 2) == 0)'
"./test.sv", 10: top.test0.al: started at 55000ps failed at 55000ps
	Offending '((b % 2) == 0)'
"./test.sv", 10: top.test0.al: started at 75000ps failed at 75000ps
	Offending '((b % 2) == 0)'
"./test.sv", 10: top.test0.al: started at 95000ps failed at 95000ps
	Offending '((b % 2) == 0)'
$finish called from file "./test.sv", line 16.
$finish at simulation time               100000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 100000 ps
CPU Time:      0.210 seconds;       Data structure size:   0.2Mb
Sat Aug  5 13:01:29 2023

“al”是给断言的命名,括号内是断言的内容,如果断言失败了,会自动打印报错信息,当然这个信息也是可以定制的。

如果对断言内容进行自定义,如下:

program test (input bit clk,
            output bit[2:0] a,
            input bit[2:0] b);
    
    initial begin
        forever @(posedge clk) begin
            a = b + 3'd1;

            // assert
            al : assert (b % 2 == 0)
            else $error("Failed %0d\tis odd number", b);
        end
    end

    initial begin
        #100 $finish;
    end
endprogram

以下是仿真结果,可以看在打印错误的同时,打印了我们自定义的错误内容。

      (Specify +UVM_NO_RELNOTES to turn off this notice)

"./test.sv", 10: top.test0.al: started at 15000ps failed at 15000ps
	Offending '((b % 2) == 0)'
Error: "./test.sv", 10: top.test0.al: at time 15000 ps
Failed 1	is odd number
"./test.sv", 10: top.test0.al: started at 35000ps failed at 35000ps
	Offending '((b % 2) == 0)'
Error: "./test.sv", 10: top.test0.al: at time 35000 ps
Failed 3	is odd number
"./test.sv", 10: top.test0.al: started at 55000ps failed at 55000ps
	Offending '((b % 2) == 0)'
Error: "./test.sv", 10: top.test0.al: at time 55000 ps
Failed 5	is odd number
"./test.sv", 10: top.test0.al: started at 75000ps failed at 75000ps
	Offending '((b % 2) == 0)'
Error: "./test.sv", 10: top.test0.al: at time 75000 ps
Failed 7	is odd number
"./test.sv", 10: top.test0.al: started at 95000ps failed at 95000ps
	Offending '((b % 2) == 0)'
Error: "./test.sv", 10: top.test0.al: at time 95000 ps
Failed 1	is odd number
$finish called from file "./test.sv", line 17.
$finish at simulation time               100000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 100000 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Sat Aug  5 13:09:50 2023

上面这些实际上都是直接断言的例子,还存在另一种断言,叫做并发断言,可以认为是一个连续运行的模块,在整个仿真过程都会检查信号的值,需要在断言内指定一个采样时钟,可以使用“disable iff”语句,来设定断言失效的条件,例子如下:

program test (input bit clk,
            output bit[2:0] a,
            input bit[2:0] b);

    logic dis;
    
    initial begin
        forever @(posedge clk) begin
            a = b + 3'd1;
        end
    end

    initial begin
        #0 dis = 0;
        #10 dis = 1;
        #20 dis = 0;
        #70 $finish;
    end

    property p_check_x_is_odd (bit [2:0] x);
        @(posedge clk)
        disable iff(dis)
        x % 2 == 0;
    endproperty

    assert_check_x_is_odd : assert property (p_check_x_is_odd(b));
endprogram

以下是仿真结果:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

"./test.sv", 26: top.test0.assert_check_x_is_odd: started at 45000ps failed at 45000ps
	Offending '((b % 2) == 0)'
"./test.sv", 26: top.test0.assert_check_x_is_odd: started at 65000ps failed at 65000ps
	Offending '((b % 2) == 0)'
"./test.sv", 26: top.test0.assert_check_x_is_odd: started at 85000ps failed at 85000ps
	Offending '((b % 2) == 0)'
$finish called from file "./test.sv", line 17.
$finish at simulation time               100000
           V C S   S i m u l a t i o n   R e p o r t 
Time: 100000 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Sat Aug  5 13:54:05 2023

在这个并发断言的例子中,使用了 disable iff 的语句,在10ns的时候关闭了断言,在30ns的时候开启了断言,和之前例子相比,15ns和35ns的断言失败的报告信息消失了。

对于并发断言,我们同样可以在断言的声明后面,加上自定义的警告信息。

断言还有其他的用法,例如在接口中使用断言,那么接口不仅可以传送信号,还可以检查协议的正确性。

4.9 ref端口的方向

ref是对变量的引用,类似于得到了变量的地址,直接修改地址上的值,所以它的值是变量最后一次赋值,如果将一个变量连接到多个ref端口,那么多个模块之间就可能会因为在同一时间修改变量而产生竞争。

4.10 仿真的结束

仿真程序块最后一个initial块结束的时候,仿真就结束了,隐性地调用了$exit标志着程序的结束,当所有程序块都推出了$finish函数隐性调用也就结束了,也可以在需要的时候调用$finish来结束仿真。

模块和程序可以拥有一个或者多个finial块来执行仿真器退出前的代码,这是一个用来放置清理任务的绝佳位置,比如关闭文件,输出一个发生的错误和警告数量的报告。