保姆级教程超硬核包会,SystemVerilog SV类(class)

前言: 介绍了类的封装、类的继承、类的多态包的使用、随机约束、线程间的同步和通信和类型转化。文章很长但通俗易懂,耐心看下去你会通透的。

类class

类和模块的异同

  • 从数据和方法定义而言,二者均可作为封闭的容器来定义和存储。
  • 从例化来看,module(默认是静态)在仿真还没运行就被确定了,而class(默认是动态)是在仿真开始之后的任意时间被创建的。
  • 从封装性来看:class具有封装性可以保护变量,而module没有封装性,无法保护变量。
  • 从继承性来看:class具有集成性,class之间可以发生集成关系,而module没有继承性。

类与结构块的异同

  • 二者本身都可以定义数据成员
  • 类例化后才能存储动态数据,而结构块声明后即可。
  • 类可以声明方法,结构块不能。

1. 简单了解概念:类class

在SV中提到类的时候一般从==类的封装、类的继承和类的多态(虚方法)==这三个方面来介绍它。本文着重介绍这三个方面,并

  • 类是可以包含数据和方法(function,task)的类型。
  • 类中可以包含指令、地址、队列ID、时间戳和数据等成员。
  • 定义了类,可以在其中对数据做初始化、设置指令、读取该类状态以及检查队列ID。
  • 对比于module,类是一个软件概念,在下文会做讲解。

2. 类的封装

类的三要素类的封装、类的继承和类的多态(虚方法)

2.1 简单用法

先从类中有什么,怎么用说起。

  • 类(class):包含成员变量和方法。
  • 对象(object):类在例化后的实例,只存在在仿真的后台
  • 句柄(handle):指向对象的指针。
  • 原型(prototype):程序的声明部分,包括程序名、返回类型和参数列表。

类有点类似于模块和结构体,但也有差别。从下面这段代码可以帮助理解类和模块的差别,类中的对象,句柄可以做一个简单的了解。

class paket_c ;//声明一个类
	integer command ; //integer 四值有符号,int二值有符号
endclass

module packet_m ; 
	integer command ; 
endmodule

typedef struct{//结构体
	integer command ; 
}packet_s

module tb ;
	packet_m m1() ;//实例化模块
	initial begin
		packet_m m2() ; //模块不能在initial中例化,所以此行错误
	end
	packet_s s1; //例化结构体
	packet_c c1; //c1,c2被称作句柄。
	packet_c c3 = new() ;
	c1 = new()//c1、c3都完成了类的例化
	initial begin 
		packet_s s2 ;//
		packet_c c2	;
		packet_c c4 = new() ;//类的例化
	end
endmodule
  1. 看完上边的代码,不难看出类(class)模块(module)都是名字开头,end名字结束。模块(module)例化不能发生在initial中,而结构块(struct)类(class)可以在initial中的例化

  2. packet_m m1,m1为例化后的模块;packet_s s1,s1为例化后的结构体,而对于packet_c c1,c1只是一个声明的句柄,并不是例化后的class。

  3. 只有当调用了new() 函数packet_c c3 =new();才算完成了类的例化,才能存放动态数据。使用new()相当于在内部开辟一个空间,这个空间的名字叫"对象"。但对象是一个偏软件的概念,想找这个对象只能通过句柄找,也就是句柄指向对象。

顶级奸细d(对象)、普通奸细p(句柄)和接头点来模仿上述过程。特务组织首先培养p ,也就是class p,接着组织告诉p,他还有一个同伙d,他不是一个人在战斗因为隐藏的太深不能暴露身份。只能和他在固定的接头点联络。也就是p = new(),new()就是那个接头点。在任务中 d是不会暴露的也就是不会被找到,但是可以找到pp可以去接头点找到 d。通透了吧?

运行上述代码,在仿真刚开始时,结构体s1,s2会有初始值,c1,c2(句柄)此时也会有初始值但为null,如下图。因为此时new() 的动作还没开始,只有在仿真以后才会创建空间。运行仿真以后c1,c2值不再为null。

  • 仿真刚开始,s1,s2值为32’hxxxxxxxx,c1,c2值为null。
  • 仿真开始后,c1,c2有了值,@packet_c表示所属的类,@1、@2表示第几次例化。 句柄(c1)指向对象(@packet_c@1)。
  • 上述s1,s2(在仿真刚开始就有初始值)就属于静态成员。类的成员(变量/方法)默认都是动态成员。每一个对象的变量和方法都会在仿真时开辟新的空间。

