第五章-面向对象编程基础

目录

5.1概述

5.2考虑名词而非动词 

5.3编写第一个类(class) 

5.4在哪里定义类

5.5OOP术语

5.6创建新对象

5.6.1没有消息就是好消息

5.6.2定制构造函数

5.6.3将声明和创建分开

5.6.4new( )和new[ ]的区别

5.6.5为对象创建一个句柄 

5.7对象的解除分配

5.8使用对象

5.9静态变量和全局变量

5.9.1简单的静态变量 

5.9.2通过类名访问静态变量

5.9.3静态变量的初始化

5.9.4静态方法 

5.10类的方法

5.11在类之外定义方法

5.12作用域规则

5.12.1this是什么

5.13在一个类中使用另一个类

5.13.1类该做成多大

5.13.2编译顺序的问题 

5.14理解动态对象

5.14.1将对象传递给方法 

5.14.2在任务中修改句柄

5.14.3在程序中修改对象

5.14.4句柄数组

5.15对象的复制

5.15.1使用new操作符复制一个对象 

5.15.2编写自己的简单复制函数

5.15.3编写自己的深层复制函数

5.15.4使用流操作符从数组到打包对象,或者从打包对象到数组

5.16共有和私有

5.17建立一个测试平台 


5.1概述

对结构化编程语言,例如verilog和C来说,它们的数据结构和使用这些数据结构的代码之间存在很大的沟壑。数据声明、数据类型和操作这些数据的算法经常放在不同的文件里,因此造成了对程序理解的困难。

verilog的境遇比C更加棘手,因为verilog中没有结构,只有数组和位向量。如果想存储总线事物的信息,就需要多个数组:一个用于保存地址,一个用于保存数据,一个用于保存指令等等。最糟糕的是,这些数组都是静态的,如果测试平台只配置了100个数组项,而当需要测试101个时,就需要修改源代码来改变数组的大小,并且重新编译。

面向对象编程(OOP)使用户能够创建复杂的数据类型,并且将它们与使用这些数据的程序紧密的结合在一起。用户可以在更加抽象的层次建立测试平台和系统级模型,通过调用函数来执行一个动作而不是改变电平的信号。当使用事务来代替电平翻转的时候,就会更加高效。SV相当严格的遵守OOP的规则。

5.2考虑名词而非动词 

将数据和代码组合在一起可以有效的帮你编写和维护大型测试平台。如何把数据和代码组合到一起?先想想测试平台是如何工作的。

测试平台的目标是给一个设计施加激励,然后检查其结果是否正确。如果把流入和流出设计的数据组合到一个事务里,那么围绕事务及其操作实施测试平台就是最好的办法。在OOP中,事务就是测试平台的焦点。

传统的测试平台强调的是要做的操作:创建一个事务、发送、接收、检查结果,然后产生报告。而在OOP中,你需要重新考虑测试平台的结构,以及每部分的功能。发生器(generator)创建事务并将它们传递给下一级,驱动器(driver)和设计进行会话,设计返回的事务被监视器(monitor)捕获,记分板(scoreboard)将捕获的结果与预期结果进行对比。因此测试平台应分成若干个块(block),然后定义它们相互之间如何通信。

5.3编写第一个类(class) 

类封装了数据和操作数据的子程序。如下例:在Transaction类中有两个子程序,一个是输出数据包地址的函数和一个计算循环冗余校验码(CRC)的函数。

为了方便的对齐一个快的开始和结束部分,建议在块的最后加上一个标记(label)类名的首字母一般用大写字母,常量都使用大写字母定义,变量全部采用小写字母,这是面向对象的通用写法。

5.4在哪里定义类

在SV中,可以把类定义在program、module和package中,或者在这些块之外的任何地方。类可以在程序和模块中使用

当创建一个项目时,可能需要将每个类保存在单独的文件夹中。当文件的数目太大时,可以使用SV的包(package)将一组相关的类和类型定义捆绑在一起

5.5OOP术语

