第五章 面向对象基础
5.1. 介绍
对于像Verilog和C这样的过程性编程语言,在数据结构和使用它们的代码之间存在很强的划分。数据的声明和类型通常与操作它们的算法在不同的文件中。因此,很难理解程序的功能,因为这两部分是分开的。
Verilog用户比C用户更糟糕,因为在Verilog中没有结构,只有位向量和数组。如果希望存储关于总线事务的信息,则需要多个数组:一个用于地址,一个用于数据,一个用于命令,等等。关于事务N的信息分布在所有数组中。用于创建、传输和接收事务的代码位于一个模块中,该模块可能实际连接到总线,也可能没有。最糟糕的是,数组都是静态的,所以如果您的testbench只分配了100个数组条目,而当前的测试需要101个,您就必须编辑源代码来更改大小并重新堆。因此,数组的大小被调整为能够容纳最大数量的事务,但在正常测试期间,大部分内存都被浪费了。
面向对象编程(OOP)允许您创建复杂的数据类型,并将它们与使用它们的例程联系在一起。您可以通过调用例程来执行一个操作,而不是切换位,从而在更抽象的级别上创建testbench和系统级模型。当您处理事务而不是信号转换时,您是在一个更高的层次上工作,您的代码更容易编写和理解。作为一个额外的好处,您的测试工作台从设计细节中分离出来,使它更加健壮,并且在未来的项目中更容易维护和重用。
如果您已经熟悉OOP,请浏览本章,因为SystemVerilog非常严格地遵循OOP指南。请务必阅读第5.18节以学习如何构建测试平台。第8章介绍了高级面向对象的概念,如继承和更多的测试台技术;它应该被每个人阅读。
5.2. 想想名词,而不是动词
将数据和代码分组在一起有助于创建和维护大型测试工作台。数据和代码应该如何组合在一起?您可以从思考如何执行测试工作台的工作开始。
试验台的目标是对设计施加刺激,然后检查结果是否正确。流入和流出设计的数据被分组到事务中,例如总线周期、操作码、包或数据样本。组织测试工作台的最佳方法是围绕事务,以及您在事务上执行的操作。在OOP中,事务是测试平台的焦点。
你可以想到汽车和试验台之间的相似之处。早期的汽车需要对其内部结构(名词)有详细的了解才能运行。如果你在湿滑的路面上行驶,你必须提前或延迟火花,打开或关闭阻风门,密切注意引擎的速度,并注意轮胎的牵引力。今天,你与汽车的互动达到了很高的水平。当你上了车,你就会执行不同的动作(动词),比如启动、前进、转弯、停车和一边开车一边听音乐。如果你想发动汽车,只要转动钥匙点火,就可以了。按下油门踏板使汽车移动;用刹车把它停下来。你是在雪地上开车吗?别担心:防抱死刹车可以帮助你在直线上安全停车。你不必考虑底层的细节。
您的测试平台应该以同样的方式构建。传统的testbench是面向必须发生的操作的:创建一个事务,传输它,接收它,检查它,并制作一个报告。相反,您应该考虑测试台的结构,以及每个部分的作用。生成器创建事务并将它们传递到下一层。驱动程序与响应由监视器接收的事务的设计对话。记分板将这些数据与预期数据进行核对。您应该将您的测试工作台划分为块,然后定义它们如何通信。本章展示了这些组件的许多例子。
如何在SystemVerilog中表示这些块?类可以描述一个以数据为中心的块,如总线事务、网络数据包或CPU指令。或者一个类可以代表一个控制块,比如驱动程序或记分牌。无论采用哪种方式,类都将数据与操作数据的例程封装在一起。类如何实现数据生成或检查等操作的细节从外部隐藏,使类更具可重用性。
5.3. 你的第一节课
示例5.1显示了泛型事务的类。它包含一个地址、一个校验和和一个数据值数组。事务类中有两个例程:一个用于显示地址,另一个用于计算数据的校验和。
为了更容易匹配命名块的开头和结尾,您可以在它的结尾添加一个标签。在样例5.1中,这些结束标签可能看起来是多余的,但在具有许多嵌套块的复杂代码中,这些标签可以帮助您找到简单end、endtask、endfunction或endclass的配偶。
示例5.1简单事务类
每个公司都有自己的命名风格。本书使用如下约定:类名以大写字母开头,避免使用下划线,如在事务或包中。常量都是大写的,如CELL_SIZE,变量都是小写的,如count或opcode。你可以随意使用任何你想要的风格。
5.4. 在哪里定义类
您可以在程序、模块、包或任何外部定义和使用SystemVerilog中的类。
启动项目时,可以为每个文件存储一个类。当文件的数量变得太大时,您可以将一组相关的类和类型定义分组到SystemVerilog包中,如示例5.2所示。例如,您可以将所有USB3事务和bfm组合到一个包中。现在您可以独立于系统的其他部分编译这个包了。不相关的类,例如用于其他事务、记分板或不同协议的类,应该保留在单独的文件中。
本书中的代码示例省略了包,以使文本更紧凑。
包中的示例5.2类
样例5.3展示了如何将包导入程序。
例5.3在程序中导入一个包
5.5. OOP的术语
OOP新手和专家的区别是什么?首先是你用的词。通过使用Verilog,您已经了解了一些OOP概念。下面是Verilog 2001中的一些OOP术语、定义和大致的等价内容。
类——一个包含例程和变量的基本构建块。在Verilog中类似的是一个模块。
对象——类的实例。在Verilog中,您需要实例化一个模块来使用它。
句柄——指向对象的指针。在Verilog中,在引用模块外部的信号和例程时使用实例的名称。句柄类似于对象的地址,但存储在只能引用一种类型的指针中。句柄类似于其他OOP语言中的引用。
属性——保存数据的变量。在Verilog中,这是一个信号,比如寄存器或线路。
方法——操作变量的过程代码,包含在任务和函数中。Verilog模块有任务和函数以及initial和always块。
Prototype——例程的头,显示了名称、类型、参数列表和返回类型。例程的主体包含可执行代码。关于原型和体外体验的更多信息,请参阅5.10节。
本书在讨论非oop代码时使用了Verilog中更为传统的术语“变量”和“例程”,对于类则使用“属性”和“方法”。
在Verilog中,您可以通过创建模块并分层实例化它们来构建复杂的设计。在OOP中,您创建类并构造它们(创建对象)来创建类似的层次结构。模块在编译期间实例化,而类在运行时构造。
这里有一个类比来解释这些OOP术语。把一个班级想象成一座房子的蓝图。这个平面图描述了房子的结构,但你不能生活在蓝图中;你需要建造实体的房子。物体就是真正的房子。就像一套蓝图可以用来建造一大片房屋一样,一个单一的类别可以用来建造许多物体。房子的地址就像一个句柄,它唯一地标识你的房子。在你的房子里,你有一些东西,比如灯(开或关),用开关控制它们。类具有保存值的变量和控制值的例程。一个类的房子可能有许多灯。只需调用turn_on_porch_light(),就可以在单个房子中打开light变量。
5.6. 创建新对象
Verilog和OOP都有实例化的概念,但在细节上有一些不同。在编译设计时实例化一个Verilog模块,比如一个计数器。SystemVerilog类,例如网络数据包,在测试工作台需要时在模拟过程中被实例化。Verilog实例是静态的,因为硬件在模拟过程中不会发生变化;只有信号值改变。SystemVerilog刺激对象不断被创建,并用于驱动DUT和检查结果。稍后,这些对象可能会被释放,以便它们的内存可以被新的对象使用。回到房子的类比:地址通常是静态的,除非你的房子被烧毁了,所以你需要建造一个新的。家里的垃圾收集不是自动的,尤其是当你有十几岁的孩子的时候。
OOP和Verilog之间的类比有一些其他的例外。顶级Verilog模块是隐式实例化的。但是,SystemVerilog类必须在使用之前进行实例化。接下来,Verilog实例名仅引用单个实例,而SystemVerilog句柄可以引用许多对象,但一次只能引用一个对象。
5.6.1 处理和构造对象
在示例5.4中,tr是指向事务类型对象的句柄。为简单起见,您可以只说tr是一个事务句柄。
例5.4声明和使用句柄
当声明句柄tr时,它被初始化为特殊值null。
在下一行中,调用new()函数来构造事务对象。
这个特殊的新函数为事务分配空间,将变量初始化为默认值(2状态变量为0,4状态变量为X),并返回存储对象的地址。例如,事务类有两个32位寄存器(addr和csm)和一个带有8个值(数据)的数组,总共有10个长字,或40个字节。因此,当您调用new时,SystemVerilog会分配至少40个字节的存储空间。如果您使用过C语言,那么这个步骤类似于调用malloc函数。注意,SystemVerilog需要额外的内存用于4个状态变量和诸如对象类型之类的内部管理信息。
当您创建类的实例时,这个过程称为实例化。新函数有时被称为构造函数,因为它构建对象,就像你的木匠用木头和钉子建造房子。对于每个类,SystemVerilog创建一个默认的新函数来分配和初始化对象。
5.6.2 自定义构造函数
您可以定义自己的new()函数来设置自己的值。注意,不能给出返回值类型,因为构造函数是一个特殊函数,它会自动返回与类类型相同的对象句柄。
示例5.5简单的用户定义new()函数
在样例5.5中,首先SystemVerilog自动为对象分配空间。接下来,它将addr和data设置为固定值,但将csm保留为默认值x。您可以使用带有默认值的参数来创建一个更灵活的构造函数,如示例5.6所示。现在您可以在调用构造函数时指定addr和data的值,或者使用默认值。
带参数的new()函数
SystemVerilog如何知道要调用哪个new()函数?它查看赋值左边句柄的类型。在样例5.7中,在驱动构造函数内部调用new函数调用Transaction的new()函数,即使驱动函数调用的函数更接近。因为tr是一个事务句柄,所以SystemVerilog会做正确的事情,并创建一个事务类型的对象。
示例5.7调用正确的new()函数
5.6.3 分离声明和构造
你应该避免声明句柄并调用构造函数new, allin one语句。虽然这是合法的语法,而且不那么冗长,但它可能会产生排序问题,因为在第一个过程语句之前调用构造函数。您可能需要按特定顺序初始化对象,但如果在声明中调用new(),则不需要这样做
有相同的控制。此外,如果忘记使用自动存储,则在模拟开始时调用构造函数,而不是在进入块时调用。
5.6.4 New()和New[]的区别
您可能已经注意到,这个new()函数看起来很像2.3节中描述的new[]操作符,该操作符用于设置动态数组的大小。它们都分配内存和初始值。最大的区别是调用new()函数来构造单个对象,而new[]操作符构造一个包含多个元素的数组。new()可以接受参数来设置对象值,而new[]只接受数组中元素个数的单个值。只需记住,带方括号[]的new是用于数组的,而带圆括号()的是用于类的,类通常包含方法。
5.6.5 获取对象的句柄
新的OOP用户经常把对象和它的句柄搞混。两者截然不同。您声明一个句柄并构造一个对象。在模拟过程中,句柄可以指向许多对象。这就是OOP和SystemVerilog的动态特性。不要把句柄与对象混淆。
在示例5.8中,t1首先指向一个对象,然后指向另一个对象。图5.1显示了产生的手柄和对象。
示例5.8分配多个对象
图5.1分配多个对象后的句柄和对象
为什么要动态地创建对象?在模拟过程中,您可能需要创建数百或数千个事务。SystemVerilog允许您在需要时自动创建对象。在Verilog中,您必须使用足够大的固定大小数组来容纳事务的最大数量。
注意,这种对象的动态创建不同于之前在Verilog语言中提供的任何其他东西。在编译期间,Verilog模块的实例及其名称被静态地绑定在一起。即使使用在模拟过程中不断变化的自动变量,名称和存储也总是捆绑在一起的。
把手的一个类比是参加会议的人。每个人都像一个物体。当你到达时,你的名字就会被“构造”成一个徽章。这个徽章是一个把手,组织者可以用它来记录每个人。当你坐下来听讲座时,空间已经分配好了。您可能有多个参与者、演示者或组织者的徽章。当您离开会议时,您的徽章可以通过在其上写一个新的名字来重用,就像一个句柄可以通过分配来指向不同的对象。最后,如果你丢了你的徽章,没有任何东西能证明你的身份,你会被要求离开。你占用的空间,你的座位,会被别人回收利用。
5.7. 对象回收
现在你知道了如何创建一个对象——但是你如何摆脱它呢?例如,你的测试工作台创建并发送数千个事务,比如数据包、指令、帧、中断等等到你的DUT。一旦知道事务已经成功完成,就不需要保留它了。你应该回收内存;否则,长时间的模拟可能会耗尽内存。
垃圾回收是自动释放不再引用的对象的过程。SystemVerilog能够判断一个对象是否不再被使用的一种方法是跟踪指向该对象的句柄的数量。当最后一个句柄不再引用对象时,SystemVerilog将释放该对象的内存。(寻找未使用对象的实际算法因模拟器而异。本节描述最容易理解的引用计数)。
示例5.9创建多个对象
示例5.9中的第二行调用new()构造一个对象并将地址存储在句柄t中。下一个调用new()构造第二个对象并将其地址存储在t中,覆盖之前的值。由于没有指向第一个对象的句柄,SystemVerilog可以释放它。对象可以立即删除,也可以在短时间后删除。最后一行显式地清除句柄,以便现在可以释放第二个对象。
如果您熟悉c++,那么这些对象和句柄的概念是熟悉的,但是有一些重要的区别。SystemVerilog句柄只能指向一种类型的对象,因此它们被称为“类型安全的”对象。在C语言中,一个典型的空指针只是内存中的一个地址,你可以将它设置为任何值,或者用预增操作符(pre-increment)修改它。您不能确定指针是否有效。c++类型的指针要安全得多,但您可能会被C的灵活性所吸引。SystemVerilog不
允许对句柄进行任何修改,或使用一种类型的句柄引用另一种类型的对象。(SystemVerilog的OOP规范更接近Java,而不是c++)。
由于SystemVerilog垃圾在没有更多句柄引用对象时收集对象,所以可以确保代码总是使用有效句柄。在C / c++中,指针可以指向一个已经不存在的对象。在这些语言中,垃圾收集是手动的,因此当您忘记释放对象时,您的代码可能会遭受“内存泄漏”。
SystemVerilog不能垃圾收集仍然由句柄引用的对象。例如,如果将对象保留在一个链接列表中,则SystemVerilog无法释放对象,直到您通过将它们设置为null手动清除所有句柄。如果一个对象包含派生线程的例程,则该对象在线程运行时不会被分配。同样地,任何对象
被衍生的线程使用的线程在线程终止之前不能被释放。有关线程的更多信息,请参阅第7章。
5.8. 使用对象
既然已经分配了一个对象,那么如何使用它呢?回到Verilog模块的类比,您可以使用“。标记,如示例5.10所示。
在对象中使用变量和例程
在严格的OOP中,对对象中变量的唯一访问应该是通过访问函数,如get()和put()。这是因为直接访问变量会限制您将来更改底层实现的能力。如果将来出现了更好的(或者只是不同的)算法,您可能无法采用它,因为您还需要修改对变量的所有引用。
这种方法的问题在于,它是为寿命长达十年或更长的大型软件应用程序编写的。在数十名程序员进行修改的情况下,稳定性是至关重要的。然而,您正在创建一个测试平台,其目标是最大限度地控制所有变量,以生成最大范围的刺激值。实现这一目标的一种方法是使用受限随机刺激
逻辑单元的生成,如果一个变量隐藏在冰毒屏幕后面,这就不能完成。尽管get()和put()方法适用于编译器、gui和api,但您应该坚持使用可以在测试台中任何地方直接访问的公共变量。
这个规则的例外是由一个组(如公司)创建和维护的验证IP,该组与最终用户没有直接关系。例如,如果您从另一家公司购买了一个PCI处理程序,他们将限制对内部的访问,迫使您将其视为一个黑盒。开发人员必须为您提供足够的方法来生成好的事务和注入各种各样的错误。
5.9. 类方法
类中的方法只是定义在类范围内的任务或函数。样例5.11为事务和PCI_Tran定义了display()方法。SystemVerilog根据句柄类型调用正确的句柄。
示例类中的5.11例程
类中的方法默认使用自动存储,因此不必担心记住自动修饰符。
5.10. 在类的外部定义方法
一个好的经验法则是,在您最喜欢的编辑器中,您应该将一段代码限制在一个“页面”或一个屏幕内,以使其易于理解。对于例程,您可能熟悉这个规则,但它也适用于类。如果您能一次在屏幕上看到类中的所有内容,就更容易理解了。
但是,如果每个方法都取一个页面,那么整个类怎么能装得下一个页面呢?在SystemVerilog中,可以将一个方法分解为类内部的原型(方法名和参数),以及类之后的主体(过程代码)。下面是如何创建块外声明。复制方法的第一行,包括名称和参数,并在开头添加extern关键字。现在获取整个方法并将其移动到类主体之后,并在方法名称之前添加类名和两个冒号(::作用域操作符)。上面的
类的定义如示例5.12所示。
样例5.12块外方法声明
一个常见的编码错误是原型与外体不匹配。SystemVerilog要求原型与out- block方法声明相同,除了类名和作用域操作符::之外。样机可以有质量保证
本地的、受保护的或虚拟的,但不是体外体的。如果任何参数有默认值,它们必须在原型中给出,但它们在out- body中是可选的。
另一个常见的错误是在类之外声明方法时省略了类名。因此,它定义在更高的作用域(可能是程序或包作用域),当任务试图访问类级变量和方法时,编译器给出一个错误。示例5.13中显示了这一点。
样例5.13体外方法缺少类名
5.11. 静态变量与全局变量
每个对象都有自己的局部变量,不与任何其他对象共享。如果有两个事务对象,每个事务对象都有自己的addr、csm和数据变量。但有时,您需要一个由特定类型的所有对象共享的变量。例如,您可能希望保持已创建事务数量的运行计数。如果没有OOP,您可能会创建一个全局变量。然后,您将拥有一个由一小段代码使用的全局变量,但对整个测试工作台是可见的。这“污染”了全局名称空间,使每个人都可以看到变量,即使您希望将它们保持在本地。
5.11.1 一个简单的静态变量
在SystemVerilog中,您可以在类中创建静态变量。这个变量在类的所有实例中共享,但是它的作用域仅限于类。在示例5.14中,静态变量count保存到目前为止创建的对象的数量。它在声明中被初始化为0,因为在模拟开始时没有事务。每次构造一个新对象时,它都会被标记为唯一的值,并且count会增加。
带有静态变量的示例5.14类
在示例5.14中,无论创建了多少事务对象,静态变量count都只有一个副本。您可以认为count存储在类中,而不是对象中。变量id不是静态的,所以每个事务都有自己的副本,如图5.2所示。现在,您不需要为计数创建一个全局变量。
图5.2类中的静态变量
使用ID字段是跟踪对象在设计中流动的好方法。在调试测试平台时,您通常需要一个惟一的值。SystemVerilog不允许打印对象的地址,但是可以创建一个ID字段。当您想要创建一个全局变量时,请考虑创建一个类级静态变量。一个类应该是独立的,有尽可能少的外部引用。
5.11.2 通过类名访问静态变量
示例5.14展示了如何使用句柄引用静态变量。你不需要一个手柄;可以使用类名后跟::(类作用域解析操作符),如示例5.15所示。
类作用域解析操作符
5.11.3 初始化静态变量
静态变量通常在声明中进行初始化。不能在类构造函数中轻松初始化它,因为每个新对象都会调用这个函数。您需要另一个静态变量作为标志,指示原始变量是否已初始化。如果需要更详细的初始化,则可以使用初始块。确保在构造第一个对象之前对静态变量进行初始化。静态变量的另一个用途是当类的每个实例都需要来自单个对象的信息时。例如,事务类可以引用具有事务数量的配置对象。如果在事务类中有一个非静态句柄,那么每个对象都将有自己的副本,这会浪费空间。样本
5.16展示了如何使用静态变量来代替。
示例5.16句柄的静态存储
5.11.4 静态方法
当您使用更多的静态变量时,操作它们的代码可能会发展成一个完整的例程。在SystemVerilog中,甚至在创建第一个实例之前,您就可以在一个类中创建一个静态方法,该方法可以读写静态变量。
样例5.17有一个简单的静态函数来显示静态变量的值。SystemVerilog不允许静态方法读取或写入非静态变量,比如id。您可以根据下面的代码理解这个限制。当在示例结束时调用函数display_statics时,还没有构造事务对象,因此没有为id变量创建存储。
静态方法显示静态变量
5.12. 范围规则
在编写测试工作台时,您需要创建并引用许多变量。SystemVerilog遵循与Verilog相同的基本规则,但有一些有益的改进。
范围是一个代码块,如模块、程序、任务、函数、类或开始/结束块。for和foreach循环自动创建一个块,以便可以在循环的范围内声明或创建索引变量。
你只能在一个块中定义新的变量。SystemVerilog的新特性是能够在未命名的开始-结束块中声明变量。
名称可以是相对于当前范围的,也可以是以$root开头的绝对名称。对于相对名称,SystemVerilog会查找作用域列表,直到找到匹配的为止。如果希望明确,可以在名称的开头使用$root。变量不能在$root中声明,也就是说,在任何模块、程序或包之外。
样例5.18在几个作用域中使用了相同的名称。注意,在实际的代码中,您将使用更有意义的名称!名称限制用于初始块中的全局变量、程序变量、类变量、函数变量和局部变量。后者位于一个未命名的块中,因此创建的标签依赖于工具,以及信号的层次名称。
示例5.18名称范围
对于testbench,可以在程序中或初始块中声明变量。如果一个变量只在单个初始块(如计数器)中使用,则应该在那里声明它,以避免与其他块可能的名称冲突。注意,如果在一个未命名的块中声明一个变量,例如示例5.18中的初始值,那么就没有能够在所有工具中一致工作的分层名称。
在包中的任何程序或模块之外声明类。这种方法可以被所有testbench共享,并且您可以在最内层声明临时变量。这种样式还消除了忘记在类中声明变量时发生的常见错误。SystemVerilog在更高的作用域中查找该变量。
如果一个块使用了一个未声明的变量,而另一个同名的变量恰巧在程序块中声明了,则类会使用它,而不会发出警告。在示例5.19中,函数Bad::display没有声明循环变量i,所以SystemVer-
ilog使用程序级别i代替。调用函数会改变的值
测试。我,可能不是你想要的!
示例5.19类使用了错误的变量
如果将类移动到包中,类就无法看到程序级变量,因此不会像示例5.20中所示的那样无意地使用它们。
将类移到包中以查找bug
- 这是什么?
当您使用变量名时,SystemVerilog会在当前范围中查找该变量,然后在父范围中查找,直到找到该变量。这与Verilog使用的算法相同。如果您在类的深处,并且想要明确地引用类级别的对象,该怎么办?这种样式代码最常用在构造函数中,
程序员对类变量和参数使用相同的名称。在示例5.21中,关键字“this”消除了歧义,让SystemVerilog知道您将局部变量name赋值给类变量name。
示例5.21使用这个引用类变量
有些人认为这种参数命名风格使代码更容易阅读;其他人认为这是一个懒惰的程序员的捷径。
5.13. 在另一个类中使用一个类
使用对象句柄,一个类可以包含另一个类的实例。这就像Verilog的概念一样,在另一个模块中实例化一个模块来构建设计层次结构。使用包含的一个常见原因是代码重用和控制复杂性。例如,你的每一个事务都可能有一个统计块,包括表明事务开始和结束传输的时间戳,以及所有事务的信息,如图5.3和样本5.22所示。
类交易;
| |
---|---|
endclass |
total_elapsed_time静态时间;endclass |
图5.3包含对象
示例5.22统计类声明
现在,您可以在另一个类(如事务)中使用Statistics类,如示例5.23中所示。
示例5.23封装Statistics类
最外层的类Transaction可以使用通常的层次结构语法引用Statistics类中的内容,比如stats.startT。
记住要实例化对象;否则,句柄stats为null,启动调用失败。这最好在外部类Transaction的构造函数中完成。
当你的类变大时,它们可能会变得很难管理。当变量声明和方法原型增长到超过一个页面时,您应该查看类中是否有一个项的逻辑分组,以便将其分成几个较小的分组。
这也是一个潜在的信号,表明是时候重构你的代码了。美国把它分成几个较小的、相关的类。关于类继承的更多细节请参见第8章。看看你想在课堂上做什么。有什么东西你可以移动到一个或多个基类,例如。,将单个类分解为一个类层次结构?一个典型的指示是类似的代码出现在类的不同位置。您需要将该代码分解为当前类中的一个函数,当前类的父类之一,或两者兼得。
5.13.1 我的班级应该多大还是小?
正如您可能想要划分太大的类一样,您也应该对类的大小有一个下限。只有一两个成员的类会使代码更难理解,因为它增加了一层额外的层次结构,并迫使您不断地在父类和所有子类之间来回跳转
理解它的作用。另外,看看它的使用频率。如果一个小的类只实例化一次,您可能想要将它合并到父类中。
Synopsys的一位客户将每个事务变量放入自己的类中,以便更好地控制随机化。该事务有一个单独的对象,用于地址、校验和、数据等。最后,这种方法只会使类层次结构更加复杂。在下一个项目中,他们将等级制度扁平化了。
请参阅第8.4节了解关于分区类的更多思想。
5.13.2 编译顺序问题
有时,您需要编译包含另一个尚未定义的类的类。句柄的声明会导致错误,因为编译器无法识别新类型。用typedef语句声明类名,如示例5.24所示。
示例5.24使用typedef类语句
5.14. 理解动态对象
在静态分配的语言(如Verilog)中,每个信号都有一个惟一的关联变量。例如,可能有一个称为grant的连接、整数计数和一个模块实例i1。在OOP中,并不存在相同的一对一的对应。可以有许多对象,但只有少数几个命名句柄。在模拟过程中,一个testbench可能分配一千个事务对象,但是可能只有几个句柄来操作它们。如果您只编写过Verilog代码,则需要一些时间来适应这种情况。
实际上,有一个句柄指向每个活动对象。一些句柄可以存储在数组或队列中,或者存储在另一个对象中,比如链表。对于存储在邮箱中的对象,句柄位于内部SystemVerilog结构中。有关邮箱的更多信息,请参见第7.6节。请记住,一旦给指向对象的最后一个句柄赋了新值,该对象就会被垃圾回收。
5.14.1 将对象和句柄传递给方法
将对象传递给方法时会发生什么?也许该方法只需要读取对象中的值,比如上面的transmit。或者,您的方法可以修改对象,就像创建包的方法一样。无论哪种方式,当您调用方法时,您传递的是对象的句柄,而不是对象本身。
图5.4跨方法的句柄和对象
在图5.4中,生成器任务刚刚调用了transmit。有两个把手,生成器。t和传输。t,两者都指向同一个物体。
在调用方法时,如果将一个标量变量(如句柄)传递给ref参数,则SystemVerilog将传递变量的地址,以便方法可以对其进行修改。如果不使用ref,则SystemVerilog将标量的值复制到参数变量中,因此对方法中参数的任何更改都不会影响原始值。
示例5.25传递对象
在示例5.25中,初始块分配一个事务对象,并使用指向该对象的句柄调用传输任务。使用这个句柄,transit可以读取和写入对象中的值。但是,如果transmit试图修改句柄,结果将不会在初始块中看到,因为t参数没有声明为ref。
即使句柄参数没有ref修饰符,方法也可以修改对象。这常常会给新用户带来困惑,因为他们混淆了句柄和对象。如上所示,传输者可以修改对象中的数据[0]
如果不希望在方法中修改对象,则传递该对象的副本,这样原始对象就不会被修改。有关复制对象的更多信息,请参阅5.15节。
5.14.2 修改任务句柄
一个常见的编码错误是忘记在要修改的方法参数上使用ref,特别是句柄。在样例5.26中,参数tr没有声明为ref,因此调用代码不会看到对它的任何更改。参数tr有默认的输入方向。
错误的事务创建任务,缺失ref on句柄
尽管create修改了参数tr,但调用块中的句柄t仍然保持为空。您需要将参数tr声明为ref,就像示例5.27中看到的那样。
带有ref on句柄的良好事务创建器任务
如果一个方法只打算修改对象的属性,那么该方法应该将句柄声明为输入参数。如果一个方法要修改句柄,例如使其指向一个新对象,那么该方法必须将句柄声明为ref参数。
5.14.3 修改飞行中的物体
一个非常常见的错误是忘记为testbench中的每个事务创建一个新对象。在示例5.28中,generate_ bad任务创建了一个带有随机值的事务对象,并在几个循环中将其传递到设计中。
坏生成器只创建一个对象
这个错误的症状是什么?上面的代码只创建一个事务,因此每次通过循环时,generator_bad都会在传输对象的同时更改该对象。当您运行它时,$display会显示许多addr值,但是所有传输的事务对象都有相同的addr值。当传输衍生出一个需要几个周期来发送事务的线程时,bug就会变得可见,因此在传输过程中对象中的值被重新随机化。如果您的传输任务复制了该对象,则可以反复回收同一对象。这个错误也可能发生在邮箱上,如示例7.32所示
为了避免此错误,您需要在每次经过循环时创建一个新事务,如示例5.29所示。
Good generator创建了许多对象
5.14.4 数组的处理
当您编写testbench时,您需要能够存储和引用许多对象。可以创建句柄数组,每个句柄指向一个对象。样例5.30展示了在一个数组中存储10个总线事务处理。
使用句柄数组的示例5.30
数组tarray由句柄组成,而不是对象。因此,您需要在使用数组中的每个对象之前构造它,就像使用普通句柄一样。没有办法在整个句柄数组上调用new。
没有所谓的“对象数组”,尽管您可以使用这个术语作为指向对象的句柄数组的简写。请记住,一些句柄可能被设置为null,或者多个句柄可能指向一个对象。
5.15. 复制对象
您可能希望复制对象以防止方法修改原始对象,或在生成器中保存约束。您可以使用new操作符提供的简单内置副本,也可以为更复杂的类编写自己的副本。请参阅第8.2节了解为什么您应该创建复制方法的更多原因。
5.15.1 使用New操作符复制对象
使用new操作符复制对象很容易,也很可靠,如Sample所示
5.31。为新对象分配内存,并复制现有对象中的所有变量。但是,您可能已经定义的任何new()函数都不会被调用。
用new复制一个简单的类
这是一种浅拷贝,类似于原始的影印,盲目地从源端抄写值到目的端。如果该类包含另一个类的句柄,则new操作符只复制句柄的值,而不是较低级别的对象的完整副本。在示例5.32中,事务类包含Statistics类的句柄,最初在示例5.22中显示。
示例5.32复制带有new操作符的复杂类
初始块创建第一个事务对象,并在包含的对象统计中修改一个变量,如图5.5所示。
图5.5使用new操作符复制之前的对象和句柄
当使用new操作符进行复制时,会复制事务对象,但不会复制统计数据对象。这是因为new操作符不调用您自己的new()函数。相反,将复制变量和句柄的值。所以现在两个事务对象有相同的id,如图5.6所示。
图5.6使用new操作符复制后的对象和句柄
更糟糕的是,两个事务对象都指向同一个统计对象,因此使用src句柄修改startT会影响使用dst句柄所看到的情况,如图5.7所示。
图5.7 src和dst对象都指向一个统计对象,并看到更新的startT值
5.15.2 编写自己的简单复制函数
如果您有一个简单的类,不包含对其他类的任何引用,那么编写复制函数就很容易,正如您在示例5.33和5.34中看到的那样。不是调用new()函数并复制每个单独的变量,copy函数可以使用new操作符,但是它需要复制在new()中完成的任何处理,比如设置id。
示例5.33带有copy函数的简单类
示例5.34使用复制函数
5.15.3 编写深度复制函数
对于非平凡类,您应该始终创建自己的复制函数,如示例5.35所示。通过调用包含的所有对象的复制函数,可以使其成为深度复制。您自己的复制功能可以确保所有用户字段(如id)保持一致。创建自己的复制函数的缺点是,在添加新变量时需要保持更新——如果忘记一个变量,您可能会花费数小时调试以找到丢失的值。
带有深度复制函数的示例5.35复杂类
new()构造函数是通过copy调用的,因此每个对象都有一个唯一的id。为Statistics类添加一个copy()方法,如示例5.36所示,以及层次结构中的所有其他类。
示例5.36统计类声明
现在,当您创建事务对象的副本时,它将拥有自己的副本
统计对象,如示例5.37所示。
复制带有new操作符的复杂类
图5.8深度复制后的对象和处理
好消息是UVM数据宏会自动创建复制函数,因此您不必手工编写它们。手动创建这些变量非常容易出错,尤其是在添加新变量时。
5.15.4 使用流操作符在数组和数组之间打包对象
有些协议,如ATM,一次一个字节传输控制和数据值。在发送事务之前,需要将对象中的变量打包到一个字节数组中。同样,在接收到字节串之后,您需要将它们解包回事务对象中。对于这两个函数,使用流操作符,如2.12节所示。
您不能只是流化整个对象,因为这将收集所有属性,包括数据和元数据,如时间戳和您可能不想打包的自检信息。您需要编写自己的包函数,如示例5.38和5.39中所示,该函数只使用您选择的属性。
更多的好消息- UVM数据宏创建了pack和unpack方法。
带有pack和unpack函数的5.38事务类示例
示例5.39使用pack和unpack函数
5.16. 公共与地方
OOP的核心概念是将数据和相关方法封装到一个类中。默认情况下,变量对类来说是局部的,以防止一个类在另一个类内部到处乱翻。类提供了一组访问器方法来访问和修改数据。这也允许您更改实现,而不需要让类的用户知道。例如,只要用户界面(访问器方法)具有相同的功能,图形包就可以将其内部表示从笛卡尔坐标更改为极坐标。
考虑具有有效负载和校验和的事务类,以便硬件可以检测错误。在传统的OOP中,您需要使用一个方法来设置有效负载,并设置校验和,使它们保持同步。因此,你的对象将总是充满正确的值。
然而,testbench不同于web浏览器或文字处理程序等其他程序。测试工作台需要创建错误。您希望有一个错误的校验和,以便测试硬件对错误的反应。
OOP语言,如c++和Java,允许你指定变量和方法的可见性。默认情况下,类中的所有内容都是局部的,除非有其他标记。
在SystemVerilog中,所有内容都是公共的,除非标记为本地或受保护。您应该坚持使用这个默认值,这样您就可以最大限度地控制DUT的运行,这比长期的软件稳定性更重要。例如,使校验和可见允许您轻松地将错误注入
DUT。如果校验和是本地的,您就必须编写额外的代码来绕过数据隐藏机制,从而产生更大更复杂的测试平台。
5.17. 迷失偏离轨道
作为一名新的OOP学生,您可能倾向于跳过将项分组到类中所需要的额外思想,而只是将数据存储在几个变量中。避免诱惑!一个基本的DUT监视器从一个接口中采样几个值。不要只是将它们存储在一些整数中并传递给下一个阶段。这首先会节省您几分钟的时间,但最终您需要将这些值组合在一起以形成一个完整的事务。可能需要对其中几个事务进行分组,以创建更高级别的事务,如DMA传输。相反,应立即将这些接口值放入事务类中。现在,您可以将相关信息(端口号、接收时间)与数据一起存储,并轻松地将该对象传递给测试工作台的其余部分。
5.18. 建立一个Testbench
现在您已经了解了OOP的基础知识,您可以了解如何从一组类创建分层的测试工作台。图5.9是第一章的图。显然,在块之间流动的事务是对象,但是每个块也用一个类建模。
图5.9分层试验台架
生成器、代理、驱动程序、监视器、检查器和记分板都是类,建模为事务处理程序(将在下面描述)。它们在Environment类中实例化。为简单起见,测试位于层次结构的顶部,实例化环境类的程序也是如此。函数覆盖定义可以放在环境类的内部或外部。关于分层验证环境及其组件的描述,请参见1.10节。
事务处理程序由一个简单的循环组成,该循环从前一个块接收事务对象,进行一些转换,并将其发送到下一个块,正如您在示例5.40中看到的那样。有些,如生成器,没有上游块,因此这个事务处理程序构造并随机化每个事务,而其他的,如驱动程序,接收事务并将其作为信号转换发送到DUT。
示例5.40基本处理程序
如何在块之间交换事务?使用过程代码,可以让一个对象调用下一个对象,或者可以使用FIFO这样的数据结构来保存块之间流动的事务。在第7章中,你将学习如何使用邮箱,这是一种fifo,它具有在添加新值之前暂停线程的能力。
5.19. 结论
使用面向对象编程是一大步,特别是如果您的第一种计算机语言是Verilog。这样做的好处是您的测试工作台更加模块化,因此更容易开发、调试和重用。
要有耐心——您的第一个OOP测试平台可能看起来更像添加了几个类的Verilog。当您掌握了这种新的思维方式后,您就开始为事务和测试台中处理事务的事务创建和操作类。
在第8章中,你将学到更多的面向对象技术,这样你的测试就可以改变底层测试平台的行为,而不需要改变任何现有的代码。
5.20. 练习
创建一个名为MemTrans的类,该类包含以下成员,然后在初始块中构造一个MemTrans对象。
逻辑类型的8位数据
逻辑类型的4位地址
一个名为print的void函数,输出data_in和address的值
使用练习1中的MemTrans类,创建一个自定义构造函数,即new函数,这样data_in和address都初始化为0。
使用练习1中的MemTrans类,创建一个自定义构造函数,使data_in和address都初始化为0,但也可以通过传入构造函数的参数进行初始化。此外,编写一个程序来执行以下任务。
创建两个新的MemTrans对象。
在第一个对象中初始化address为2,按名称传递参数。
在第二个对象中,将data_in初始化为3,并将address化为4,通过名称传递参数。
修改练习3中的解决方案以执行以下任务。
建设后,设置第一个对象的地址为4'hF。
使用print函数打印出这两个对象的data_in和address的值。
显式地释放第二个对象。
使用练习4中的解决方案,创建一个静态变量last_address,它保存最近创建的对象的address变量的初始值,就像构造函数中设置的那样。在分配MemTrans类的对象之后(练习4中完成),打印last_address的当前值。
使用练习5中的解决方案,创建一个名为print_last_ address的静态方法,该方法打印出静态变量last_address的值。分配MemTrans类的对象后,调用print_last_address方法打印last_address的值。
给定以下代码,在MemTrans类中完成print_all函数,使用类prinutility打印data_in和address。演示如何使用print_all函数。
- 完成以下以//开头的注释所指示的代码。
- 对于下面的类,创建一个复制函数并演示它的用法。假设Statistics类有自己的复制函数。