SystemVerilog 第3章过程语句和子程序

105 篇文章 13 订阅

3.2任务、函数以及void函数
        在 Verilog中,任务(task)和函数( function)之间有很明显的区别,其中最重要的一点是,任务可以消耗时间而函数不能。函数里面不能带有诸如#100的时延语句或诸如( posedge clock)、wait( ready)的阻塞语句,也不能调用任务。另外, Verilog中的函数必须有返回值,并且返回值必须被使用,例如用到赋值语句中System verilog对这条限制稍有放宽,允许函数调用任务,但只能在由fork join_none语句生成的线程中调用,7.1节中有这方面的介绍。
        如果你有一个不消耗时间的 Systemverilog任务,你应该把它定义成void函数,这种函数没有返回值。这样它就能被任何任务或函数所调用了。从最大灵活性的角度考虑,所有用于调试的子程序都应该定义成void函数而非任务,以便于被任何其他任务或函数所调用。例3.3可以输出状态机的当前状态值。
3.3用于调试的void函数

   function void  print_state( void );
	$display("@%0t :state=%s ",$time,surrent_state.name());
  endfunction

        在 System Verilog中,如果你想调用函数并且忽略它的返回值可以使用void进行结果转换,如例3.4所示。有些仿真器,如VCS,允许你在不使用上述void语法的情况下忽略返回值。
例3.4忽略函数的返回值

        void'($fscanf(file,"%d",i));

3.3任务和函数概述

        System Verilog在任务和函数上做了一些小改进,使得它们看起来更像C或C++的程序。一般情况下,不带参数的子程序在定义或调用时并不需要带空括号()。为清楚起见,本书对此种情形的子程序将全部带括号。

        3.3.1在子程序中去掉 begin...end

        在 System verilog中,你可能会注意到的第一个改进就是, begin...end块变成可选了,而在 Verilog-1995中则对单行以外的子程序都是必须的。如例3.5所示,task/endtask和 function/ endfunction的关键词已经足以定义这些子程序的边界了。
例3.5不带 begin...end的简单任务

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

3.4子程序参数
        SystemVerilog对子程序的 很多改进使参数的声明变得更加方便,同时也扩展了参数的传递方式。
3.4.1C语言风格的子程序参数
        SystemVerilog和Verilog-2001在任务和函数参数的声明上更加简洁,更少重复。例3.6中的verilog任务要求对一些参数进行两次声明,一次时方向声明,另一次是类型声明。
例3.6 Verilog-1995的子程序参数

task my_task;
    output [31:0] x;
    reg [31:0] x;
    input y;
    ...
endtask

        在SystemVerilog中,可以采用简明的C语言风格,如例3.7所示。但注意必须使用通用的输入类型logic。

例3.7C语言风格的子程序参数

task my_task(output logic [31:0] x,input logic y);
    ...
endtask

3.4.2参数的方向

        在声明子程序参数方面还可以有更多的便捷。因为缺省的类型和方向是“ logic输入”,所以在声明类似参数时可不必重复。例3.8所示为采用 System Verilog的数据类型,但以Verilog-1995的风格编写的一个子程序头。
例3.8带 Verilog风格的繁冗的子程序参数

task t3;
    input a, b;
    logic a, b;
    output [15: 0]    u,v;
    bit[15:0]    u, v;
...
endtask
//可以把它重写成例3.9的形式。

例39带缺省类型的子程序参数

 task T3(a, b, output bit [15: 0] u, v);

        参数a和b是1比特宽度的 logic输入。参数u和v是16比特宽度的bit类型输出。尽管有这种简洁的编程方式,但不建议使用这种方式,因为如同3.4.6节中解释的那样,这种方式将使代码滋生一些细小而难以发现的漏洞。所以建议对所有子程序参数的声明都带上类型和方向

3.4.3高级的参数类型

        verilog对参数的处理方式很简单:在子程序的开头把 input和 inout的值复制给本地变量,在子程序退出时则复制 output和 inout的值。除了标量以外,没有任何把存储器传递给Verilog子程序的办法。在 System verilog中,参数的传递方式可以指定为引用而不是复制。这种ref参数类型比 Input、 output或 inout更好用。首先,你现在可以把数组传递给子程序。