下面是OOP的一些术语、定义以及它们与verilog的大致对应关系:

(1)类(class):包含变量和程序的基本模块,verilog中与之对应的是模块(module);

(2)对象(object):类的一个实例;在verilog中,需要实例化一个模块才能使用它; 

(3)句柄(handle):指向对象的指针;在verilog中,通过实例名在模块外部引用信号和方法;一个OOP句柄就像一个对象的地址,但是它保存在一个只能指向单一数据类型的指针中;

(4)属性(property):存储数据的变量;在verilog中,就是reg或wire类型的信号;

(5)方法(method):任务或函数中操作变量的程序性代码;verilog模块除了initial和always块之外还有任务和函数;

(6)原型(prototype):程序的头,包括程序名、返回类型和参数列表;

在verilog中,通过创建模块并逐层例化就可以得到复杂的设计;在OOP中创建类并例化它们(创建对象),就可以得到相似的层次结构。下面是对OOP术语的比喻:将类视为房子的蓝图,该设计描述了房子的结构,但是你不能住在蓝图里,需要建造一栋实际的房子,对象就是实际的房子。一张蓝图可以建造许多栋房子,一个类可以创建许多对象。房子的地址就像是句柄,它唯一的标志了你的房子。在房子里有许多东西,就如同类里有许多变量,类中的变量用来保存数值,而程序用来控制这些数值。

5.6创建新对象

 verilog和OOP都有例化的概念,但是在细节方面却存在着差别。一个verilog模块是在代码被编译的时候例化的,而SV的类却是在运行中测试平台需要的时候才被创建。verilog的例化是静态的,就像硬件在仿真时不会变化,只有信号的值在变;而SV中,激励对象不断的被创建并且来驱动DUT,检查结果。最后这些对象所占用的空间可以被释放以供新对象使用。

OOP与verilog之间的相似性也有一些例外。verilog的顶层模块不会被显示的例化,但SV的类在使用前必须先例化(创建类的对象)。例外,verilog的实例名只可以指向一个实例,而SV句柄可以指向很多对象,当然一次只能指向一个

5.6.1没有消息就是好消息

看下面例子:

Transaction为一个类;在声明句柄tr时,它被初始化为特殊值null;接下来,调用new()函数来创建类的对象,并为对象分配空间,将类中的变量初始化为默认值,并返回保存对象的地址。对于每一个类来讲,SV创建一个默认的new函数来分配并初始化对象

5.6.2定制构造函数

当调用new函数例化一个对象时,你是在为该对象申请一块内存来保存对象的变量。构造函数除了分配内存外,还可以初始化变量。在默认的情况下,它将变量设置为默认值-二值变量为0,四值变量为X。你也可以通过自定义new函数将默认值设为你所要的数值;但是new函数不能有返回值,因为构造函数总是返回一个指向类对象的句柄,其类型就是类本身

new()函数一般不带参数,当然可以在自己定义的new函数上带参数当使用带参数的构造函数来创建对象时,这说明使用类中自定义的带参数的构造函数;若使用不带参数的构造函数来创建对象,则使用类中默认的无参构造函数。上例将addr和data设为固定数值,但是crc任然被初始化为默认值。你也可以使用具有默认值的函数参数来创建更加灵活的构造函数,如例5.4所示:

上例的new(10)将10赋值给new的第一个参数a;SV怎么知道该调用哪个new函数呢?这取决于赋值操作符左边的句柄类型。

上例中tr是Transaction的句柄,SV会做出正确的选择,创建一个Transaction类的对象。

5.6.3将声明和创建分开

应该避免在声明一个句柄时就调用构造函数,这可能会引起顺序问题,因为在这时构造函数在第一条过程语句前就被调用了;其次,如果忘记了使用automatic存储空间,构造函数将在仿真开始时而非进入块时被调用;

5.6.4new( )和new[ ]的区别

两者最大的区别在于new()函数仅创建了一个对象,而new[ ]操作则建立了一个含有多个元素的数组。前者可以再括号里面进行参数设置来初始化对象的参变量,而后者只需在括号中填写一个数值来设置数组的长度。

