System verilog基础-面向对象编程
类和对象的概述
verilog的module+method的方式与SV的class定义有本质上的区别,即面向对象编程的三要素:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。这一篇章讲解SV中最重要的一个类型-类,所以篇幅会很长。
类的定义核心即是属性声明(property declaration)和方法定义(method definition),所以类是数据和方法的自洽体,即可以保存数据也可以处理数据。这是与struct结构体在数据保存方面的重要区别,因为结构体只是单纯的数据集合,而类则可以对数据做出符合需要的处理。
为什么验证世界中,需要有类的存在?
※ 激励生成器(stimulus generator):生成激励内容。
※ 驱动器(driver):将激励以时序形式发送至DUT。
※ 检测器(monitor):检测信号并记录数据。
※ 比较器(checker):比较数据。
验证环境的不同组件其功能和所需要处理的数据内容是不相同的。
不同环境的同一类型的组件其所具备的功能和数据内容是相似的。
面向对象编程(OOP)术语:
(1)类(class):包含变量和子程序的基本构建块。Verilog中与之对应的是模块(module).
(2)对象(object):类的一个实例。在Verilog中,你需要实例化一个模块才能使用它。
(3)句柄(handke):指向对象的指针。在Verilog中,你通过实例名在模块外部引用信号和方法。一个OPP句柄就像一个对象的地址,但是它保存在一个只能指向单一数据类型的指针中。
(4)属性(property):存储数据的变量。在Verilog中,就是寄存器(reg)或者线网(wire)类型的信号。
(5)方法(method):任务或者函数中操作变量的程序性代码。Verilog模块除了initial和always块以外,还含有任务和函数。
(6)原型(prototype):程序的头,包括程序名、返回类型和参数列表。程序体则包含了执行代码。
Verilog和OOP都具有例化的概念,但是在细节方面却存在着一些差别,一个Verilog模块,例如加法器,是在代码被编译的时候例化的。而一个System Verilog类,例如一个网络数据包,却是在运行测试平台需要的时候才被创建。Verilog的例化是静态的,就像硬件一样在仿真的时候不会变化,只有信号值在变。而System Verilog中,激励对象不断地被创建并且用来驱动DUT,检查结果。最后这些对象所占用的内存可以被释放,以供新的对象使用。
综上:verilog的例化时静态的,即在编译连接时完成,而SV class的例化是动态的,可以再任意时间点发生,这也使得类的例化方式更加灵活和节省空间。

new( )和new[ ]的区别
new( )和设置动态数组大小的new[ ]看起来非常相似。它们都申请内存并初始化变量。两者最大的不同在于调用new( )函数仅创建了一个对象,而new[ ] 操作则建立一个含有多个元素的数组。new( ) 可以使用参数设置对象的数值,而new[ ]只需使用一个数值来设置数组的大小。
定义一个class,并给他初始化。

把tr定义为一个对象,然后用new函数返回给tr一个句柄,在这里做的初始化,a的初值为3,addr默认为‘h10,使用new函数后,因为传入了参数10,默认给第一个a,所以a=10,经过foreach函数,给addr赋值10,所以addr = 10。如果使用new()函数的时候,没有传入数值,那么a就默认为3.
为多个对象分配地址

只要看到一次new函数,那就是例化了一个新的对象。所以看到上面代码的第四行,就应该知道已经例化了两个对象。

对于上面这个开辟空间和释放的例子,请问在1ps的时候,总共为word开辟了多少的存储空间,假设每创建一次,开辟1B空间?1B,开辟了四次个对象,最后wd指向第四个对象,前三个对象都没有句柄指向,所以前三个对象销毁了,所以是1B。

静态变量
与硬件域例如module、interface不同的是,在class中声明的变量其默认类型是动态变量,即其生命周期在仿真开始后的某个时间点开始到某时间点结束。具体来讲,其声明周期始于对象创建,终于对象销毁。
如果用static来声明class内的变量时,则其为静态变量。静态变量的声明开始于编译阶段,贯穿于整个仿真阶段。
如果在类中声明了静态变量,那么可以直接引用该变量class::var,或者通过例化对象引用object.var。类中的静态变量声明后,该静态变量有且只有一个,无论例化多少个对象(0...N),只可以共享一个同名的静态变量,因此类的静态变量在使用时需要注意共享资源的保护。