仿真刚开始:
仿真开始以前
仿真开始后:
仿真开始后
看到这个对句柄和对象应该稍微熟悉那么一点了,通过下面的代码来加强一下。

class paket_c ;
	integer command ; 
	function new (int inival ) ; //当系统未添加function时,系统会自动补全function。
		command = inival;
	endfunction
endclass

module tb2 ;
	initial begin 
		packet_c c1;
		for(int i =1 ;i<=100, i++) begin
			c1 = new(i) ; //创建了100个实例因为调用了100次new()函数,最终c1显示最后一个实例,之前的实例找不回来。当一个对象不再有句柄指向它时,这个对象会被回收。
			$display("c1.command = %0d" , c1.command) ;
		end
	end
endmodule
  • 这段代码共创建了100个实例,因为调用了100次new()函数。
  • 当一个对象不再有句柄指向它时,这个对象会被回收,最终c1显示最后一个实例,之前的实例找不回来。

那这段代码做了几次例化呢?

module tb3;
	pocket_c c1, c2;
	initial gegin : 
		$display("c1.command = %0d" , c1.command) ;
	end
endmodule
//上述中c1只是句柄,并没有空间更没有值,所以索引不到。
module tb3;
	pocket_c c1, c2;
	initial gegin : 
		c1 = new(10);
		$display("c1.command = %0d" , c1.command) ;
		c2 = c1;
		$display("c2.command = %0d" , c2.command) ;
	end
endmodule

答案是只有1次。调用了几次new()函数,例化了几次。 c2 = c1 这个赋值让c1,c2两个句柄指向了同一个对象
仿真图
看到这儿大家会不会有这样的疑问:module储存数据可以直接声明并赋值,class只有例化后才能储存数据吗?

class paket_c ;
	integer command ; 
	static data = 1;// static静态。如果多个对象需要共享一个成员(变量/方法),可以通过添加关键字static。这样访问该成员时,无需进行例化。

	function new (int inival ) ;
		command = inival;
	endfunction
endclass

module tb3;
	pocket_c c1, c2;
	initial gegin : 
		$display("packet static data is %0d" , packet_c::data) ;
		$display("c1.data = %0d, c2.data = %0d" , c1.data, c1.data) ;
	end
end

下面图可以看出在仿真一开始data就已经有了初始值。但此时c1,c2没有调用new()函数并没有例化啊?c1.data,c2.data为什么也能索引出来?在这里插入图片描述

这就是static的作用,类的空间里有单独存放静态变量的位置,无论在仿真的任何时候都不会被释放。所以可以索引出来data

2.2 this关键词

  • 类的封装经常会使用一个关键词this 。
  • this可以用来索引当前所在对象的成员(变量、参数、方法)
  • this只可以用在类中的非静态成员、约束和覆盖组
  • this的使用可以明确指向变量的作用域。

上面几句话可以结合下面的代码来理解

class Demo;
	integer x ;//成员变量x1
	function new (integer x) ;//参数x2
		int x;//称它为变量x3
		x = x ; //此时的x采用的是就近原则,等同于x3 = x2。
		this.x = x ; //此时的x只能是x1(类中的变量x而不是方法中的变量)。
	endfunction
endclass

代码中出现了很多个x,这时候对变量赋值:x = x ,非常容易混淆。使用this可以避免当写了很多行代码时,找不到想要的那个变量x

2.3 赋值和拷贝

  • 声明句柄和创建对象可以分两步也可以一步完成。
	Packet p1 ;
	p1 = new();
	Packet p1= new()
  • 如果将p1赋值给另外一个变量p2,那么依然只有一个对象,只是有两个句柄指向了这个对象。
  • 可以在创建p2的同时将p1拷贝。
	Packet p1 ;
	Packet p1 ;
	p1 = new ;
	p2 = new p1;//浅拷贝
  • 对于深拷贝举个例子简单了解下,ABCD四个人,AB是邻居,CD是邻居。
    A找他的邻居B蹭饭,C浅拷贝A,C也会去找B吃饭而如果C深拷贝A,C会找他的邻居D蹭饭。

目前用到的大多数拷贝都是浅拷贝,类在句柄中可能会遇到深拷贝。在这就不多赘述了,大家了解概念就好了。

