SystemVerilog学习笔记4——OOP、句柄、对象、静态变量/方法、类、包


类和对象的概述

  • 类是将相同个体抽象出来的描述方式,对象是实体,其具备有独立行为的能力具有相同属性和功能的对象属于同一类,不同类之间可能有联系(继承)或没有联系;
  • C语言中,编程基于过程方法,在Verilog中提供了笨拙的类对象编程可能性,即在module中定义方法,而后调用module实例中的方法;
  • Verilog的module+method的方式与SV的class定义有本质差别,即面向对象编程三要素:封装、继承、多态(Encapsulation、Inheritance、Polymorphism);
  • 类的定义核心是属性声明和方法定义,所以类是数据和方法的自洽体,即可以保存数和处理数据。这是与struct结构体在数据保存方面的重要区别,因为结构体只是单纯的数据集合,而类可以对数据做符合需要的处理。

验证为何需要OOP?

激励生成器(stimulus generator):生成激励内容;

  • 驱动器(driver):将激励以时序形式发送至DUT;
  • 监测器(monitor):监测信号并记录数据;
  • 比较器(checker):比较数据;
  • 验证环境的不同组件功能和所需要处理的数据内容是不相同的。
  • 不同环境同一类型的组件其所具备的功能和数据内容是相似的。
  • 验证世界的各组件角色明确、功能分立,使用面向对象编程与验证世界的构建原则十分符合。

OOP概念要素

Class类:基本模块包括成员变量和方法。在Verilog中module也可以包含变量和方法,只不过它是“硬件盒子”,class是“软件盒子”

  • Object对象:类的实例。Verilog中module也可以例化,这是“硬件”例化,在SV中可以使用class来例化,这是“软件”的例化
  • Handle句柄(指针):用来指向对象的指针。在Verilog中可通过层次化的索引找到结构中的设计实例,而SV的对象索引需通过句柄来索引对象的变量和方法
  • Property属性(变量):在类中声明的存储数据的变量。在Verilog中可以是wire或者reg类型;
  • Method方法:类中可以使用task或function定义方法以便处理自身或外部传入的数据。Verilog中可以在module中定义task/function,也可使用initial/always处理数据。

Verilog的例化和SV的class例化的差别:

  • 共同点在于使用相同的“模板”来创建内存实例
  • 不同点在于Verilog的例化是静态的,即在编译链接时完成,而SV中class例化是动态的,可以在任意时间点发生
  • Verilog中没有句柄的概念,即只能通过层次化的索引方式A.B.sigx,而SV中的class通过句柄可以将对象的指针赋予其它句柄,使得操作更加灵活。

创建对象开辟新的内存空间用来存放新的成员变量和方法;可通过自定义构建函数完成变量的初始化和其他初始操作;构建函数new()是系统预定义函数,不需指定返回值,函数会隐式返回例化后的对象指针,可定义多个参数作为初始化时外部传入数值的手段

Transaction tr;		//声明句柄
tr = new();			//创建对象
class Transaction;
	logic[31:0] addr = 'h10;
	logic[31:0] crc, data[8];
	function new(logic[31:0] a=3, d=5);
		addr = a;
		foreach(data[i])
			data[i] = d;
	endfunction
endclass
initial begin
	Transaction tr;
	tr = new(10);
//结果:tr.addr的值为10

句柄的传递

对象是指的是存储空间,而句柄指的是空间指针。也就是在创建了对象之后,该对象的空间位置不会改变,而指向该空间的句柄可以有多个

Transaction t1, t2;		//声明句柄
t1 = new();				//例化对象,将其句柄赋予t1
t2 = t1;				//将t1的值赋予t2,即t1和t2指向同一个对象
t1 = new();				//例化第二个对象,并将其句柄赋予t1
//看到一次new()就是例化了一次对象

在这里插入图片描述

对象的销毁

软件编程的灵活在于可以动态地开辟使用空间,在资源闲置或不再需要时,可以回收空间,这样使得内存空间保持在一个合理的区间。

  • SV中采用了自动回收空间的处理方式,自动回收空间的基本原理是,当一个对象在整个程序中没有任何一个地方再“需要”它时便会被“销毁”。这里的“需要”的意思指的是有句柄指向该对象

句柄的使用:句柄可用来创建多个对象,也可前后指向不同对象

Transaction t1, t2;	//声明句柄
t1 = new();		//创建对象并将其指针赋予t1
t2 = new();		//创建对象并将其指针赋予t2
t1 = t2;		//将t2的值赋予t1,t1和t2指向同一对象,t1之前指向的对象被释放
t2 = null;		//将t2赋值为空,此时指针悬空,悬空的指针很危险