5.6.5为对象创建一个句柄 

注意对象和对象的句柄之间的区别:你通过声明一个句柄(对象的地址,即指针)来创建一个对象;在一次仿真中,一个句柄可以指向很多对象,这就是OOP和SV的动态性。

上例中t1首先指向一个对象,然后指向另一个对象。图给出了对象和指针最后的结果。

5.7对象的解除分配

SV分辨对象不再被引用的方法就是记住指向它的句柄数量,当最后一个句柄不再引用某个对象了,SV就释放该对象的空间。看下例:

SV的句柄只能指向一种类型,即所谓的“安全类型”;在C中,一个典型的无类型指针只是内存的一个地址,这时候无法保证指针一定是合法的;SV不允许将一种类型的句柄指向另一种类型的对象;

其次,因为SV在没有任何句柄指向一个对象的时候自动回收垃圾,这保证了代码中所使用的任何句柄都是合法的。

5.8使用对象

如果你已经分配了一个对象,那么如何来使用它?回到verilog模块的对比,可以对对象使用“.”符号来引用变量和子程序。看下例:

严格的OOP规定,只能通过对象的公有方法来访问对象的变量,例如get()和put()。这是因为直接访问变量会限制以后对代码的更改。

5.9静态变量和全局变量

每个对象都有自己的局部变量这些变量不与任何其他对象共享。但有时候你需要一个某种类型的变量,被所有的对象所共有。例如,可能需要一个变量来保存已创建事务的数目。

5.9.1简单的静态变量 

在SV中,可以在类中创建一个静态变量,这个变量被这个类的所有对象共享,并且作用范围仅限于这个类。看下例:

静态变量count用来保存所创建对象的数目,在声明的时候已被初始化为0.每构造一个新对象,count加1.在上面例子中,不管创建了多少对象,静态变量count只存在一个。可以认为count保存在类中而不是对象中。变量id不是静态的,所以每个对象都有自己的id变量,如下图所示,这样你就不需要为count创建一个全局变量了。

使用ID域是在设计中跟踪对象的一个非常好的方法。当你考虑创建一个全局变量时,首先考虑创建一个类的静态变量。一个类应该是自给自足的,对外部的引用越少越好。

5.9.2通过类名访问静态变量

上例使用了句柄来引用静态变量,其实也可以使用类名加上::,即类作用域操作符。看下例:

5.9.3静态变量的初始化

静态变量一般在声明时初始化,不能简单的在类的构造函数中初始化静态变量,因为每个新的对象都会调用构造函数;必须保证在创建第一个对象前,就已经初始化了静态变量

5.9.4静态方法 

静态变量的另一种用途是在类的每一个对象都需要从同一个对象获取信息的时候。看下例:

在SV中,可以在类中创建一个静态方法来读写静态变量,甚至可以在第一个实例产生前读写静态变量。下例中含有一个简单的静态函数来显示静态变量的值,注意SV不允许静态方法读写非静态变量

5.10类的方法

类的程序也称为方法,也就是在类的作用域内部定义的内部task或者function.看下例:

类中的方法默认使用自动存储,不必担心忘记使用automatic修饰符

5.11在类之外定义方法

一条值得注意的规则是,应当限制代码的长度在一页范围内以保证其可读性。在SV中,可以将方法的原型定义(方法名和参数)放在类的内部,而方法的程序体(过程代码)放在类的后面定义。

下面是创建一个块外声明的例子。复制该方法的第一行,包括方法名和参数,然后再开始处添加关键词extern,然后将整个方法移至类后面定义,并在方法名前加上类名和“::”(作用域操作符)。

方法的原型定义与内容不相匹配是一个常见的编码错误SV要求除了多一个类名和作用域操作符外,原型定义与块外的方法定义一致另一个常见的错误是在类的外部声明方法时忘记添加类名。这样做使得方法的作用范围高了一级,当某个任务试图调用类一级的变量和方法的时候,编译器就会报错。看下例:

5.12作用域规则

作用域是一个代码块,例如一个模块、程序、任务、函数、类或者begin...end块。变量的名字可以相对于当前作用域,也可以用绝对作用域表示,例如以$root开始。对于一个相对的名字,SV查找作用域内的名字清单,直至找到匹配的名字。如果不想引起歧义,可以在名字的开头使用$root。下例在不同的作用域内使用了相同的名字:

类应当在program或者package中定义,这是所有测试平台都应当遵守的。如果在一个块内使用了未声明的变量,恰巧在程序块中有一个同名的变量,那么类就会使用程序块中的变量,不会给出任何警告。在下例中函数display没有声明循环变量i,所以SV将使用程序级的变量i,这可能并不是你所希望的。

如果将类移到一个package中,那么类就看不到程序一级的变量了,由此就不会无意调用到它了。

5.12.1this是什么

当使用一个变量名的时候,SV将先在当前作用域寻找,接着在上一级作用域内寻找,直至找到该变量为止。但是如果你在类的很深的底层作用域,却想明确的引用类一级的对象呢?在下例中,关键词“this”明确的告诉 SV你正在将局部变量赋给类一级变量其实this就是对象的句柄,即指针

5.13在一个类中使用另一个类

通过使用指向对象的句柄,一个类的内部可以包含另一个类的实例。这如同在verilog中,在一个模块内部包含另一个模块的实例,以建立设计的层次结构,看下例:

在另一个类中使用上面那个类:

最外层的类Transaction可以通过分层调用语法来调用Statistics类中的成员;一定要记得例化对象,否则句柄ststs是null.

5.13.1类该做成多大

可能需要将太大的类分成若干小类,同时也要定义一个类的下限;

5.13.2编译顺序的问题 

有时候你需要编译一个类,这个类中包含一个尚未定义的类。声明这个被包含的类的句柄就会引起错误,因为编译器还不认识这个新的数据类型。这时候需要使用typedef语句声明一个类名。看下例:

 

5.14理解动态对象

在静态分配语言中,每一块数据都有一个变量与之关联;在OOP中,不存在这一一对应的关系。可能有很多对象,但是只定义了少量句柄,但是一个句柄每次只能指向一个对象。

5.14.1将对象传递给方法 

当将对象传递给一个方法时发生了什么?也许该方法只需要读取对象中的变量值,或者修改对象中的变量值。不管是哪种情形,当将对象传递给方法时,传递的是句柄而不是对象本身

在上图中,任务generator调用了transmit。两个句柄generator.t和transmit.t都指向同一个对象。

当你调用一个带有标量变量(不是数组也不是对象)的方法并且使用ref关键词的时候,SV传递该标量的地址,所以方法可以修改标量变量的值如果不使用ref关键词,SV将该标量变量的值复制到参数变量中,对该参数变量的任何修改都不会影响原变量的值

transmit任务的参数是指向对象的句柄,通过使用句柄,transmit可以读写对象中的值。但是如果transmit试图改变句柄,初始化块将不会看到结果,因为参数t没有带ref修饰符。如果不想让对象在方法中被修改,那么就传递一个对象的拷贝给方法,这样原来的对象就不变。

5.14.2在任务中修改句柄

一个常见的错误就是当你想修改参数的值的时候,忘记在方法参数前加ref修饰符,尤其是句柄。在下例中,参数tr默认的方向是input,且没有加ref修饰符,所以方法内部对tr的修改不会被调用该方法的代码看到

尽管creat修改了参数tr,调用块中的句柄t任然是null,此时你需要将参数tr声明为ref。

5.14.3在程序中修改对象

在测试平台中,一个常见的错误就是忘记为每个事务创建一个新的对象。看下例:

上面的代码仅创建了一个Transaction对象,所以每次循环,generator_bad在发送对象的同时又修改了它的内容。当运行这段代码的时候,display会显示很多不同的addr值,但是所有被传送的对象t都有相同的addr数值。如果transmit的线程需要耗费几个周期发送,就有可能出现错误,因为对象的内容在传送期间被随机化了。如果transmit发送的是对象的副本,你就可以重复利用这个对象了。