2.4 数据的隐藏

  • 类的成员默认情况下是公共属性的,对于类本身和外部均可访问该成员。
  • 类的提供方会限制一些类成员的外部访问,隐藏类成员的更多细节。
  • 这样做类的外部访问接口更为精简,使类的测试和维护变得简单起来。
  • 关键词local 只有该类可以访问,关键词protect该类和子类均可以访问。

在一般情况下不会使用到类的隐藏,避免给我们带来不必要的麻烦,简单了解即可

3. 类的继承

类的三要素类的封装、类的继承和类的多态(虚方法)

  • 龙生龙,凤生凤,对于class也一样。
  • 上述代码中定义的Packet,可以扩展为一个新的类LinkedPacket。
  • 通过extends,LinkedPacket继承其父类Packet的所有成员(变量和方法)。

上边两句客套话不足以道出精髓,看完下面的代码想不懂都难。

class packet;
	integer i = 1;//定义变量integer i并赋值。当被例化时,先执行i=1,接着会被new()函数中的i = 2 替换掉,所以最终i =2;
	function new() ;
		i = 2 ;
	endfunction
endclass

class linkedpacket extends packet ; 
endclass

module tb ;
	initial begin
		packet p = new() ;
		linkedpacket lp = new() ;
		$display ("p.i = %0d" , p.i) ; //i = 2
		$display ("lp.i = %0d" , lp.i) ;//i = 2
	end
endmodule
  • 定义变量了integer i并赋值1。当被例化时,先执行i=1,接着会被new()函数中的i = 2 替换掉,所以最终i =2。
  • 对于lp这个子类会先在自己中搜索, 搜索不到会回到父类中搜索。

总结来说就是子类在没钱的时候,会去找父类要。那子类有钱还会找父类吗?接着往下看。

class packet;
	integer i = 1;//定义变量integer i并赋值。当被例化时,先执行i=1,接着会被new()函数中的i = 2 替换掉,所以最终i =2;
	function new() ;
		i = 2 ;
	endfunction
endclass
class linkedpacket extends packet ; 
	integer i = 3 ;
endclass

module tb ;
	initial begin
		packet p = new() ;
		linkedpacket lp = new() ;
		$display ("p.i = %0d" , p.i) ; 
		$display ("lp.i = %0d" , lp.i) ;
	end
endmodule
  • 子类lp自己定义了变量 i = 3 ;结果为3。
  • 看的出来子类此时并没有找父类,但父类的i = 2 其实在运行的时候已经被继承了,但在不同的域中。

如果此时在子类中添加function,结果还是3吗?

class packet;
	integer i = 1;
	function new() ;
		i = 2 ;
	endfunction
endclass
class linkedpacket extends packet ; 
	function new()
	endfunction
endclass

module tb ;
	initial begin
		packet p = new() ;
		linkedpacket lp = new() ;
		$display ("p.i = %0d" , p.i) ; 
		$display ("lp.i = %0d" , lp.i) ;
	end
endmodule
  • 这段代码在子类定义了空白的new()函数,结果为lp.i = 2
  • 子类的new()默认调用了父类的new()。

在上面的代码里,子类虽然定义了new(),但是空的,如果不是空的,结果会不会有所不同?

class packet;
	integer i = 1;
	function new() ;
		i = 2 ;
	endfunction
endclass
class linkedpacket extends packet ; 
	function new()
		i = 3 ;
	endfunction
endclass

module tb ;
	initial begin
		packet p = new() ;
		linkedpacket lp = new() ;
		$display ("p.i = %0d" , p.i) ; 
		$display ("lp.i = %0d" , lp.i) ;
	end
endmodule
  • 在子类的new()函数n中定义了i=3,结果i = 3 。
  • 子类先调用父类new()函数,再回到自己的new()函数中,对父类的i =2进行了覆盖。

那是不是所有的方法都会默认调用父类同名的方法呢?在下面代码中都定义了方法shift,来看shift会不会被调用。

class packet;
	integer i = 1;
	function new() ;
		i = 2 ;
	endfunction
	function shift() ;
		i = i<<1 ;//往左移一位
	endfunction
endclass
class linkedpacket extends packet ; 
	function new()
		i = 3 ;
	endfunction
	function shift() ;
		i = i<<2 ;
	endfunction
endclass

module tb ;
	initial begin
		packet p = new() ;
		linkedpacket lp = new() ;
		$display ("p.i = %0d" , p.i) ; 
		$display ("lp.i = %0d" , lp.i) ;
		p.shift() ;
		$display ("p.i = %0d" , p.i) ; 
		lp.shift() ;
		$display ("lp.i = %0d" , lp.i) ;
	end