不管创建了多少Transaction对象,静态变量count只存在一个。变量id不是静态的,所以每个Transaction都有自己的id变量。这样你就不需要为count创建一个全局变量了。
静态方法
静态方法内可以声明并使用动态变量,但是不能使用类的动态成员变量,原因是因为在调用静态方法时,可能并没有创建具体的对象,也因此没有为动态成员变量开辟空间,因此在静态方法中使用类的动态变量是禁止的。但是在静态方法中可以使用类的静态变量,因为静态方法和静态变量一样在编译阶段就已经为其分配好了内存空间。

类中的方法

仿真结果

类的成员
类作为载体,具备了天生的闭合属性,即将其属性和方法封装在内部,不会直接将成员变量暴露给外部,通过protected和local关键词来设置成员变量和方法的外部访问权限。所以封装属性在设计模式中称之为开放封闭原则(OCP open Closed Principle)。

用 local 修饰了类中的成员变量之后,外部无法访问到该类被 local声明的变量。但是可以通过使用类中的方法来改变类内部的成员变量。但是外部是不能访问内部的变量的,所以第二条display的访问方式会报错。
如果没有指明访问类型,那么成员的默认类型是public,子类和外部均可以访问成员。
面试提问:local和protected有什么区别?
如果指明了访问类型是protected,那么只有该类或者子类可以访问成员,而外部无法访问。
如果指明了访问类型是local,那么只有该类可以访问成员,子类和外部均无法访问成员。
类与结构体的异同
※ 二者本身都可以定义数据成员。
※ 类变量在声明之后,需要构造(construction)才会构建对象(object)实体,而struct在变量声明时已经开辟内存。
※ 类除了可以声明数据变量成员,还可以声明方法(function/task),而struct不行。
※ 从根本来讲,struct仍然是一种数据结构,而class则包含了数据成员以及针对这些成员操作的方法。
类和模块的异同
※ 从数据和方法来看,二者均可以作为封闭的容器来定义和存储。
※ 从例化来看,模块必须在仿真一开始就确定是否应该被例化,而对于类而言,它的变量在仿真的任何时候都可以构造创建新的对象。
※ 从封装性(encapsulation)来看,模块内的变量和方法是对外部公共(public)开放的,而类则可以根据需要来确定外部访问的权限是public、protected还是local。
※ 从继承性(inheritance)来看,模块没有任何的继承性可言,即无法在原有module的基础上进行新的module功能的扩展,而继承性是类的一大特点。
类的定义可以在module、interface、program和package中,也就是所有的“盒子”。
类中也可以再声明类,如果在类中使用this,即表明this.X所调用的成员是当前类的成员,而非同名的局部变量或者形式参数:

类的编译是具有顺序的,应该先编译基本类,在编译高级类。或者说,先编译将来被引用的类,再编译引用之前已编译的类。
类的继承
类的三要素之一,类的继承。类的继承通过extend来使新的类可以拥有其继承的父类的成员变量和成员方法。

子类white_cat 和 black_cat 在new函数里面,调用了this.color,因为它们本身类里面没有定义color,所以它们就会去父类里面找父类的color,可以看到父类cat的类型是protected,即不能被外界访问,但可以被内部及子类访问。
这里要注意几点,color变量是protected类型的,所以不能通过外部来修改子类的颜色。子类也不能在初始化时,通过this.is_good = 1,因为is_good是local变量,只允许父类内部访问,不允许子类访问。外部也不允许访问is_good属性来判断该猫是否是一只好猫,但是外部可以通过function set_good来把类里面的is_good属性设置为1.(可以set,不能get)
例子2
在test类中通过继承basic_test的两个子类test_wr和test_rd,分别对DUT发起写测试和读测试。

上面为父类basic_test,下面为父类的两个子类test_wr和test_rd

test_wr和test_rd继承了basic_test的成员变量int fin,也继承了它的成员方法test( )。所以就继承来看,类的继承包括了继承父类的成员变量和成员方法。
子类在定义new函数时,应该调用父类的new函数,即super.new()。如果父类的new函数没有参数,子类也可以省略该调用,而系统会在编译时自动添加super.new()。
在父类和子类里,可以定义相同名称的成员变量和方法(形式参数和返回类型也应该相同),而在引用时,也将按照句柄类型来确定作用域。

上面子类里调用的new函数中的def,调用的是子类的def = 200,如果没有指明,那么变量调用的查找是由近到远的,先查找子类内部,如果子类内部没有才会查找父类。如果子类和父类里都有同名变量,且想调用父类而非子类的变量,可以使用super的方式直接访问父类中的成员变量。