通过句柄来使用对象中的成员变量或成员方法

Transaction t;
t = new();
t.addr = 32'h42; //对象的成员变量赋值
t.display(); //调用对象的成员方法

静态变量

  • 与硬件域不同,在class中声明的变量默认为动态变量,即其生命周期在仿真开始后的某时间点开始到某时间点结束,始于对象创建,终于对象销毁
  • 使用关键字static声明class内的变量时,则其为静态变量。静态变量的生命周期开始于编译阶段,贯穿于整个仿真阶段
  • 如果在类中声明静态变量,可直接引用该变量class::var,或通过例化对象引用object.var。类中的静态变量声明以后,无论例化多少个对象,只可以共享一个同名的静态变量

静态方法

  • 在class中定义的方法默认是动态方法,也可通过关键字static修改为静态方法;
  • 静态方法内可以声明并使用动态变量,但不能使用类的动态成员变量。因为调用静态方法时可能并没有创建具体对象,故没有为动态成员变量开辟空间,因此在静态方法中使用类的动态成员变量是禁止的,可能会造成内存泄漏,但静态方法可以使用类的静态变量,因为静态方法同静态变量一样在编译阶段就已经分配了内存空间。
class Transaction
	static Config cfg; //创建静态变量
	static int count = 0;
	int id;
	//通过静态方法来操作静态变量
	static function void display_statics();
		$display("Transaction cfg.mode=%s,count=%0d",cfg.mode.name(),count);
	endfunction
endclass
Config cfg;
initial begin
	cfg = new(MODE_ON);
	Transaction::cfg = cfg; //静态变量赋值
	Transaction::display_statics(); //调用静态方法
end  //即使类里没做过任何静态对象的创建,仍可索引其成员方法或变量

类的成员

  • 类是成员变量和成员方法的载体,之所以称之为自洽体,是因为其变量和方法应符合“聚拢”原则,即一个类的功能应尽可能简单,不应承担过多的职责,更不应承担不符合它的职责,这在设计模式中称为单一职责原则
  • 类作为载体,也具备天生的闭合属性,即将其属性和方法封装在内部,不会直接将成员变量暴露给外部,通过protected和local关键字来设置成员变量和方法的外部访问权限。所以封装属性在设计模式中称之为开放封闭原则
  • 如果没有指明访问类型,那么成员默认是public子类和外部均可以访问成员
  • 如果指明访问类型是protected,那么只有该类或者子类可以访问成员,而外部无法访问;
  • 如果访问类型是local,那么只有该类可以访问成员,子类和外部均无法访问;
  • 访问类型的设定是为了更好地封装类,尤其是要发布供他人使用的软件包。
class clock;
	local bit is_summer = 0;
	local int nclock = 6;
	function int get_clock();
		if(is_summer == 0)
			return this.nclock;
		else
			return this.nclock + 1;
	endfunction
	function bit set_summer (bit s);
		this.is_summer = s;
	endfunction
endclass
clock ck;
initial begin
	ck = new();
	$display("now time is %0d",ck.get_clock());
	ck.set_summer(1);
	$display("now time is %0d",ck.nclock); 
	//会报错,因为nclock加了local,外部句柄访问不了,若无local则为7
end

类与结构体的异同

  • 二者本身都可以定义数据成员
  • 类变量在声明之后需要构造才会创建对象实体,而struct在变量声明时就已经开辟内存
  • 类还可以声明方法,而结构体则不能;
  • 从根本上讲struct是一种数据结构,而class则包含了数据成员以及针对这些成员操作的方法

类与模块的异同

  • 数据和方法定义而言,二者均可作为封闭的容器来定义和存储
  • 例化来看,模块必须在仿真一开始就确定是否应该被例化,可通过generate来实现设计结构的变化;而类的变量在仿真任何时间段都可被构造创建新的对象。即硬件部分必须在仿真一开始就确定下来,module和其内部过程块、变量都应是静态的;而软件部分,即类可以在仿真任何阶段声明并动态创建出新的对象;
  • 封装性来看,模块内的变量和方法是对外开放的,而类可根据需要来确定外部访问的权限是否是默认的公共类型、或者保护类型还是私有类型;
  • 继承性来看,模块没有任何继承性,即无法在原有module上进行新module的功能扩展,唯一可支持的方式只有简单的拷贝和在拷贝的module上做修改,而类可以进行继承