endmodule
  • 结果表面子类并不会调用父类的shift函数,但是会强制调用new函数。
  • 当new函数有参数时,运行程序会报错。子类需在function中添加super.new()。
  • 子类的方法名可以与父类同名,想继承父类,调用super.方法。
  • super.只能只能只能只能出现在类中
  • 为了不必要的麻烦,子类的变量尽量不要与父类变量同名

把上边例子中的变量写成不同的名字会变得很简单,像下面这段代码一样简单。下面代码中tmp.i等于多少?tmp.k等于多少?

class packet;
	integer i = 1;
	function new() ;
		i = 2 ;
	endfunction
endclass

class linkedpacket extends packet ; 
	integer k = 5; 	 //子类中k = 5,
	function new()
		i = 3 ;
	endfunction
endclass

module tb ;
	initial begin
		packet p = new() ;
		linkedpacket lp = new() ;
		packet tmp ; //声明了父类句柄tmp
		tmp = lp ; //将子类的句柄赋值给父类
		$display ("p.i = %0d" , p.i) ; 
		$display ("lp.i = %0d" , lp.i) ;// 子类i = 3 ,父类i =2 
		$display ("tmp.i = %0d" , tmp.i) ;
		$display ("tmp.k = %0d" , tmp.k) ;
		
end

在上述代码,将子类句柄赋值给了父类句柄,此时两个句柄都指向同一个子类的对象,但是父类的句柄此时不能访问子类对象的内容。

在这里插入图片描述

  • 子类的句柄可以访问自己,也可以访问父类,子类的访问空间很大,父类句柄只能访问父类,空间相对小。
  • 当子类句柄赋值给父类句柄后,把大空间变成小空间,还是只能访问父类。tmp = lp ;子类赋值给父类。
  • 而父类的句柄不可以直接赋予子类的句柄,编译认为 小空间赋值给大空间是不安全的,所以会报错。lp2 = tmp ; 父类赋值给子类
  • 当父类句柄指向子类的对象的时候,可以将父类句柄赋值给子类。方法为:$cast(子类,父类)。

4. 包的使用package

把相关的类、方法变量结构体弄到一个箱子里。

  • 两个package尽量不要重名,里边的类型尽量不要重名。
  • 在package中可以包含的类型为软件类型,类结构体方法这些,module、interface除外。

通过下面的代码来了解包的使用。

package pkg_a ;//声明了两个包,里边包含了class,struct和数据
	class packet_a; //类
	endclass
	typedef struct { // 结构体
	int data;
	int command;
	} struct_a;
	int va = 1;
	int shared = 10;
endpackage

package pkg_b ;
	class packet_b;
	endclass
	typedef struct {
	int data;
	int command;
	} struct_b;
	int vb =2 ;
	int shared = 20;
endpackage

module mod_a;//声明了两个空白的模块
endmodule 
module mod_b;
endmodule

module tb;//在这个module也定义了class,struct
	class packet_tb;
	endclass
	typedef struct {
	int data;
	int command;
	} struct_tb;
	mod_a ma();//module的例化
	mod_b mb();
	//使用包的时候必须要import,不然的话系统不知道去哪里找你要的参数,就是不知道包里边有它需要的东西,你得import告诉他,你这里有,让他来取。
	//包的声明:这四行的意思是将包a中的packet_a,va,包b中的packet_b,vb分别导入到这个模块。
	import pkg_a::packet_a;
	import pkg_b::packet_b;
	import pkg_a::va;
	import pkg_b::vb;
	//包的声明:这两行的意思是将两个包中的所有内容全部导入到这个module中,这样做很简单,但占得内存相对而言就很大了。
	import pkg_a::*;
	import pkg_b::*; 
	//两个包中都含有shared,编译器不知道先从a索引还是先从b索引会报错。
	
	initial begin
		pkg_a::packet_a pa = new() ;
		pkg_b::packet_b pb = new() ;
		packet_tb ptb = new() ;
		$display("pkg_a::va = %0d, pkg_b::vb = %0d" 
		,pkg_a::va,pkg_b::vb = %0d)
		$display("shared = %0d," shared); 
		$display("shared = %0d," pkg_a::shared); 
	end