父类有个def=100,子类也有个def=200。把子类的句柄赋值给父类的句柄,那么通过父类的句柄,索引到的def的值应该为多少?
子类如果出现了和父类同名的变量,通过子类索引到的变量是200,通过父类索引到的变量是100,即wr.def = 200,t.def = 100。如果把子类的句柄赋值给父类,子类可以访问到父类及子类里面的全部的对象和方法,而父类仅能访问子类继承于父类的那部分成员变量和方法。如上面,t 和 wr 均指向同一个对象,但是 t 只能访问对象中继承于父类的变量和方法,而 wr 可以访问父类及子类的全部变量和方法。
为什么会有这样的规定?因为你把子类的句柄赋值给一个父类,那么父类和子类都指向同一个对象,且子类能访问该对象中父类和子类的全部变量及方法,父类仅能访问对象中属于父类的部分,这是因为对象有很多很多,句柄可以指向很多对象,你不知道一个句柄接下来可能指向的对象是子类的对象还是父类的对象,所以为了数据安全,规定如果是把子类赋值给父类,那么父类仅能访问子类继承于父类的那些变量和方法。
句柄的使用
可以把子类的句柄赋值给父类的句柄,但是不能把父类的句柄直接赋值给子类的句柄,哪怕你是说他们句柄都指向同一个对象,但是也不能这么操作。因为父类句柄指向对象能访问的内容仅是父类内的成员变量及方法,而子类句柄指向的对象能访问子类及继承父类的全部变量和方法,你可以把范围大的(子类)赋值个范围小的(父类),但是不能把范围小的(父类)赋值给范围大的(子类)。
如果父类的句柄指向的是子类的对象,而且还要把父类的句柄赋值给子类的句柄,有且仅有一个方法,调用cast函数,还拿上面的做例子,假设有一个子类wr2,可以利用$cast(wr2,t) 来把父类句柄赋值给子类。但是如果父类的句柄指向的是父类的对象,那么调用cast函数也会失败,因为不能把父类的对象赋值给子类。
句柄可以当做形参传递,也可以在方法内部完成修改。

上面的代码是有问题的,在声明函数的时候,没有指明句柄tr是input还是output,所以默认为input,句柄传入并在函数内部完成初始化赋值是成功的,但是函数执行完后因为句柄是input,所以在退出函数的时候一切都会被释放掉,所以退出creat(t)之后,句柄 t 没有执行过new函数,句柄是悬空的,不能给悬空的句柄赋值,t.addr就会报错。如果要修改,可以在函数变量中声明inout,或者指明ref都是可以的。
问:在下面这个task执行完之后,t等于多少?

这里是很容易犯错的一个点,t是指向对象的句柄,for循环内部执行了三次,分别把句柄t压到了队列fifo中,要注意的是,fifo中存放的三个位置都是同一个句柄t,也就是指向的都是同一个变量,所以经过第一个循环t=0,第二个循环t=4,第三个循环t=8。之后,fifo中的三个句柄指向的都是同一个对象,所以t.addr,都是8。
包的使用(package)
SV语言提供了一种在多个module、interface和program之中共享parameter、data、type、task、function、class等的方法,即利用包(package)的方式来实现。
include的功能是文本替换,`include “stimulator.sv”这一行的意思就是把整个.sv文件内的所有内容都在这里展开,比如stimulator.sv里面有十行代码,include就是把这十行全都展开在当前文件内,但是如果直接把.sv的内容copy过来的话,package文件的代码行数就会很多,不好维护,所以采用include的方式把其他文本的内容在这里作展开。

当两个验证人员写的类的名称一样时,在整合的时候会出现命名冲突,为了避免这一情况,使用包来将一系列相关的类包装在一起,在引用某个类的时候在前面加上所属的包,这样就避免了不同人员编写类命名相同的问题。

也可以直接把两个package直接import进来,变量选择*。然后直接写package里面的类,这样在module里面如果没有找到使用的类,那么编译器就会到package里面找。但是这种方式前提是两个包里面不要有同名的变量,所以在命名的时候,为了不混淆最好加上包的前缀来保证变量的不二性。

在包里面可以定义类、静态方法和静态变量,不能定义module之类和硬件相关的东西,包内可以导入其它包定义的类。如果把类封装在某一个包中,那么它就不应该在其它地方编译,这么做的好处在于之后对类的引用更加方便。一个完整模块的验证环境组件类,应该由一个对应的模块包来封装。