一、概述
对结构化编程语言,例如Verilog和C语言来讲,它们的数据结构和使用这些数据结构的代码之间存在很大的沟壑。数据声明、数据类型与操作这些数据的算法经常放在不同的文件里,因此造成了对程序理解的困难。
Verilog程序员的境遇比C程序员更加棘手,因为 Verilog 语言中没有结构(structures),只有位向量和数组。如果你想要存储一个总线事务(bus transaction)的信息,你就需要多个数组:一个用于保存地址,一个用于保存数据、一个用于保存指令等等。事务(transaction)N的信息分布在这些所有的数组中。用来创建、发送和接收事务的代码位于模块( module)中,但这个模块可能连接到总线上,也可能根本没有连接到总线上。最糟糕的是,这些数组都是静态的,所以如果测试平台( testbench)只配置了100个数组项,而当前测试需要101个时,就需要修改源代码来改变数组的大小,并且重新编译。结果,数组的大小被配置成可以容纳最大数目的事务,但是在一个普通的测试中,大多数的存储空间却浪费了。
面向对象编程(OOP)使用户能够创建复杂的数据类型,并且将它们跟使用这些数据类型的程序紧密地结合在一起。用户可以在更加抽象的层次建立测试平台和系统级模型,通过调用函数来执行一个动作而不是改变信号的电平。当使用事务来代替信号翻转的时候,你就会变得更加高效。这样做的附加好处是,测试平台跟设计细节分开了,它们变得更加可靠,更加易于维护,在将来的项日中可以重复使用。
将数据和代码结合到一起有助于帮助我们编写和维护大型的测试平台,如何把数据和代码组合到一起?我们首先来想象一下测试平台是如何工作的。
测试平台的目标是给一个设计施加激励,然后检查其结果是否正确。如果把流入和流出设计的数据组合到一个事务里,那么围绕事务以及其他操作实施测试平台就是最好的办法,在OOP中,事务就是测试平台的焦点。
传统的测试平台强调的是要做的操作:创建一个事务,发送,接收,检查结果,然后产生报告。而在OOP中,我们需要重新考虑测试平台的结构,以及每部分的功能。发生器(generator)创建事务并且将它们传递给下一级,驱动器(driver)和设计进行会话,设计返回的事务将被监视器(monitor)捕获,记分板(scoreboard)会将捕获的结果跟预期的结果进行对比。因此,测试平台应该分成若干个块(block),然后定义它们相互之间如何通信。
二、类的封装
1、类的概述
类是一种包含了数据和方法(function、task)的类型。 例如一个数据包,可能被定义为一个类,类中可以包含指令、地址、队列ID、时间戳和数据等成员。
面向对象编程(OOP,Object-Oriented Programming)使用户能够创建复杂的数据类型,并且将它们跟使用这些数据类型的程序紧密地结合在一起。 用户可以在更加抽象的层次建立测试平台和系统级模型,通过调用函数来执行一个动作而不是简单地改变信号的电平。在验证环境中,包括stimulator、monitor、checker以及其它验证组件接下来都将按照OOP的方式来构建。
2、在哪里定义类
在SV中,我们可以把类定义在program、module、package中,或者在这些之外的任何地方。类可以在程序和模块中使用。
当你创建一个项目的时候,可能需要将每个类保存在独立的文件中。当文件的数目变得太大的时候,可以使用SystemVerilog的包( package)将一组相关的类和类型定义捆绑在一起。例如,可以将所有的 SCSI/ATA事务组合到一个包中。这个包与系统的其他部分独立,可以单独地编译。其他不相关的类,例如事务、记分板或者不同协议( protocol)的类应该放在不同的文件中。
3、OOP 术语
作为一个OOP小白,还是很有必要了解以下OOP的一些术语和概念的。为了方便理解,这里把OOP的术语与我们熟悉的verilog进行了一个对应。
- 类(class):包含变量和子程序的基本构建块。在Verilog中,与之对应的是模块(module)。
- 对象(object):类的一个实例。在Verilog中,与之对应的是实例化一个模块。
- 句柄(handle):指向对象的指针。在Verilog中,我们需要通过实例名在模块外部引用信号和方法。一个OOP句柄就像一个对象的地址,但是它保持在一个只能指向单一数据类型的指针中。
- 属性(property):存储数据的变量。在Verilog中,就是寄存器(reg)或者线网(wire)类型的信号。
- 方法(method):任务或者函数中操作变量的程序性代码。Verilog模块除了initial和always块以外,还含有任务和函数。
- 原型(prototype):程序的头,包括程序名、返回类型和参数列表、程序体现包含了执行代码。
下面是一个对这些OOP术语的比喻。将类视为一个房子的蓝图( blueprint),该设计图描述了房子的结构,但是你不能住在一个蓝图里,你需要建造一幢实际的房子。一个对象就是一个实际的房子。如同一组蓝图可以用来建造每个房子的各个部分,一个类也可以创建很多的对象。房子的地址就像一个句柄,它唯一地标志了你的房子。在你的房子里面,你有很多东西﹐例如带有开关的灯(开或者关)。类中的变量用来保存数值,而子程序用来控制这些数值。一个房子类可能具有很多盏灯﹐对turn_on_porch_light ()的一个简单调用就可以将一个房子的走廊灯变量值置为(N
4、创建新对象
Verilog 和 OOP 都具有例化的概念,但是在细节方面却存在着一些区别。一个Verilog模块,例如一个计数器,是在代码被编译的时候例化的。而一个SystemVerilog类,例如一个网络数据包,却是在运行中测试平台需要的时候才被创建。Verilog的例化是静态的,就像硬件一样在仿真的时候不会变化,只有信号值在改变。而SystemVerilog 中,激励对象不断地被创建并且用来驱动DUT,检查结果。最后这些对象所占用的内存可以被释放,以供新的对象使用。
OOP 和 Verilog 之间的相似性也有一些例外。Verilog 的顶层模块是不会被显式地例化的。但是SystemVerilog类在使用前必须先例化。另外,Verilog 的实例名只可以指向一个实例,而 SV 句柄可以指向很多对象,当然一次只能指向一个。 如果多个对象为了共享一个成员(变量/方法),那么可以为其添加关键字static。
同时,SV并不像C++语言一样要求复杂的存储空间开辟和销毁的手段,而是采用了像Java一样空间自动开辟和回收的手段。 因此SV的类在定义时,只需要定义构建函数(constructor),而不需要定义析构函数(destructor)。类在定义时,需要定义构建函数,如果未定义,则系统会自动帮 助定义一个空的构建函数(没有形式参数,函数体亦为空)。
三、类的继承
之前定义过的类Packet,可以进一步扩展构成一个新的类LinkedPacket。 通过extends,LinkedPacket继承于其父类Packet,包括继承其所有的成员(变量/方法)。
如果子类中声明了与父类同名的成员(变量/方法),那么子类中对其同名成员的访问都将指向子类,而父类的成员将被隐藏。
super是用来访问当前对象其父类的成员。 尤其当子类的成员如果与父类的成员同名,那么需要使用super来指定访问其父类成员,而非默认的子类成员。
四、总结
使用面向对象编程是一个很大的跨越,尤其当Verilog是我们的第一种计算机语言时。使用OOP的成果是我们的测试平台将变得更加的模块化,这样就更加容易开发,调试和重用。
要有耐心——我们的第一个OOP测试平台可能看起来很像是增加了几个类的Verilog。但是一旦掌握了这种思维方式,我们就能开始为测试平台中的事务和操作这些类的事务处理器创建和操作类了。