为了避免这种错误,你需要在每次循环的时候创建一个新的对象

5.14.4句柄数组

在写测试平台的时候,可能需要保存并引用许多对象。可以创建句柄数组,数组的每个元素指向一个对象。看下例:

 

tarray数组由句柄组成,而不是由对象构成。所以在使用它们之前创建所有的对象,没有任何办法通过调用new函数为整个句柄数组创建对象。记住,不存在对象数组的说法

5.15对象的复制

有时候可能需要复制一个对象,以防止对象的方法修改原始对象的值,或者在一个发生器中保留约束

5.15.1使用new操作符复制一个对象 

使用new复制一个对象简单可靠,它创建了一个新对象,并且复制了现有对象的所有变量。但是你已经定义的任何new()函数都不会被调用。

这是一种简易复制,原对象的值被盲目抄写到目的对象中。如果类中包含一个指向另一个类的句柄,那么只有最高一级的对象被new操作符复制,下层的对象都不会复制。看下例:

初始化创建第一个Transaction对象,并修改它内部Statistics对象的变量,如下图:

当调用new函数进行复制时,Transaction对象被拷贝,但是Statistics对象没有被复制。这是因为使用new操作符复制第一个对象的时候,它不会调用你自己的new函数。相反的,变量和句柄的值被复制,所以现在两个对象都具有相同的id,如下图5.6所示:

更糟糕的是,两个Transaction对象都指向同一个Statistics对象,所以调用src句柄修改startT会影响到dst句柄可以看到的值。

5.15.2编写自己的简单复制函数

如果有一个简单的类,它不包含任何对其他类的引用,则编写copy函数非常简单。

5.15.3编写自己的深层复制函数

对于并非简单的类,应该创建自己的copy函数。通过调用类所包含的所有对象的copy函数,可以做一个深层的拷贝。自己的copy函数需要确保所有用户域保持一致。创建自定义copy函数的最后阶段需要在新增变量的同时更新它们。

copy调用了构造函数new,所以每个对象都有唯一的id。需要为Statistics类和层次结构中的每一个类增加一个copy()方法.

这样,当你赋值一个Transaction对象时,它会有自己的Statistics对象,如下例所示:

5.15.4使用流操作符从数组到打包对象,或者从打包对象到数组

某些协议,如ATM协议,每次传输一个字节的控制或者数据值。在送出一个Transaction之前,需要将对象中的变量打包成一个字节数组。类似的,在接收到一个字符串后,也需要将它们解包到一个transaction对象中。这两种功能都可以用流操作符来实现。

你不能将整个对象送入流操作符,因为这样会包含所有的成员,包括数据成员和其他额外的信息。你需要编写自己的pack函数,仅打包你需要的成员变量。

5.16共有和私有

OOP的核心是把数据和相关方法封装成一个类。在一个类中,数据默认定义为私有,这样防止了其他类对内部数据成员的随意访问。类提供了一系列的方法来访问和修改数据,使得你能够在不让类用户知道的情况下修改方法的具体实现方式。

5.17建立一个测试平台 

上图中的事务就是对象,每一块代表了一个类。图中的generator、Agent、driver、monitor、checker和scoreboard都是类,被建模成事务处理器(transactor).它们在envirenment类内部例化。为了简单起见,test在最高层,即处在例化environment类的程序中。功能覆盖(functional coverage)的定义可以放在environment类的内部或外部。、

事务处理器由一个简单的循环构成,这个循环从前面的块接收事务对象,经过变换后送给后续块。有一些块,如generator没有上游块,所以该事务处理器就随机创建和复制每一个事务,而其他的对象例如driver接收到一个事务后将其作为信号发送到DUT中。

块之间如何交换事务?在程序性代码中,需要在一个对象里调用另一个对象,或者使用FIFO之类的数据结构来保存块之间的事务。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值