例3.10使用ref和 const传递数组

   function automatic void print_check_sum(const ref bit [31:0] a[]);
	  bit [31:0] checksum;
	for (int i=0 ;i<a.size();i++)
		checksum^=a[i];
	$display("the array checksum is %0d ",checksum);
   endfunction 

        System verilog允许不带ref进行数组参数的传递,这时数组会被复制到堆栈区里。这种操作的代价很高,除非是对特别小的数组。System verilog的语言参考手册(LRM)规定了ref参数只能被用于带自动存储的子程序中。如果你对程序或模块指明了 automatic属性,则整个子程序内部都是自动存储的。3.6节中有关于存储的更多细节。例3.10也用到了 const修饰符。其结果是,虽然数组变量a指向了调用程序中的数组,但子程序不能修改数组的值。如果你试图改变数组的值,编译器将报错。
        ref参数的第二个好处是在任务里可以修改变量而且修改结果对调用它的函数随时可见。当你有若干并发执行的线程时,这可以给你提供一种简单的信息传递方式。更多细节可参考第7章关于使用 fork-join的介绍。
        在例3.11中,一旦 bus. enable有效,初始化块中的 thread2块马上就可以获取来自存储器的数据,而不用等到 bus_read任务完成总线上的数据处理后返回,这可能需要若干个时钟周期。由于参数data是以ref方式传递的,所以只要任务里的data一有变化,@data语句就会触发。如果你把data声明为 output,则 @data语句就要等到总线处理完成后才能触发。
3.11多线程间使用ref

	task bus_read (input logic [31:0] addr,
		ref logic [31:0] data);
		
		//请求总线并驱动地址
		bus.request=1'b1;
		@(posedge bus.grant)
			bus.addr=addr;
		
		//等待来自存储器的数据
		@(posedge bus.enable )
			data=bus.data;
		
		//释放总线并等待许可
		bus.request=1'b0;
		@(negedge bus.grant);
	endtask
	
	logic [31:0] addr,data;
	
	initial
	begin
		fork
			bus_read(addr,data);
			thread2:begin
			@data:begin
			$display("Read %h from bus",data);
			
			end
			end
		join 

3.4.4参数的缺省值

        当测试程序越来越复杂时,你可能希望在不破坏已有代码的情况下增加额外的控制。在例3.10的函数里,可能想把数组中间部分元素的校验和打印出来,但是又不希望改写代码,为每次函数调用增加额外的参数。在 Systemverilog中,可以为参数指定一个缺省值,如果在调用时不指明参数,则使用缺省值。例3.12为 print_checksum函数增加了low和high两个参数,这样你就能够打印出指定范围内的数组内容的校验和。
例3.12带缺省参数值的函数

	function automatic void print_checksum(ref bit [31:0] a[],input bit [31:0] low=0,input int high=-1);
	bit [31:0] checksum;
	 if (high==-1 || high<=a.size())
		high=a.size()-1;
	for (int i=low;i<=high;i++)
		checksum+=a[i];
	$display("the array checksum is %d \n",checksum);
	endfunction

        你可以使用如例3.13所示的方式调用这个函数。注意,第一个调用对两种形式的print_checksum子程序都是可行的。

print_checksum(a);//a[0:size()-1]中所有元素的校验和—缺省情况
print_checksum(a,2,4);∥/a[2:4]中所有元素的校验和
print_checksum(a,1);//i从1开始
print_checksum(a,,2);//a[0:2]中所有元素的校验和
print_checksum();//编译错误:a没有缺省值

        使用-1(或其他任何越界值)作为缺省值,对于获知调用时是否有指定值,不失为个好方法。        
        Verilog中的for循环总是在执行初始化(int  i=low)和条件测试(i<=high)之后再开始循环。所以,如果你不小心把一个大于high或数组宽度的数值传递给low,那么for循环的循环体将不会被执行

3.4.5采用名字进行参数传递

        你也许已经注意到,在 System Verilog的语言参考手册(LRM)中,任务或函数的参数有时被称为端口“port”,就跟模块的接口一样。如果有一个带着许多参数的任务或函数,其中一些参数有缺省值,而你又只想对它们中的部分参数进行设置,那么可以通过采用类似port的语法指定子程序参数名宇的方式来指定一个子集,如例3.14所示。
例3.14采用名字进行参数传递

task many(input int a=1,b=2,c=3,d=4);
  $display("%0d %0d %0d %0d",a,b,c,d);
  endtask
  
  initial 
	begin
		many(6,7,8,9); //指定所有值,a=6 b=7 c=8 d=9
		many();  //1 2 3 4 ,使用缺省值
		many(.c(5)); //1 2 5 4,只指定c
		many(,6,.d(8)); //1 6 3 8 混合方式
	end

3.4.6常见的代码错误
        在编写子程序代码时最容易犯的错误就是,你往往会忘记,在缺省的情况下参数的类型是与其前一个参数相同的,而第一个参数的缺省类型是单比特输入。先看看例3.15所示的简单的任务头。
 例3.15原始的任务头

