SV 第三章 过程语句和子程序

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

SystemVerilog验证

3 过程语句和子程序

在编写验证代码的时候,很多代码是在任务和函数里面的,SV增加了很多改进,使其更接近C语言。

3.1 过程语句

和C++类似,SV在for循环中可以定义循环变量,作用域在循环体内,同时也添加了自增自减运算符,即++ --。对于循环,SV增加了continue和break来进入下一循环或跳出循环,与C语言类似。

在begin 和 end或类似的结束语句后面,可以增加标志符,来使得begin和end 更容易配对。例如

begin : example
end : example 

这些特性就不代码仿真了,后面肯定还会编写很多次。

3.2 任务、函数以及void函数

在Verilog中,任务(task)和函数(function)之间有很明显的区别,任务可以消耗时间,而函数不行,函数里不能带有诸如#100的实验语句或诸如@(posedge clock)、wait(ready)的阻塞语句,也不能调用任务。并且函数必须有返回值,返回值必须被使用。

在SV中允许函数调用任务就,但只能在fork···join_none语句生成的线程中调用。

如果你有一个不消耗时间的systemverilog任务,那么应该定义为void函数,这种函数没有返回值,它就可以被任何任务或函数所调用。

如果你想调用函数并忽略他的返回值,可以使用void进行转换,例如:

void'($fsacnf(file,"%d",i))

3.3 任务和函数的概述

SV在函数定义时改进了函数范围,begin···end变成可有可无的了,使用对应的结束语句对于边界定义已经足够了。例:

task multiple_line;
    $display("First line");
    $display("Second line");
endtask : multiple_lines

3.4 子程序的参数

在SV中,函数声明和C语言更接近,符合C语言的风格。缺省的参数类型时“logic 输入”,如果不特别声明,那么默认就是这种参数形式。但是不推荐,容易滋生不容易排查的细小漏洞。

使用ref参数:ref参数时引用,尤其是对数组的传参来说,如果不带ref,那么会对数组进行拷贝到对战区域,在如果数组很大,那么这样的操作代价很高,使用ref引用,就可以在原数组直接修改。这一点与C++类似。

并且,SV的函数支持缺省,例如原本写了一个一个参数的函数,但是想增加对于该函数更多的控制,但是不想取大规模修改代码,那么就可以增加缺省参数。针对以上内容,以下编写一个例子:

module tb();

    function void print_checksum_old (  ref bit [31:0] a[]);
        automatic bit [31:0] checksum = 0;

        $display("---------------------------");
        $write("[old] Add process is : ");
        for(int i = 0; i < a.size(); i++) begin
            checksum += a[i];
            $write("%0d ", a[i]);
        end
        $display();
        $display("[old] The array checksum is %0d", checksum);
    endfunction

    function void print_checksum_new (  ref bit [31:0] a[],
                                        input bit [31:0] low = 0,
                                        input int high = -1);
        automatic bit [31:0] checksum = 0;
        

        $display("---------------------------");
        $write("[new] Add process is : ");
        if(high == -1 || high > a.size() - 1)
            high = a.size() - 1;
        for (int i = low; i <= high; i++) begin
            checksum += a[i];
            $write("%0d ", a[i]);
        end
        $display();
        $display("[new] The array checksum is %0d", checksum);
    endfunction

    initial begin

        bit [31:0] a[] = '{32'd111, 32'd222, 32'd333, 32'd444, 32'd555};

        print_checksum_old(a);
        print_checksum_new(a);

        print_checksum_new(a, 1, 2);
        print_checksum_new(a, 1);
        print_checksum_new(a,.low(1));
        print_checksum_new(a, , 2);
        print_checksum_new(a,.high(2));
    
    end

endmodule

代码模拟了利用缺省值对老函数进行修改,并和老函数进行对比,保证不改变原先的功能正常实现,还可以通过新增加参数来增加对于求和函数的控制。以下是仿真代码:

      (Specify +UVM_NO_RELNOTES to turn off this notice)