类的相关问题

  • 类可以在module、interface、program和package中定义;
  • 可以在类中再声明类成员,类也是一种数据载体;
  • 如果在类中使用this,即表明this.x所调用成员是当前类的成员,而非同名的局部变量或形式参数;
  • 类也有编译顺序,先编译基本类再编译高级类。或者说先编译将来被引用的类,再编译引用之前已编译类的类(如已编译好的父类的其他子类)。

类的继承

class  cat;
	protected cloor_t color;
	local bit is_good;
	function set_good(bit s);
		this.is_good = s;
	endfunction
endclass
class black_cat extends cat;
	function new();
		this.color = BLACK;
	endfunction
endclass
class white_cat extends cat;
	function new();
		this.color = WHITE;
	endfunction
endclass
black_cat bk;
white_cat wt;
initial begin
	bk = new();
	wt = new();
	bk.set_good(1);
	wt.set_good(1);
end

A、可通过外部修改黑猫的颜色使其变为白猫——✖ color的封装是protected,只有自身及其子类可以访问,所以无法通过外部修改颜色;
B、黑猫可自己在初始化时使用this.is_good=1夸自己是好猫——✖ is_good的封装是local,子类无法访问,黑猫是猫的子类,所以无法修改;
C、外部可通过访问黑猫的is_good属性得知它是不是一只好猫——✖ 外部无法访问,外部只能set_good,要访问得有set_good函数预先在父类或子类定义;
D、至于白毛是不是一只‘大脸猫’,无从得知——✔ 因为没有这个属性。

子类中使用this.xxx先访问子类有没有xxx的变量或者方法,而super.xxx直接访问父类的变量和方法,不会访问子类

示例:在test类中通过继承于basic_test的两个子类test_wr和test_rd,分别用于对DUT进行写测试和读测。

class basic_test; //父类
	int def = 100;//成员变量赋予默认值
	int fin;
	task test(stm_ini ini);
		$display("basic_test::test");
	endtask
	function new(int val);
		...
	endfunction
endclass 
class test_wr extends basic_test;//子类wr
	function new();
		super.new(def);
		$display("test_wr::new");
	endfunction
	task test(stm_ini ini);
		super.test(ini);
		$display ("test_wr::test");
		   ...
	endtask
endclass
class test_rd extends basic_test;//子类rd
	function new();
		super.new(def);
		$display("test_rd::new");
	endfunction
	task test(stm_ini ini);
		super.test(ini);
		$display ("test_rd::test");
		   ...
	endtask
endclass
class test_wr extends basic_test;//子类wr
	int def = 200;
	function new();
		super.new(def);
		$display("test_wr::new");
		$display("test_wr::super.def = %0d",super.def);   //100
		$display("test_wr::this.def = %0d",this.def);     //200
	endfunction
    ...
endclass
  • 类的继承包括继承父类的成员变量和成员方法
  • 子类在定义new函数时应先调用父类的new函数即super.new(),如果父类的new函数没有参数,子类可省略该调用,系统会在编译时自动添加super.new();

从对象创建时初始化的顺序来看,用户应注意有如下规则:

  • 子类的实例对象在初始化时首先会调用父类的构造函数
  • 当父类构造函数完成时,会将子类实例对象中各成员变量按照它们定义时显式的默认值初始化,如果没有默认值则不被初始化;
  • 在成员变量默认值赋予后(声明的同时即赋值),才会最后进入用户定义的new函数中执行剩余的初始化代码;
  • 父类和子类里可定义相同名称的成员变量和方法(形参和返回类型也应相同),在引用时将按照句柄类型确定作用域;
  • 当子类作用域中如果出现同父类相同的变量名或方法名,则以子类作用域为准
module tb;
    ...
	basic_test t; //父类声明句柄
	test_wr wr; //子类声明句柄
	initial begin
		wr = new(); //子类创建对象
		t = wr;  //子类句柄赋值给父类
		$display("wr.def = %0d",wr.def);   //200
		$display("t.def = %0d",t.def);     //100
	end
endmodule

子类句柄赋值给父类之后,被赋值的父类句柄只能指向整个子类中属于父类的部分,如图,t只能指向basic部分,所以t.def = 100,wr.def = 200。但父类的句柄不能赋值给子类
在这里插入图片描述
默认情况下,若无super或this指示作用域,则依照从近到远的原则引用变量:

  • 首先看变量是否是函数内部定义的局部变量;
  • 其次看变量是否是当前类定义的成员变量;
  • 最后看变量是否是父类或更底层类的变量。

句柄的使用

句柄可作为形式参数通过方法来完成对象指针的传递,从外部传入方法内部
在这里插入图片描述
句柄可以在方法内部首先完成修改,而后再由外部完成使用