task sticky(int a, b);

        这两个参数都是整型输入。在编写这个任务时,你需要访问一个数组,因此又加入了个新的数组参数.并且使用ref类型以便让数组值不被复制。修改后的子程序头如例3.16所示。
例3.16加入额外数组参数的任务头

task sticky (ref int array 50]int a, b);/这些变量的方向是什么?

        a和b的参数类型是什么呢?它们在方向上实际采用的是与前一个参数一致的ref类型。对简单的int变量使用ref通常并无必要,但编译器不会对此做出任何反应,连警告都没有,所以你不会意识到正在使用一个错误的方向类型。如果在子程序中使用了非缺省输入类型的参数,应该明确指明所有参数的方向,如例3.17所示。
例3.17加入额外数组参数的任务头

​​​​​​​task sticky(ref int array[ 50],
    input int a, b);∥/明确指定方向

3.5.1返回( return)语句
        System Verilog增加了 return语句,使子程序中的流程控制变得更方便。例3.18中的任务由于发现错误而需要提前返回。如果不这样做,那么任务中剩下的部分就必须被放到一个else条件语句中,从而使得代码变得不规整,可读性也降低了。
例3.18在任务中用 return返回

task load array(int len, ref int array[ ]);
        if (len<=0)
                begin
                        $display("Bad len");
                        return;
                end
                //任务中其余的代码
            ...
 endtask

return语句也可以简化函数,如例3.19所示。
例3.19在函数中用 return返回

​​​​​​​function bit transmit(...);
    //发送处理
    ...
    return ~ifc.cb.error;//返回状态:0= error
endfunction

3.5.2从函数中返回一个数组
        Verilog的子程序只能返回一个简单值,例如比特、整数或是向量。如果你想计算并返回一个数组,那就不是一件容易的事情了。在 System verilog中,函数可以采用多种方式返回一个数组,第一种方式是定义一个数组类型,然后在函数的声明中使用该类型。例3.20使用了例2.35的数组类型,并创建了一个函数来初始化数组。
例3.20使用 typedef从函数中返回一个数组

	typedef int fixed_array5[5];
    fixed_array5 f5;
    
    function fixed_array5 init(int start);
        foreach(init[i])
                init[i]=i+start;
        endfunction

initial begin
    f5=init(5);
    foreach (f5[i])
        $display("f5[%0d]=%0d",i, f5[i]);
    end

        使用上述代码的一个问题是,函数init创建了一个数组,该数组的值被拷贝到数组f5中。如果数组很大,那么可能会引起一个性能上的问题。
        另一种方式是通过引用来进行数组参数的传递。最简单的办法是以ref参数的形式将数组传递到函数里,如例3.21所示。
例3.21把数组作为ref参数传递给函数

 function automatic void init (ref int f[5],input int start);
	foreach (f[i])
		f[i]=i+start;
endfunction

int fa[5];
initial begin
	init(fa,5);
	foreach(fa[i])
		$display("fa[%0d]=%0d ",i ,fa[i]);
		
end

从函数中返回数组的最后一种方式是将数组包装到一个类中,然后返回对象的句柄。

3.6局部数据存储

        Verilog在20世纪80年代被创建时,最初的目的是用来描述硬件。因此,语言中的所有对象都是静态分配的。特别是,子程序参数和局部变量是被存放在固定位置的,而不像其他编程语言那样存放在堆栈区里。诸如递归子程序一类的动态代码没有对应的芯片实现方式,那还有什么必要为它们建模呢?对于那些做验证的软件工程师来说使用 Verilog可能会有些困难,他们已经习惯了像C一类的基于堆栈区( stack- based)的语言,因而在使用子程序库创建复杂测试平台方面可能会显得力不从心。        
 3.6.1自动存储
        在 verilog-1995里,如果你试图在测试程序里的多个地方调用同一个任务,由于任务里的局部变量会使用共享的静态存储区,所以不同的线程之间会窜用这些局部变量。在verilog2001里,可以指定任务、函数和模块使用自动存储,从而迫使仿真器使用堆栈区存储局部变量。
        在 System verilog中,模块( module)和 program块中的子程序缺省情况下仍然使用静态存储。如果要使用自动存储,则必须在程序语句中加入automatic关键词。第4章将详细讲解用于编写测试平台代码的 program块。7.1.6节给出了如何在创建多线程时使用动态存储。例3.22所示的是一个用于监测数据何时被写入存储器的任务。
例3.22在 program块中指定自动存储方式

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

        因为参数addr和 expect data在每次调用时都使用不同的存储空间,所以对这个任务同时进行多次调用是没有问题的。但如果没有修饰符 automatic,由于第一次调用的任务处于等待状态,所以对 wait_for_mem的第二次调用会覆盖它的两个参数。