---------------------------
[old] Add process is : 111 222 333 444 555  
[old] The array checksum is 1665
---------------------------
[new] Add process is : 111 222 333 444 555  
[new] The array checksum is 1665
---------------------------
[new] Add process is : 222 333  
[new] The array checksum is 555
---------------------------
[new] Add process is : 222 333 444 555  
[new] The array checksum is 1554
---------------------------
[new] Add process is : 222 333 444 555  
[new] The array checksum is 1554
---------------------------
[new] Add process is : 111 222 333  
[new] The array checksum is 666
---------------------------
[new] Add process is : 111 222 333  
[new] The array checksum is 666
           V C S   S i m u l a t i o n   R e p o r t 
Time: 0 ps
CPU Time:      0.200 seconds;       Data structure size:   0.2Mb
Fri Jul 28 16:25:24 2023

可以看到,老函数的功能正常实现,新增加的控制也可以实现。

对于缺省参数的传参的时候,可以像C语言一样的赋值,不传参的地方就空着就可以了,由于函数在SV中,参数也是port,所以也可以使用对端口传参的形式,来指定给某个缺省参数传参。

在使用缺省值的时候,往往会忽略他们的参数类型,包括方向和类型。不指定的情况下,他们和前一个参数的类型是一样的,例如:

task sticky (ref int array[50],

​ int a,b);

在这里的a和b都是ref类型,犯了一个方向的错误。

值得注意的是,我在函数变量定义中使用了automatic关键字,与之相对应的是static关键字。因为在仿真时发现,每次调用函数得到的值会累加在上一次计算结果上。使用automatic关键字,说明该变量为动态变量,在使用完毕之后会回收,如果使用static或不直接声明,那么就是静态变量,将会在第一次调用的时候产生存储空间,不会回收,所以每次累加都会在上次的计算结果之上进行。

在模块一级中,只能使用静态变量,在函数、任务中以及begin end 或fork join字段中,可以使用关键字。

调用时的区别

  • 动态变量:每次调用时会重新开辟新的空间
  • 静态变量:在两次调用期间变量的值会保持

因此,如果希望知道某个变量被调用的次数,那么这个变量应该声明为静态变量

缺省声明时的区别

  • 在模块、begin...end、fork...join以及非动态的任务和函数中,缺省时为静态变量
  • 在动态的任务和函数中,缺省时为动态变量
  • 模块一级的变量必须是静态变量

静态变量和动态变量使用原则

  • 在always和initial块中,需要内嵌初始化就声明为动态变量,不需要内嵌初始化用静态变量
  • 如果任务或函数是可重入的,应设置被动态的,其内的变量也应是动态的
  • 如果任务或函数用来描述独立的硬件部分,且不可重入,应该设置为静态的,其内的变量也应为静态的

3.5 子程序的返回

和C语言一致,在SV中加入了return语句。return使得子程序在流程控制变得更加方便。

当然和C语言一致,在函数或任务定义的时候,就应该指明返回数据的类型。

如果想返回数组,可以像之前那样,使用ref引入数组,也可以通过自定义一个数组类型,然后在函数声明中使用该类型,以下是一个例子:

typedef int fixed_array_5[5];
fixed_array_5 f5;

function fixed_array_5 init(int statrt);
    ···
endfunction

3.6 局部数据存储

在Verilog中,最初是用来描述硬件的,所以语言中的所有对象都是静态分配的,特别是,子成熟参数和局部变量是被存储在固定的位置的,而不是像C语言那样存储在堆栈区域的。

而做验证的软件工程师来说,使用Verilog可能会有些困难。如果你的一个测试程序需要在多个地方调用一个任务,因为任务的局部变量是使用一个共享的静态存储区,不同线程之间会窜用这些局部变量,在verilog中可以使用自动存储,迫使仿真器使用盾战存储这些局部变量。

例如:

program automatic test;
    tsk wait_for_mem(input[31:0] addr, expect_data,
                     output success);
    while (bus.addr != addr)
        @(bus.addr);
    sucess =(bus.addr == expect_data);
    endtask
    ···
endprogram

关于动态变量和静态变量,我在3.4的时候遇到了,并做了详细介绍,这里就不在赘述了。

3.7 时间值

你可以把时间值存储在变量里面,但是由于量程和精度,数值会被缩放或舍入。time类型的变量不能保存小数时延,因为它们是64bit的整数。系统任务$time的返回值是一个根据所在模块的时间精度要求进行舍入的整数,不带小数部分,而$realtime返回值是一个带小数部分的完整实数。