function void create(Transaction tr);
	//加入ref或inout即可有返回值,默认为input所以悬空
	tr = new();
	tr.addr = 100;
	...
endfunction
Transaction t;	  //此时t=null,为null
initial begin
	create(t);
	t.addr = 10;  //t仍为null
	$display(t.addr);
end //t.addr会报错

句柄的动态修改:在程序执行时,可以在任何时刻为句柄创建新的对象,并将新指针赋值给句柄。

task generate_trans();
	Transaction t;
	Transaction fifo[$];  //存放句柄的队列
	t = new(); //要存放三个对象则将此句置于for循环里,new多少次就创建了多少个实例
	for(int i = 0; i < 3; i++) begin	
	//通过for循环向队列里存放了3个句柄,而不是存放了3个对象
		t.addr = i << 2;
		fifo.push_back(t);
	end
	t = fifo.pop_front();	//从队头取出第一个元素,此时t.addr=8
endtask

在这里插入图片描述

包的使用

包的意义

  • SV语言提供一种在多个module、interface和program之中共享parameter、data、type、task、function、class等的方法,即利用package方式来实现。通常将不同模块的类定义归整到不同package中
  • package将一簇相关的类组织在了单一的命名空间下,使得分属于不同模块验证环境的类来自于不同的package,这样可以通过package来解决类的归属问题

包的定义

package regs_pkg;
	'include "stimulator.sv"  //表示纯文本的替换,放到package里
	'include "monitor.sv"
	'include "chker.sv"
	'include "env.sv"
package arb_pkg;
	'include "stimulator.sv"
	'include "monitor.sv"
	'include "chker.sv"
	'include "env.sv"  //包都默认编译至work library里

将重名的类归属到不同的package中编译,不会发生重名的编译冲突,因为package是将命名空间分隔开来的。使用时需要注明使用哪一个package中的类。

包和库的区分:

  • 尽管不同包中存在同名的类,可在引用类名时通过域名索引::操作符的方式显式指出所引用的类具体来自于哪个package,这样能通过不同名的package来管理同名的类。package这个容器对类名做一个隔离的作用;
  • package更多意义在于将软件(类、类型、方法等)封装在不同命名空间中,以此与全局的命名空间进行隔离。package需额外定义,容纳各种数据、方法和类;
  • library是编译的产物,在没有介绍软件之前,硬件(module、interface、program)都会编译到库中,如果不指定编译库的话会被编译进入默认的库中。从容纳的类型来看,库可容纳硬件类型和软件类型

包的命名规则

  • 在创建package时已经在指定包名称时隐含指定了包的默认路径,即包文件所在的路径。如果有其他要被包含在包内的文件在默认路径之外,需要在编译包的时候加上额外指定的搜寻路径选项“+incdir+PATH”;
  • 如果遵循package的命名习惯,不但要求定义的package名称独一无二,其内部定义的类也应尽可能的独一无二
  • 如果不同package中定义的类名也不相同时,在顶层引用可通过import pkg_name::*形式表示在module mcdf_tb中引用的类,如果在当前域中没有定义的话,会搜寻regs_pkg和arb_pkg中定义的类,又由于它们包含的类名不相同,因此不用担心搜寻中会遇到同名类冲突问题。
package regs_pkg;
	'include "regs_stm.sv"
	'include "regs_mon.sv"
	'include "regs_chk.sv"
	'include "regs_env.sv"
package arb_pkg;
	'include "arb_stm.sv"
	'include "arb_mon.sv"
	'include "arb_chk.sv"
	'include "arb_env.sv"
module mcdf_tb;
	import regs_pkg::*; //*表示可以引用包内的所有类型
	import arb_pkg::*;  //没有这两行会报错,因为mon被package封装起来,即被隔离
	regs_mon mon1 = new();
	arb_mon  mon2 = new();
endmodule

使用包的注意事项

  • 在包中可定义类、静态方法和静态变量
  • 如果将类封装在某一个包中,那么它就不应该在其它地方编译,这么的好处在于之后对类的引用更加方便;
  • 包是类的归宿,类是包的子民
  • 一个完整模块的验证环境组件类,应该由一个定义的模块包来封装;
  • 使用’include的关键字完成类在包中的封装,要注意编译的前后顺序来放置各个’include的类文件
  • 编译一个包的背后实际是将各个类文件“平铺”在包中,按照顺序完成包和各个类的有序编译;
  • 使用类可通过import完成包中所有类或某一个类的导入,使得新环境可以识别出该类,否则类会躺在包这个盒子里不被外部识别。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值