3.6.2变量的初始化
  
      当你试图在声明中初始化局部变量时,类似的问题也会出现,因为局部变量实际上在仿真开始前就被赋了初值。常规的解决方法是避免在变量声明中赋予除常数以外的任何值。对局部变量使用单独的赋值语句也会使控制变得更方便
        例3.23中的任务在检测总线五个周期以后,创建了一个局部变量并试图把当前地址总线的值作为初值赋给它。
例3.23静态初始化的漏洞

 program initialization ;//有漏洞的版本
	task check_bus;
		repeat(5) @(posedge clock);
			if(bus_cmd=='READ)
				begin
				//何时对local_addr 赋初始值?
				logic [7:0] local_addr=addr<<2;//有漏洞
				$display("local addr =%h",local_addr);
			end
		endtask
	endprogram

        存在的漏洞是,变量local_addr是静态分配的,所以实际上在仿真的一开始它就有了初值,而不是等到进入 begin.,end块中才进行初始化。同样,解决的办法是把程序块声明为 automatic,如同例3.24所示。
例3.24修复静态初始化的漏洞:使用 automatic

program automatic initialization;/漏洞被修复
​​​​​​​  ...
        endprogram

        此外,你如果不在声明中初始化变量,那这个漏洞也可以避免,只是这种方式不太好记住,尤其是对那些习惯了C语言的程序员。例3.25给出了一种较为可取的编码风格,用于分离声明和初始化。
例3.25修复静态初始化的漏洞:把声明和初始化拆开

​​​​​​​logic [7: 0] local addr;local addr=add<2;//漏洞

3.7时间值

        System Verilog有几种新结构使你可以非常明确地在你的系统中指明时间值。

3.7.1时间单位和精度

        当你依赖于编译指示语句' timescale时,在编译文件时就必须按照适当的顺序以确保所有的时延都采用适宜的量程和精度。 timeunit和 timprecision声明语句可以明确地为每个模块指明时间值,从而避免含糊不清。例3.26展示了这些声明语句。注意,如果你使用这些语句替代' timescale,则必须把它们放到每个带有时延的模块里。
3.7.2时间参数

        System Verilog允许使用数值和单位来明确指定一个时间值。代码里可以使用类似0.1ns和20ps的时延。只要记得使用 timeunit和 timprecision、或者' timescale即可。你还可以通过使用经典的Verilog时间函数 $timeformat, $time和 $realtime来使代码在时间标度上更清楚。 $timeformat的四个参数分别是时间标度(-9代表纳秒12代表皮秒),小数点后的数据精度,时间值之后的后缀字符串,以及显示数值的最小宽度。
 例3.26所示的是使用 $timeformat()和各指定符进行格式化后的多种时延以及打印结果。
例3.26时间参数和 $timeformat

module timing;
		timeunit 1ns;
		timeprecision 1ps;
		
		initial begin
			$timeformat(-9,3,"ns",8);
			#1 $display("%t",$realtime);//1.00ns
			#2ns $display("%t",$realtime);//3.00ns
			#0.1ns $display("%t",$realtime);
			#41ps $display("%t",$realtime);//3.141ns
			
		end

3.7.3时间和变量
        你可以把时间值存放到变量里,并在计算和延时中使用它们。根据当前的时间量程和精度,时间值会被缩放或舍入。time类型的变量不能保存小数时延,因为它们是64比特的整数,所以时延的小数部分会被舍入。如果你不希望这样,那你应该采用rea1变量。例3.27使用实型(real)变量保存精确的数值,它们只在用作时延量的时候才被舍入。

`timescale 1ps/1ps
module pps;
	initial begin
	real rdelay=800fs;//以0.800存储
	time tdelay =800fs;  //舍入后得到1
	$timeformat(-15,0,"fs",5);
	#rdelay;			//时延舍入后得到1ps
	$display("%t ",rdelay);//"800fs"
	
	#tdelay;
	$display("%t",tdelay);
	
	end
endmodule

3.7.4 $time与$realtime的对比系统任务
        $time的返回值是一个根据所在模块的时间精度要求进行舍入的整数,不带小数部分,而 $realtime的返回值则是一个带小数部分的完整实数。本书为简洁起见,所举例子中全部使用 $time,但请不要忘记你的测试平台可能需要使用 $realtime。

3.8结束语
        System Verilog的程序化结构和任务、函数中的新特点使得它与诸如C/C++一类的编程语言更加接近,从而也更便于编写测试平台。和C/C++相比, System verilog还拥有额外的HDL结构,例如,时序控制、简单的线程控制和四态逻辑等。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值