endmodule
  • 首先定义了两个package,里边有class,struct,数据。接着定义了两个空白的module。通过import来完成package的导入,最后打印一些参数。
  • package是一个域,tb中的import对于package来说是必须的,不然的话系统不知道包里边有它需要的东西,package需要import告诉系统。package这里有,让系统来取。
  • import pkg_a::packet_a;将包中的packet_a导入module。
  • import pkg_a:: *;将包中的所有内容全部导入module,这样做很简单,但占得内存相对而言就很大了。
  • 使用::来索引域中的东西

下段代码在tb中添加了 packet_a ,packet_b,与package中的内容相同,此时会咋样呢?

module tb;
	class packet_tb;
	endclass
	typedef struct {
	int data;
	int command;
	} struct_tb;
	mod_a ma();//可以识别,编译通过
	mod_b mb();
	//定义了与package中相同名字的class。
	class packet_a;
	int tb_a;
	endclass	
	class packet_b;
	int tb_a;
	endclass


	import pkg_a::*;
	import pkg_a::*; //当类型找不到的话会去上述包中含的类中寻找,但如果这个module含有同名的类就不会去包中寻找类。
	
	initial begin
		packet_a pa = new() ;
		packet_b pb = new() ;
		packet_tb ptb = new() ;
		$display("pkg_a::va = %0d, pkg_b::vb = %0d" ,pkg_a::va,pkg_b::vb = %0d)
	end
endmodule

运行结果说明了当module中含有他需要的变量就不会去包中寻找了

5. 随机约束

仿真时,定向测试是无法完整的检查设计功能的完整性也无法预测用户在用产品时遇到的问题。这时候就引入了随机约束测试,也就是随机约束。经常会接触到测试向量这个东西,在这做一个简单介绍。

  • 测试向量:产生什么样的随机数据,激励是快还是慢,产生正确还是错误的时序。

5.1 产生随机数

module stim;
	bit[15:0] addr ;
	bit[31:0] data ;
	function bit gen_stim();
		bit success, rd_wr;
		success = randomize(addr, data, rd_wr);
		return rd_wr ;
	endfunction
endmodule
  • 通过调用系统函数std::randomize()完成随机化。(如果是class产生随机数,则可以不加std,如c.randomize)
  • $ urandom(),生成一个32位的无符号数, $ random(),生成32位有符号数。
  • $urandom()_range(maxval ,minval = 0),生成这之间的一个数。

5.2 约束

  • 上述随机数是独立的,添加一些"约束"使变量朝着希望他们变化的方向去随机。
  • 这些约束会对变量与变量之间的关系发生变化。

一般使用类这个载体来容纳这些变量与他们之间的约束

  • 类的成员变量均可声明为“随机”属性,用rand或randc来表示。
  • rand和randc
    假如rand和ranc在扑克牌中随机抽一张,rand第一次抽取了红桃A,下一次抽取会在一副新的扑克牌中抽取。randc第一次抽取了红桃A,下一次抽取会在剩下的扑克牌中抽取,直到整幅牌抽完才有可能再次遇见红桃A。
  • 任何类中的整形(bit/byte/int)变量都可以声明为rand/randc。
  • 定长数组、动态数组、关联数组、队列也可以rand/randc。
  • 指向对象的句柄也可以声明为rand,不能用randc
  • 实数不能做随机化,因为消耗资源太大。

下面的代码分别在在class、struct和moduloe中产生随机数并做约束,看完就通透。

  1. 在class中产生随机值并做约束:
//在class中做约束
class packet ; 	//定义类
	 rand bit[31:0] src, dst, data[4];//随机变量
	 rand bit[7:0] kind;//随机变量
	 constraint cstr{ //约束
		 src>10;
		 src<15;
	 }
	 function void print() ; //打印值
 		$display("src is %0d\n dst is %0d\n kind is %0d\n data is %p" ,src,dst,kind,data);
	 endfunction
 endclass
 
 module tb;
 	packet p;//1.声明句柄
 	initial begin
 		p = new();/2./例化
 		$display("before randomize");
 		p.print();//此时值为初始值还没有随机赋值
 		p.randomize();//3.随机化
 		$display("after randomize");
 		p.print();//执行.randomize后有了随机值
 	end
 endmodule

class中的变量声明了rand,只有调用了.randomize才会产生随机值

  1. 在结构体中产生随机值并做约束:
//在结构体中做约束·
 typedef struct{ 
	 rand bit[31:0] src;
	 rand bit[31:0]  dst;
	 rand bit[31:0] data[4];
	 rand bit[7:0] kind;
 }packet_t;
 
 module tb2;
 	packet_t pkt;//例化结构体
 	initial begin
 		pkt.randomize();	//只有在类里可以使用randomize,此行错误
 		
 		std::randomize(pkt) ;
 		std::randomize(pkt) with{ pkt.src >10; pkt.src<15};//嵌入约束
 	end
 endmodule
  • struct中也可以使用rand。

  • .randomize只能在类中直接调用,在struct中,需要通过std::randomize()调用randomize。

  • struct的约束方法为在initial中使用嵌入约束。

 module tb3;
	bit[31:0] src;
	bit[31:0]  dst, 
	bit[31:0] data[4];
	bit[7:0] kind;
 	initial begin
 		std::randomize(src,dst,data,kind);
 		src = $urandom();
 		dst = $urandom();
 		data[0]= $urandom();
 		kind = $urandom();

 	end
 endmodule
  • module中也可以生成随机数,但结构不那么清晰明了,而且失去了很多约束。

  • 从class到struct到$urandom都可以生成随机数,但变得越来越复杂,而且失去了约束 。

产生随机数有上述方法,但生成带有约束的随机数,最后放到一个容器里。这个容器只能是类。

5.3 权重分布

可以在取值的同时设置随机时的权重

  1. := 操作符表示每一个值的权重是相同的。
  2. :/操作符,权重会平均分配到每一个值。

x在100,101,102,200,和300的权重是1 1 1 2 5

x dist { [100:102 ] : =1, 200:= 2, 300:= 5 };

x在100,101,102,200,和300的权重是1/3 1/3 1/3 2 5

x dist { [100:102 ] :/1, 200:= 2, 300:= 5 };
  1. unique可以约束一组变量,使不会出现相同的值。
  2. 使用if-else或者->操作符表示条件约束,如下段代码。
 mode == little  ->  len <10;
 mode == big-> len>100;

if(mode ==little)
	len<10;
else if(mode ==big)
	len>100;
  • foreach可以用来迭代约束数组中的元素。
  • 没有soft描述时的约束称为硬约束,两个硬约束会导致仿真失败。添加了soft后,两个约束冲突,硬约束生效。
module tb;
	ckass packet_a;
		rand int length;
		constraint cstr{ length inside {[5:15 ]};}//父类中的约束
	endclass	
	ckass packet_b extends packet_a;
		constraint cstr{ length inside {[10:20 ]};}//子类中的约束
	endclass
	initial begin
		packet_b pkt = new();//类的例化
		if(pkt.randomize())//成功随机的话
			$display("pkt.length is %0d" , pkt.length);
		else
			$display("randomize failurel");
	end
endmodule

上述代码的解在什么范围?或者说有没有解?

  • 答案是值在[10:20],程序中调用的是子类的cstr,此时子类的cstr会覆盖掉父类cstr。
  • 当子类父类约束是两个不同的名字时,子类会继承父类,[5:15],[10:20]两个条件会同时满足。

此时如果修改子类范围为[16:20],randomize失败,可以在length前添加soft变成软约束,此时会成功运行。关于soft可以参考下面代码:

module tb;
	ckass packet_a;
		rand int length;
		constraint cstr{ soft length inside {[5:15 ]};}
	endclass	
	ckass packet_b extends packet_a;
		constraint cstr{ soft length inside {[10:20 ]};}
	endclass
	initial begin
		packet_b pkt = new();
		if(pkt.randomize() with {soft length inside{21:25};})//内嵌约束
			$display("pkt.length is %0d" , pkt.length);
		else
			$display("randomize failurel");
	end
endmodule

这段代码将子类父类中的约束改为了soft,并且增加了内嵌soft约束,此时执行就近的soft约束。

  • 硬约束必须满足,软约束能满足满足不能满足算了。

一段代码中有很多变量,自然就会遇到索引不到想约束的变量。如下面代码所示:

class c1;
	rand integer x;//1
endclass

class c2;
	integer x;//2
	integer y;
	task doit (c1 f ,intefer x,integer z);//这行的x序号定为3
		int result ;
		result = f.randomize() with{ x <y+z;};
	endtask
endclass
  1. 代码中内嵌的约束条件是 x< y+z; y、 z 都很好找到,但上述代码中x 出现了三次,约束的是哪个x呢?

这时候注意f.randomize(),f是c1的句柄。所以此时约束的x是c1中的x,一定要注意。

  1. 那现在有没有可能指向第2个x 呢?

