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返回值是一个带小数部分的完整实数。