你可能会想到直接使用this.索引,

result = f.randomize() with{ this.x <y+z;};

如果没有f这个句柄,那他指向的是2没错,但引入了c1的句柄f,这个this.x跑到了c1这个类中了。正确答案是local::。

  1. 通过local::域索引的方式来实现
result = f.randomize() with{local::this.x<y+z;};//2
result = f.randomize() with{local::x <y+z;};//3

5.4 随机控制

  • rand_mode可以用来使能或禁止随机变量。
  • 当随机数被禁止时会变成普通变量,不再参与随机化的过程 可以对单个随机变量使用,也可以对整个对象使用来控制所有的随机变量。
packet_a.rand_mode(0);
packet_a.source_value.rand_mode(1);\
constraint_mode可以实现关闭约束。
function int object.constraint identifier::constraint_mode;

5.5 内嵌变量控制

在使用类的随机化函数randomize()时,如果伴有参数,那么只会随机化这些变量。下面的代码可以很好的解释这句话。

class c;
	rand byte x,y;
endclass
	c a = new;
	a.randomize(); //xy都会参与到随机
	a.randomize(x);//只有x参与随机,y不会随机

5.6 线程控制

与顺序线程begin…end 相对应的并行语序fork…join。

在这里插入图片描述

  • fork…join所有的并行程序都结束才会进行下一步。
  • fork…join_any任何一个行程结束就会退出进行下一步,但其他线程会在后台执行,如果你手动没有关闭。(disable fork)
  • fork…join_none只启动线程不需要等待线程完成,然后退出fork。(线程在后台执行)
  • 关键词wait fork:在一个begin end 里的所有fork 必须运行完毕后再开始下一步。
  • 关键词disable fork:也就是上边说的手动关闭,有一个线程结束了其他线程不再允许执行。

6. 线程间的同步和通信

测试平台所有数据都需要相互传递或者同步信息。方式主要包括event事件 、semaphore旗语 和mailbox信箱。

6.1 event

  • 通过event来声明一个命名为event变量,并且去触发他。
  • event可以控制命名的进程。
  • 可以通过->触发事件。
  • 通过@操作符或者wait()来检测event的触发状态,@表明边沿触发,wait表明电平触发。当一个事件先发生的同时@事件可能采集不到那个时刻的上升沿,此时会导致阻塞。所以当事情发生在前,检测触发一般使用wait() 。
  • 关键词wait_order(a, b, c); 从左到右按顺序一个一个执行。

通过下面这段代码,理解上边的信息。

module tb;
	event e1, e2, e3; //声明
	task wait_event(event e, string name);
		$display("@%t start waiting event %s" , %time, name );
		@e; //等待事件被触发或者使用wait(e);
		$display("@%t finish waiting event %s" , %time, name );
	endtask
	
	initial begin
		fork
			wait_event(e1, "e1");
			wait_event(e2, "e2");
		 	wait_event(e3, "e3");
		join
	end

	initial begin
		fork
			begin #10ns->e1 ; end;//触发事件
			begin #20ns->e2 ; end;	
			begin #3 0ns->e3 ; end;
		join
	end
endmodule

声明事件后,再给出触发条件就可以使用了。

6.2 semaphore

  • 从概念上讲类似于容器。

  • 在创建旗语时会为其分配钥匙。

  • 使用旗语必须有钥匙。

  • 钥匙可以有很多个,等待钥匙的进程也可以有很多个。

可以理解为为了保护共享的资源,上了防盗锁并发了钥匙。发几把钥匙你说了算,必须有钥匙才能访问,用完钥匙需要归还。具体用法如下代码。

尝试获取一个或多个钥匙而不会阻塞try_get(n=1),当返回0时表示没有足够的钥匙,当返回正数表示有钥匙继续运行。

module tb;
	semaphore mem_a;
	int unsigned mem[int unsigned];//定义数组存放32位的数据
	//write
	task automatic write( int unsigned addr ,int unsigned data)
		mem_a.get();//取钥匙
		mem[addr] = data;
		mem_a.put();//还钥匙
	endtask
	//read
	task automatic read( int unsigned addr ,output int unsigned data)
		mem_a.get();
		if(mem.exists(addr)) //如果数组中有数据,取出数据
			data = mem[addr];
		else
			data ='x';
		mem_a.put();
	endtask
	//运行读写
	initial begin
		int unsigned data = 100;
		mem_a = new(1); //定义刚开始一把钥匙
		forever begin
			fork
				begin
					write('h10, data+100); //往这里写数据mem[addr] = data
				end
				begin
					read('h10, data);
				end
			join
	end
endmodule

写和读在forkjoin里并行执行,如果write先拿到钥匙,read只能等write还钥匙,再拿钥匙。这样就解决了两个并行的程序先后运行的目的。

6.3 mailbox

  • 信箱可以使进程之间交换信息, 创建信箱时可以限定或者不限定大小
  • 创建信箱:new()
  • 将信息写入信箱:put()
  • 试着写入信箱:try_put()
  • 获取信息:get()同时会取出数据,peek()不会取出数据。peek()也是取数据,get()拿出数据并丢掉,peek()只看一眼并不会拿出来。
  • 试着从信箱取出数据但不会阻塞:try_get()/try_peek()
  • 获取信箱信息的数目:num()
module tb;
	mailbox #(int) mb;//只能存放int型的信箱mb
	
	initial begin
		int data 
		mb = new(8); //声明了信箱的大小
		forever begin
			case($urandom()%2)//$random%b b为一个大于0的整数,表达式给出了一个范围在[-b+1 : b-1]之间的随机数;urandom去掉了符号
				0: begin 
					data = $urandom_range(0,10);//系统随机化调用函数,返回指定范围内的无符号随机整数
					mb.put(put) ;
				end;
				1:begin  
					if(mb.num()>0)begin
					mb.get(data);
				end
			endcase
		end	
	end
endmodule
  • mailbox #(int):只能存放某种类型的mailbox。

这段代码给信箱中放数,当放满的时候,如果继续放数,这个数就会一直等待信箱的空位置。那么就会造成拉顿。如果把put(),get(),换成try_put(),和try_get()就会减少这样的卡顿。

7. 虚方法 virtual

类的三要素类的封装、类的继承和类的多态(虚方法)

  • 类的成员方法可以virtual(虚方法)。变量不可以使用。
  • 虚方法是一种基本的多态结构。
  • 父类和子类中声明的虚方法,方法名、参数名参数方向都应该保持一致。
module tb;
	//定义父类
	class basepacket;
		int a =1;
		int b = 2;
		function void printa;
			$display("basepacket::a is %d" , a);
		endfunction:printa
		virtual function void printb;//b为虚方法
			$display("basepacket::b is %d" , b);
		endfunction:printb
	endclass:basepacket
	//定义子类
	class mypacket extends basepacket;
		int a =3;
		int b =4;
		function void printa;
			$display("mypacket::a is %d" , a);
		endfunction:printa
		virtual function void printb;//虚方法
			$display("mypacket::b is %d" , b);
		endfunction:printb
	endclass: mypacket

	basepacket p1 = new();
	mypackey p2 = new(); 
	initial begin
		p1.printa;//1
		p1.printb;//2
		p1 = p2;//子类句柄赋值给父类
		p1.printa;//1
		p1.printb;//4
		p2.printa;//3
		p2.printb;//4 虚方法
	end
endmodule
  • 子类句柄赋值给父类,父类句柄访问的空间只有父类的空间,上文说过。

  • 当出现虚方法时,父类会去子类中找有没有同名的方法,如果有则会采用子类的方法。

8. 类型转化

转换时指定目标类型并在表达式前加(')

int i;
real r;
i = int '(10.0 - 0.1);
r = real'(42);

动态转换
子类的句柄可以直接赋值给父类的句柄,当父类的句柄指向子类的对象时候,$cast()系统函数可以将父类句柄转化为子类句柄。

  • 父类句柄想访问子类的方法可以通过虚方法动态查找来解决。
  • 父类的句柄访问子类的变量不可以直接访问,只能把父类的句柄转化为子类的句柄再访问。

到这里有关class的一些知识点想必大家已经通透了,最后再强调以下几点。

  1. 在类的封装中尽量不要添加local。
  2. 在类的继承中父类子类中尽量不要出现同名的变量,但是同名的方法经常会遇到。
  3. 在多态中,用不同类型句柄调用子类同名方法时应该在最开始的父类声明virtual。

可能对您有帮助的参考:

保姆级超硬核包会,​System Verilog SV接口(interface )
https://blog.csdn.net/jackack/article/details/127215204
保姆级超硬核包会,System Verilog SV数组
https://blog.csdn.net/jackack/article/details/127219379

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值