第八章高级面向对象和测试工作台指南

如何为同样执行错误注入并具有随机延迟的总线事务创建复杂类?第一种方法是将所有内容放入一个大型的扁平类中。这种方法构建简单,易于理解(所有代码都在一个类中),但开发和调试可能比较慢。此外,如此大的类是一个维护负担,因为任何想要进行新的事务行为的人都必须编辑相同的文件。正如您永远不会仅仅使用一个Verilog模块来创建复杂的RTL设计一样,您应该将类分解为更小的、可重用的块。

另一种方法是作曲。正如你在第5章中所学到的,你可以在一个类中实例化另一个类,就像你在另一个类中实例化模块一样,构建一个分层的testbench。您从自顶向下或自底向上编写和调试类,在决定哪些变量和方法进入各个类时,总是寻找自然分区。一个像素可以划分为它的颜色和坐标。一个包可以分为报头和有效载荷。您可以将一条指令分解为操作码和操作数。有关分区的指导方针,请参阅第8.4节。

有时很难将功能划分为单独的部分。考虑在总线事务期间注入错误。在为事务编写原始类时,可能不会考虑到所有可能的错误情况。理想情况下,您希望为一个好的事务创建一个类,然后添加不同的错误注入器。事务具有数据字段和从数据生成的错误检查校验和字段。错误注入的一种形式是校验和字段的损坏。如果您使用组合,那么对于好的事务和错误事务,您需要单独的类。使用好的对象的Testbench代码必须重写以处理新的错误对象。您需要的是一个与原始类相似但添加了一些新变量和方法的类。这个结果是通过继承来实现的。

继承允许通过添加新的变量和方法来扩展现有类。原始类被称为基类。由于新类扩展了基类的功能,所以它被称为扩展类。继承通过在现有类上覆盖特性(如错误注入)而不修改该类,从而提供了可重用性。

OOP真正的强大之处在于,它允许您获取一个现有的类(比如一个事务),并通过替换方法有选择地更新其部分行为,而不必更改周围的基础设施。所有依赖于基类的原始测试继续工作,现在您可以使用扩展类创建新的测试。通过一些计划,您可以创建一个足够坚固的testbench来发送基本事务,但又能够容纳测试所需的任何扩展。

请注意,本章涉及了广泛的高级OOP主题,其中许多在学习SystemVerilog时并不需要。现在可以跳过后面的部分,等您深入研究UVM和VMM的内部结构时再看。

8.1. 介绍继承

图8.1显示了一个简单的测试台。测试控制发电机。生成器创建事务,随机化它们,并沿着虚线将它们发送给驱动程序。驱动程序将事务分解为pin摆动,并沿着虚线将其发送到DUT。测试台上的其他部分被忽略了。

图8.1简化分层试验台架

8.1.1 基本的事务

示例8.1中的基本事务类有用于源地址和目的地址的变量、8个数据字、一个用于错误检查的校验和,以及用于显示内容和计算校验和的方法。calc_csm函数被标记为虚函数,以便在需要时可以重新定义它,如下一节所示。虚方法将在本章后面的章节中进行更详细的解释

8.3.2。这个类非常简单,它使用默认的SystemVerilog构造函数,该构造函数分配内存并将变量初始化为默认值。

示例8.1基事务类

通常计算校验和将在post_randomize()中完成,但在这个例子中,它已经从随机化中分离出来,以展示如何注入错误。

图8.2显示了包含变量和方法的类的图。

事务

图8.2基本事务类图

  1. 扩展事务类

假设你有一个通过DUT发送好的事务的测试平台,现在你想注入错误。如果您遵循第1章的指导方针,您将希望对您现有的测试平台进行尽可能少的代码更改。那么如何重用现有的事务类呢?使用现有的类

扩展它以创建一个新类。这是通过声明一个新类BadTr作为当前类的扩展来实现的。Transaction是基类,BadTr是扩展类。代码显示在示例8.2和图8.3的图表中。

示例8.2扩展事务类

注意,在示例8.2中,变量csm is不需要分层标识符。BadTr类可以看到来自原始事务的所有变量以及它自己的变量,如bad_csm,如图8.3所示。扩展类中的calc_csm函数使用超前缀调用基类中的calc_csm。你可以向上调用一个级别,但可以跨多个级别调用,比如super.super。SystemVerilog中不允许new。这种跨越多个级别的样式会因为跨越多个边界而违反封装规则。

原来的显示方法打印了一行,从前缀开始。因此扩展的display方法打印前缀、类名和bad_csm

$write使结果仍然在单行上。

事务

BadTr

图8.3扩展事务类图

始终将类中的方法声明为虚方法,以便可以在扩展类中重新定义它们。这适用于所有任务和函数,除了new函数,该函数在构造对象时被调用,因此无法扩展它。SystemVerilog总是根据句柄的类型调用新函数。虚方法将在8.3.2节中详细描述。

8.1.2 更多的OOP的术语

这里是一个快速的术语表。正如第5章所解释的那样,面向对象的术语是类中的变量“属性”,任务或函数称为“方法”。基类不是从任何其他类派生的类。当您扩展一个类时,原始类(如事务)被称为父类或父类。扩展类(BadTr)也称为派生类或子类。方法的“原型”只是显示参数列表和返回类型(如果有的话)的第一行。当你将方法的主体移到类之外时,原型会被使用,但是需要描述方法如何通信,如5.10节所示。

8.1.3 扩展类中的构造函数

当您开始扩展类时,有一条关于构造函数(新函数)的规则要记住。如果基类构造函数有任何参数,则扩展类必须有构造函数,并且必须在第一行调用基类的构造函数。在示例8.3中,由于Base::new有一个参数,Extended::new必须调用它。

示例8.3在扩展类中带有参数的构造函数

8.1.4 驱动程序类

示例8.4中的驱动程序类从生成器接收事务,并将它们驱动到DUT中。

示例8.4驱动程序类

这个类通过邮箱gen2drv从生成器接收事务对象,将它们分解为接口中的信号变化,以刺激DUT。如果生成器将BadTr对象发送到类中会发生什么?OOP规则规定,如果您有一个基类型(事务)的句柄,它也可以指向扩展类型(BadTr)的对象。句柄tr只能引用基类中的东西,比如变量src、dst、csm和data,以及方法calc_csm。因此,您可以将BadTr对象发送到驱动程序中,而无需更改驱动程序类。

参见第10章和第11章,了解具有虚拟接口和回调等高级特性的全功能驱动程序的例子。

当驱动程序调用tr.calc_csm时,将调用哪一个,事务中的那个还是BadTr中的那个?由于calc_csm在样例8.1的基类中声明为虚方法,SystemVerilog根据tr中存储的对象类型选择合适的方法。如果对象是事务类型,SystemVerilog会调用任务Transaction::calc_csm。如果是BadTr类型,则SystemVerilog调用函数BadTr::calc_csm。

8.1.5 简单的生成器类

这个testbench的示例8.5中的生成器创建了一个随机事务,并将其放入发送给驱动程序的邮箱中。下面的(错误的)示例显示了如何根据目前所学的知识创建类。注意,这避免了一个非常常见的testbench bug,因为它在每次循环中构造一个新的事务对象,而不是只在循环外部构造一次。关于邮箱的第7.6节将详细讨论此错误。

示例8.5坏生成器类

这个发电机有一个很大的限制。运行任务构造一个事务并立即将其随机化。这意味着事务使用任何默认打开的约束。唯一可以改变这一点的方法是编辑Transaction类,这违背了本书中提出的验证指南。更糟糕的是,生成器只使用事务对象——没有办法使用扩展对象,如BadTr。修复的方法是将tr的构造与它的随机化分开,如下面的8.2节所示。

在构建面向数据的类(如网络和总线事务)时,您将看到它们具有公共属性(id)和方法(display)。面向控件的类,如生成器和驱动程序类,也有共同的结构。您可以通过使这两个类都是基处理程序类的扩展(带有用于run的虚拟方法)和wrap_up来实现这一点。UVM和VMM都有一组广泛的用于事务处理、数据等的基类。

8.2. 蓝图模式

一个有用的面向对象技术是“蓝图模式”。“如果你有一台制作标识的机器,你就不需要事先知道每一个可能标识的形状。你只需要一台冲压机,然后改变模具来切割不同的形状。同样,当您想要构建事务处理生成器时,您不必知道如何构建每种类型的事务;你只需要会盖章就行了

与给定事务类似的新事务。

与样例8.5中构造然后立即使用一个对象不同的是,构造一个blueprint对象(切模),然后用constraint_mode修改它的约束,甚至用扩展对象替换它,如图8.4所示。现在,当你随机化这个蓝图时,它会有你想要的随机值。创建此对象的副本,并将该副本发送给下游事务处理程序。

图8.4蓝图模式发生器

这种技术的美妙之处在于,如果您更改blueprint对象,您的生成器将创建一个不同类型的对象。使用符号类比,你改变切割模具从一个正方形到一个三角形作出屈服符号,如图8.5所示。

图8.5带有新图案的蓝图生成器

蓝图是一个“钩子”,它允许你改变属类的行为,而不需要改变它的代码。您需要创建一个复制方法,该方法可以复制蓝图来传输,这样原始蓝图对象就会被保留下来,以供下次通过循环时使用。

示例8.6展示了使用blueprint模式的generator类。需要注意的重要一点是,blueprint对象是在一个地方构造的(新函数)

使用蓝图模式的示例8.6生成器类

并在另一个(run任务)中使用。在这本书之前的编码指南说,分开声明和构造;类似地,您需要分离blueprint对象的构造和随机化。

复制方法通过将对象的变量复制到一个新对象来复制该对象,将在5.15节和8.5节中讨论。现在,请记住必须将其添加到事务和BadTr类中。第304页的示例8.34显示了一个使用模板的高级生成器。

每当蓝图被随机化时,这个生成器都会构造一个新的事务。这种编码风格防止了典型的OOP邮箱错误,因为邮箱将存储多个惟一对象的句柄,而不是同一个对象。

反复随机化blueprint对象的另一个好处是,randc变量可以正确工作。示例8.5中的坏生成器每次通过循环都会构造新的对象。每个带有randc变量的对象都维护为该变量生成的以前值的历史记录。每当您构造一个新对象时,历史记录就会丢失,而坏生成器会创建带有独立randc变量的对象。在示例8.6中,只有blueprint对象是随机的,因此randc历史被维护。

修改蓝图的操作请参见8.2.3章节。

  1. 环境类

第一章讨论了执行的三个阶段:构建、运行和总结。样本

8.7显示了实例化所有testbench组件的环境类,并运行这三个阶段。还要注意邮箱gen2drv是如何将事务从生成器传递到驱动程序的,因此会将每个事务传递到构造函数中。

示例8.7环境类

8.2.1 一个简单的Testbench

测试包含在示例8.8中所示的顶级程序中。基本测试只是让环境以所有默认值运行。

示例8.8使用环境默认值的简单测试程序

  1. 使用扩展的事务类

要注入错误,您需要将blueprint对象从事务对象更改为BadTr。您可以在环境中的构建阶段和运行阶段之间进行此操作。示例8.9中的顶级测试工作台运行环境的每个阶段并更改蓝图。注意,对BadTr的所有引用都在这个文件中,因此不必更改环境或生成器

类。你想限制使用BadTr的范围,所以在初始块的中间使用一个独立的begin…end块。这就形成了一个视觉上与众不同的代码块。您可以采用捷径,在声明中构造扩展类。

示例8.9向testbench注入一个扩展的事务

8.2.2 使用扩展类更改随机约束

在第6章中,您学习了如何生成受限随机数据。您的大多数测试将需要进一步约束数据,这最好通过继承来完成。在示例8.10中,原始事务类被扩展为包含一个新的约束,该约束将目标地址保持在源地址的+/−100范围内。

示例8.10用一个扩展的对象替换生成器的蓝图,该对象具有额外的约束。正如你将在本章后面学到的,邻近的类应该有一个复制方法,但是在一些章节中请稍等。

示例8.10添加带有继承的约束

请注意,如果在扩展类中定义的约束与基类中的约束同名,则扩展的约束将替换基类中的约束。这允许您更改现有约束的行为。

8.3. 向下casting和虚拟方法

当您开始使用继承来扩展类的功能时,您需要一些OOP技术来控制对象及其功能。特别是,句柄可以引用特定类或任何扩展类的对象。那么,当基本句柄指向扩展的对象时,会发生什么呢?当调用基类和扩展类中同时存在的方法时会发生什么?本节用几个例子解释发生了什么。

  1. 向下类型转换美元投

向下强制转换或转换是将基类句柄强制转换为指向从该基类类型扩展的类的对象的行为。考虑一下示例8.11和图8.6中的基类和扩展类。

示例8.11基类和扩展类

图8.6简化的扩展事务

您可以将扩展句柄分配给基本句柄,不需要特殊代码,如示例8.12所示。当一个类被扩展时,所有的基类变量和方法都被包含,所以src在扩展对象中。对tr的赋值是允许的,因为任何使用基本句柄tr的引用都是有效的,例如tr.src和tr.display。

示例8.12复制扩展句柄到基本句柄

如果尝试相反的方向,如示例8.13所示,将基对象的句柄复制到扩展句柄中,会怎么样?这将失败,因为基对象缺少仅存在于扩展类中的属性,如bad_csm。SystemVerilog编译器对句柄类型进行静态检查,不会编译第二行。

示例8.13复制基本句柄到扩展句柄

将基句柄分配给扩展句柄并不总是非法的,但必须始终使用$cast。当基本句柄指向扩展对象时,允许赋值。$cast方法检查句柄引用的对象类型,而不仅仅是句柄。如果源对象与目标对象类型相同,或者是从目标的类扩展而来的类,则可以将扩展对象的地址从基句柄tr复制到扩展句柄bad2中。

示例8.14使用$cast复制句柄

当您使用$cast作为任务时,SystemVerilog在运行时检查源对象的类型,如果它与目标不兼容,则给出一个错误。当您使用$cast作为函数时,SystemVerilog仍然检查类型,但如果不匹配,则不再打印错误。当类型不兼容时,$cast函数返回0,兼容类型返回1。

作为示例8.14中if语句的替代,您可以使用6.3.2节中的SV_RAND_CHECK宏。您不应该使用immediate断言语句,因为如果禁用断言,就不会计算断言表达式,这意味着$cast和bad2赋值将永远不会执行。

8.3.1 虚拟方法

到目前为止,您应该已经习惯了将句柄与扩展类一起使用。如果您试图使用这些句柄之一调用方法,会发生什么?示例8.15和8.16展示了基类和扩展类以及调用这些类中的方法的代码。

示例8.15事务和BadTr类

样例8.16包含了一个使用不同类型句柄的代码块。

示例8.16调用类方法

为了决定调用哪个虚方法,SystemVerilog使用对象的类型,而不是句柄的类型。在示例8.16的最后一条语句中,tr指向一个扩展对象(BadTr),因此调用BadTr::calc_csm。

如果忽略事务::calc_csm上的虚拟修饰符,则SystemVerilog检查句柄tr(事务)的类型,而不是对象的类型。示例8.16中的最后一条语句调用Transaction::calc_csm——这可能不是您想要的。

面向对象的术语是多方法共享一个共同的名称“多态性”。它解决了一个问题,类似于计算机架构师试图制造一个可以处理大地址空间但只有少量物理内存的处理器时所面临的问题。他们创造了虚拟内存的概念,其中的代码和

程序的数据可以驻留在内存中或磁盘上。在编译时,程序不知道它的部件在哪里——这都是由硬件和运行时的操作系统来处理的。虚拟地址可以映射到一些RAM芯片,或磁盘上的交换文件。程序员在编写代码时不再需要担心这种虚拟内存映射——他们只知道处理器会在运行时找到代码和数据。参见Denning(2005)。

8.3.2 签名和多态性

使用虚方法有一个缺点:一旦您定义了一个虚方法,所有定义相同方法的扩展类都必须使用相同的“签名”,即:,相同的参数数目和类型,加上返回值(如果有的话)。不能在扩展的虚方法中添加或删除参数。这意味着你需要提前计划。

SystemVerilog和其他OOP语言要求虚拟方法必须具有与父方法(或祖父方法)相同的签名,这是有充分理由的。如果您能够添加额外的参数,或将任务转换为函数,那么多态性将不再起作用。您的代码需要能够调用虚方法,并保证扩展类中的方法具有相同的接口。

8.3.3 构造函数从来都不是虚的

当您调用虚方法时,SystemVerilog会检查对象的类型,以决定它应该调用基类中的方法还是扩展类中的方法。现在您可以看到为什么构造函数不能是虚的了。当您调用它时,没有对象的类型可以被检查。对象只在构造函数调用启动后才存在。

8.4. 组合、继承和替代

当您构建您的测试平台时,您必须决定如何将相关的变量和方法分组到类中。在第5章中,您学习了如何构建基本类并将一个类包含在另一个类中。在本章的前面,您看到了继承的基础知识。本节将向您展示如何在这两种样式之间做出选择,以及另一种选择。

8.4.1 决定在组合和继承之间

如何将两个相关的类联系在一起?组合使用“has-a”关系。一个数据包有一个报头和一个正文。继承使用“is-a”关系。

BadTr是一个带有更多信息的事务。表8.1是一个快速指南,下面有更多细节。

表8.1比较继承和组合

问题

继承

(是一个关系)

作文

(有关系)

  1. 你需要把多个扩展课程分组在一起吗?(SystemVerilog不支持多重继承)

  2. 高级类是否表示类似抽象级别的对象?

  3. 低级别的信息总是存在还是必需的?

  4. 在被预先存在的代码处理时,额外的数据需要保持附加到原始类吗?

没有 是的

是的 没有

是的 没有

是的 没有

  1. 是否有几个小班要合并成一个大班?例如,您可能有一个data类和一个header类,现在想要创建一个packet类。SystemVerilog不支持多重继承,即一个类同时从多个类扩展。相反,你必须使用合成。或者,您可以将其中一个类扩展为新类,并手动添加来自其他类的信息。

  2. 在示例8.15中,事务和BadTr类都是在生成器中创建并驱动到DUT中的总线事务,因此继承是有意义的。

  3. 低级信息如src、dst和数据必须始终存在,以便驱动程序发送事务。

  4. 在示例8.15中,新的BadTr类有一个新的字段bad_csm和扩展的calc_csm函数。Generator类只传输事务,而不关心附加信息。如果使用组合创建错误总线事务,则必须重写Generator类以处理新类型。

如果两个对象似乎通过“is-a”和“has-a”相互关联,那么您可能需要将它们分解为更小的组件。

8.4.2 问题的作文

构建类层次结构的经典OOP方法将功能划分为易于理解的小块。然而,testbench并不是标准的

软件开发项目,如第5.16节中讨论的公共属性与本地属性。像信息隐藏(使用局部变量)这样的概念与构建一个需要最大可见性和可控性的测试平台相冲突。类似地,将事务划分为更小的部分可能会导致比它解决的问题更多的问题。

当您创建一个类来表示一个事务时,您可能希望对它进行分割,以使代码更易于管理。例如,您可能有一个以太网MAC帧,您的测试工作台使用两种风格,普通(II)和虚拟LAN (VLAN)。使用composition,您可以创建一个基本单元格EthMacFrame,其中包含所有常见字段,如da和sa,以及一个判别变量kind,以表示示例8.17中所示的类型。还有第二个类用来保存VLAN信息,它包含在EthMacFrame中。

示例8.17构建带有组合的以太网帧

作文有几个问题。首先,它添加了额外的层次结构层,因此您必须不断地向每个引用添加额外的名称。VLAN信息称为eth_h.vlan_h.vlan。如果您开始添加更多的外行,层次名称就会成为一种负担。

当您想实例化和随机化类的层次结构时,会出现一个更微妙的问题。EthMacFrame构造函数创建了什么?由于kind是随机的,所以您不知道在调用new时是否要构造一个Vlan对象。当您随机化类时,约束会根据随机种类字段设置EthMacFrame和Vlan对象中的变量。你有一个循环的依赖关系,在随机化只工作在对象已经实例化,但你不能实例化这些对象,直到kind已被选择。

构造和随机化问题的唯一解决方案是总是实例化EthMacFrame::new中的所有对象。但是,如果您总是使用所有的替代方案,为什么要将以太网单元划分为两个不同的类呢?

8.4.3 继承的问题

继承可以解决其中的一些问题。扩展类中的变量可以不像eth_h.vlan那样具有额外的层次结构而被引用。您不需要判别器,但是您可能会发现只测试一个变量比执行示例8.18中所示的类型检查更容易。

示例8.18构建具有继承的以太网帧

缺点是,一组使用继承的类总是比一组没有继承的类需要更多的设计、构建和调试工作。当你有一个从基本句柄到扩展句柄的赋值时,你的代码必须使用$cast。构建一组虚拟方法具有挑战性,因为它们都必须具有相同的签名。如果需要额外的参数,则需要返回并编辑整个集合,可能还需要调用方法。

随机化也存在一些问题。你如何在两种框架中随机选择一个约束并设置适当的变量?不能在EthMacFrame中添加引用vlan字段的约束。

最后一个问题是多重继承。在图8.7中,你可以看到VLAN帧是如何从一个普通的MAC帧扩展过来的。问题是,这些不同的标准重新趋于一致。SystemVerilog不支持多重继承,因此无法通过继承创建VLAN / Snap / Control帧。

图8.7多重继承问题

8.4.4 一个真实的选择

如果组合会导致较大的层次结构,但继承需要额外的代码和计划来处理所有不同的类,而且两者都有难以构建和随机化的情况,那么您能做什么呢?相反,您可以创建一个包含所有变量和方法的单一平面类。这种方法会产生一个非常大的类,但是它可以干净地处理所有的变体。您必须经常使用判别变量来判断哪些变量是有效的,如示例8.19所示。它包含几个条件约束,它们适用于不同的情况,取决于kind的值。

示例8.19构建平面以太网帧

无论您如何构建类,都要在类中定义典型的行为和约束,然后使用继承在测试级别注入新的行为。

8.5. 复制一个对象

在示例8.6中,生成器首先随机化,然后复制蓝图以创建一个新事务。仔细看看示例8.20中的copy函数。有关复制函数的更多示例,请参见5.15节。

示例8.20带有虚拟复制函数的基本事务类

当您扩展事务类以使类BadTr时,复制函数仍然必须返回一个事务对象。这是因为扩展虚函数必须匹配基本事务::copy,包括所有参数和返回类型,如示例8.21所示

示例8.21使用虚拟复制方法的扩展事务类

8.5.1 指定复制的目的地

前面的复制方法总是构造一个新对象。对copy的一个改进是指定应该放置副本的位置。当您希望重用现有对象而不分配新对象时,此技术非常有用。

示例8.22带有copy函数的基本事务类

唯一的区别是指定目标的附加参数,以及测试将目标对象传递给此方法的代码。如果没有传递任何信息(默认值),则构造一个新的对象,或者使用现有的对象。

因为您已经向基类中的虚方法添加了一个新参数,所以您必须将它添加到扩展类中的相同方法中,例如BadTr。

示例8.23扩展事务类,带有新的复制函数

请注意,BadTr::copy只需要复制扩展类中的字段,并且可以使用基类方法Transaction::copy来复制自己的字段。

8.6. 抽象类和纯虚方法

到目前为止,您已经看到了带有用于执行常见操作(如复制和显示)的方法的类。验证的一个目标是创建可以跨多个项目共享的代码。如果您的公司对一组通用的类和方法进行标准化,那么在项目之间重用代码就会更容易。

像SystemVerilog这样的OOP语言有两种构造,允许您构建一个可共享的基类。第一个是抽象类,它是一个可以扩展的类,但不能直接实例化。它使用virtual关键字定义。第二种是纯虚拟方法,是一个没有主体的原型。从抽象类扩展而来的类只有在所有纯虚方法都有实体的情况下才能实例化。pure关键字指定方法声明是一个原型,而不仅仅是一个空的虚方法。纯方法没有endfunction或end- task。最后,纯虚方法只能在抽象类中声明。抽象类可以包含纯虚方法、包含或不包含主体的虚方法以及非虚方法。注意,如果你定义了一个没有主体的虚方法,也就是说里面没有代码,你可以调用它,但它会立即返回。

示例8.24显示了一个抽象类BaseTr,它是事务的基类。它从一些有用的属性开始,比如id和count。构造函数确保每个实例都有一个唯一的ID。接下来是用于比较、复制和显示对象的纯虚拟方法。

示例8.24带有纯虚方法的抽象类

可以声明BaseTr类型的句柄,但不能构造这种类型的对象。您需要扩展这个类并为所有纯虚方法提供实现。

样例8.25展示了Transaction类的定义,它是从BaseTr扩展而来的。由于事务具有所有从BaseTr扩展的纯虚拟方法的主体,所以您可以在您的testbench中构造这种类型的对象。

示例8.25事务类扩展了抽象类

抽象类和纯虚方法让您可以构建具有共同外观的testbench。这让任何工程师都能阅读你的代码并快速理解其结构。

8.7. 回调

这本书的主要指导方针之一是创建一个单一的验证环境,你可以在不做任何更改的情况下使用它进行所有的测试。关键的要求是这个testbench必须提供一个“钩子”,在这个钩子上,测试程序可以注入新的代码,而不需要修改原始的类。您的驱动程序可能想要做以下工作。

  • 注入错误

  • 下降的事务

  • 延迟的事务

  • 将此事务与其他事务同步

  • 把交易记录在记分牌上

  • 收集功能覆盖率数据

而不是试图预测所有可能的错误、延迟或气流中的扰动

对于事务,驱动程序只需要“回调”顶级测试中定义的方法。这种技术的美妙之处在于,在每个测试中,回调方法的定义都是不同的。因此,测试可以使用回调函数向驱动程序添加新的功能,而不需要编辑驱动程序类。对于一些极端的行为,比如删除事务,您需要提前在类中编写代码,但这是一个已知的模式。删除事务的原因留给回调。

司机:任务:运行;永远的开始

< pre_callback >传输(tr);

< post_callback >

结束endtask

任务pre_callback;

endtask

任务post_callback;

endtask

图8.8回拨流程

在图8.8中,驱动程序::run任务循环永远调用一个transmit任务。在发送事务之前,run调用预传输回调(如果有的话)。在发送事务之后,它调用后回调任务(如果有的话)。默认情况下,没有回调,因此运行只调用传输。

你可以让Driver::run一个虚拟方法,然后在扩展类中重写它的行为,比如MyDriver::run。这样做的缺点是,如果您需要在新方法中复制原始方法的所有代码

注入新的行为。现在,如果对基类进行了更改,则必须记住将其传播到所有扩展类。此外,您可以在不修改构造原始对象的代码的情况下注入回调函数。

8.7.1 创建一个回调

在顶级测试中创建回调任务,并从环境的最低级别驱动程序调用它。然而,驱动程序不需要对测试有任何了解——它只需要使用测试可以扩展的泛型类。示例8.27中的驱动程序使用一个队列来保存回调对象,它允许您添加多个对象。示例8.26中的基回调类是一个抽象类,在使用之前必须进行扩展。你的回调是一个任务,所以它可以有延迟。

示例8.26基回调类

示例8.27带回调的驱动程序类

注意,Driver_cbs是一个抽象类,pre_tx和post_tx不是纯虚拟方法。这是因为一个典型的回调函数只使用其中一个。如果一个类有一个没有实现的纯虚方法,OOP规则将不允许您实例化它。

回调是VMM和UVM的一部分。这种回调技术与Verilog PLI回调或SVA回调无关。

8.7.2 使用回调来注入干扰

回调的一个常见用途是注入一些干扰,例如引起错误或延迟。示例8.28中的测试工作台使用回调对象随机丢弃数据包。回调还可以用来将数据发送到记分板或收集函数覆盖值。请注意,您可以使用push_back()或push_front()将回调对象放入队列中,这取决于您希望这些对象被调用的顺序。例如,您可能希望在任何可能延迟、损坏或删除事务的任务之后调用记分板。您应该只在事务成功传输后才收集覆盖范围。

示例8.28使用回调错误注入的测试

8.7.3 快速介绍记分板

记分牌的设计取决于被测试的设计。处理原子事务(如包)的DUT可能有一个计分板,其中包含一个转换函数来将输入事务转换为期望的值,一个存储这些值的内存,以及一个比较方法。处理器设计需要一个参考模型来预测预期的输出,在模拟结束时可能会对预测值和实际值进行比较。

示例8.29显示了一个简单的记分牌,它将事务存储在一个预期值队列中。第一种方法保存一个预期的事务,第二种方法尝试查找与testbench接收到的实际事务相匹配的预期事务。注意,当您搜索一个队列时,您可以得到0个匹配(没有找到事务)、1个匹配(理想情况下)或多个匹配(您需要进行更复杂的匹配)。

示例8.29原子事务的简单记分牌

8.7.4 通过回调连接到记分板

示例8.30中的testbench创建了驱动程序回调类的自己扩展,并添加了对驱动程序回调队列的引用。请注意,计分板回调需要一个计分板句柄,以便它可以调用该方法来保存预期的事务。这个示例没有显示监视器端,它需要自己的回调来将实际事务发送到记分牌进行比较。

示例8.30测试使用回调计分板

VMM建议您对记分板和功能覆盖使用回调。监视器事务处理程序可以使用回调来比较接收到的事务和预期的事务。监视器回调也是收集关于由DUT实际发送的事务的功能覆盖的完美场所。

您可能已经想到将记分板或功能覆盖组放在事务处理程序中,并使用邮箱将其连接到测试工作台。这是一个糟糕的解决方案,原因有几个。这些测试工作台组件几乎总是被动的和异步的,所以它们只有在测试工作台有数据时才会苏醒,而且它们永远不会将信息传递给下游事务处理程序。因此,必须同时监视多个邮箱的事务处理程序是一个过于复杂的解决方案。此外,您可以从测试台上的几个点采样数据,但事务处理程序是为单个源设计的。相反,将方法放在你的记分板和覆盖类中来收集数据,并通过回调将它们连接到testbench。

UVM推荐TLM分析端口,用于连接监视器/驱动程序到记分板和功能覆盖。对这种结构的描述超出了本书的范围,但是您可以将其看作一个具有可选使用者的邮箱。

8.7.5 使用回调来调试事务处理程序

如果带有回调的事务处理程序没有按预期工作,则可以添加调试回调。您可以从添加回调来显示事务开始。如果有多个事务处理程序实例,则为每个实例创建唯一标识符。在其他回调之前和之后放置调试代码,以定位导致问题的回调。即使对于调试,您也希望避免对testbench环境进行更改。

8.8. 参数化的类

当您越来越熟悉类时,您可能会注意到,类(如堆栈或生成器)只适用于单一数据类型。本节展示如何定义使用多个数据类型的单个参数化类。

8.8.1 一个简单的栈

一个常见的数据结构是堆栈,它有push和pop方法来存储和检索数据。示例8.31展示了一个使用int数据类型的简单堆栈。

示例8.31使用int类型的堆栈

这个类的问题是它只适用于整数。如果您想为实数创建一个堆栈,则必须复制该类,并将数据类型从int更改为real。这将很快导致类的激增,如果您想添加新的操作(如遍历或打印堆栈内容),这可能会成为维护问题。

在SystemVerilog中,可以向类添加数据类型参数,然后在向该类声明句柄时指定类型。这与参数化模块类似,但比它更强大,在参数化模块中,您可以在实例化时指定总线宽度等值。SystemVerilog的参数化类类似于c++中的模板。

示例8.32是堆栈的参数化类。注意类型T是如何在第一行定义的,它的默认类型是int。

示例8.32堆栈的参数化类

向参数化类指定值的步骤称为专门化。

样例8.33声明了一个具有真实数据类型的stack类的句柄。

示例8.33创建参数化的堆栈类

生成器是一个很好的类参数化的例子。一旦为一个类定义了类,同样的结构就可以用于任何数据类型。样本

8.34使用了示例8.6中的原子生成器,并添加了一个参数

生成任意随机对象。生成器应该是验证类包的一部分。它需要指定一个默认类型,因此它使用示例8.24中的BaseTr,因为这个抽象类也应该是验证包的一部分。

示例8.34使用蓝图模式的参数化生成器类

使用示例8.25中的事务类和示例8.34中的生成器,您可以构建一个示例8.35中的简单测试平台。它启动生成器并打印前五个事务,使用示例7.40中所示的邮箱同步。

示例8.35使用参数化生成器类的简单testbench

8.8.2 共享参数化的类

当您专门化一个参数化类时,就像在示例8.33中的实际堆栈中一样,您正在创建一个新的数据类型,与任何其他专门化没有OOP关系。例如,不能使用$cast()在实变量堆栈和整数之一之间进行转换。为此,您需要一个公共基类,如示例8.36所示。

示例8.36参数化生成器类的公共基类

接下来的部分将展示更多参数化类的示例。

8.8.3 参数化的类的建议

在创建参数化类时,应该从非参数化类开始,彻底调试它,然后添加参数。这种分离减少了您的调试工作。

在创建参数化类时,事务类中的一组通用虚拟方法可以帮助您。Generator类使用copy方法,知道它总是具有相同的签名。同样,display方法允许您在事务流经testbench组件时轻松调试它们。

当您的类需要知道形参的名称和宽度时,系统函数$typename()和$bits()是有用的。$typename(T)函数返回形参类型的名称,比如int、real,或者句柄的类名。函数的作用是:返回参数的宽度。对于复杂类型(如结构和数组),它返回作为位流保存表达式所需的位数。UVM事务打印方法使用这个函数来正确地排列字段。

宏是参数化类的替代方案。例如,您可以为生成器定义一个宏,并将事务数据类型传递给它。宏比参数化类更难调试,除非编译器输出扩展代码。

如果需要定义几个共享相同事务类型的相关类,可以使用参数化类或单个大型宏。最后,如何定义类并不重要,重要的是它们包含了什么。

8.9. 静态和单例类

本节和下一节将展示在UVM和VMM中广泛使用的高级OOP概念。您可以通过阅读带有许多方法的源代码来尝试理解UVM的工厂机制,但是这一节应该可以通过一个非常简化的示例为您节省几天的实验时间。本章展示了几个替代方案,这样您就可以理解为什么UVM没有选择一个更简单的替代方案。

OOP的目标之一是消除全局变量和方法,因为生成的代码很难维护和重用。它们的名称存在于全局名称空间中,可能会导致名称空间冲突。packet_count是指TCP/IP数据包还是其他协议?相反,在包类中放置一个名为count的变量,以避免任何歧义。

8.9.1 用于打印消息的动态类

然而,有时确实需要全局变量。例如,所有的验证方法都提供了打印服务,这样你就可以过滤消息并统计错误。如果您尝试用目前所学的知识构建这样一个类,它可能类似于示例8.37。

示例8.37带有静态变量的动态打印类

这是VMM日志类的一个简化版本。VMM代码允许您通过类名和实例名以及许多其他特性来过滤消息。

示例8.38中的类使用示例8.37中的Print类打印错误消息。

示例8.38带有动态打印对象的事务处理类

Print类的最大限制是测试台上的每个组件都需要实例化它。上面的简单Print类占用的内存很少,但是像VMM这样的实际Print类可能有很多字符串和数组,消耗了大量的内存。当添加到一个事务处理程序类时,这个开销可能并不大,但可能会淹没一个小事务类,例如只有53个字节的ATM单元。

8.9.2 打印消息的单例类

构造所有这些打印对象的另一种方法是不构造任何对象。如第5.11.4节所述,可以将Print类中的方法声明为静态方法。这些方法只能引用静态变量,如示例8.39所示。

示例8.39静态打印类

既然类是静态的,您就不能再拥有每个实例的信息,比如父类的名称和实例。任何筛选都必须基于其他条件。

示例8.40带有静态打印类的事务处理类

示例8.40展示了使用Print类名调用error()方法。

这种类型的类被称为单例类,因为只有一个副本,就是在精化阶段用静态变量分配的副本。

随着您的静态类(如示例8.39中的类)变大,您必须用static关键字标记所有东西,这是一个小麻烦。接下来,在模拟时间之前分配类,即使您从未使用过它。此外,该类没有句柄,所以您不能将它传递给您的测试工作台。静态类的另一种选择是具有单个实例的单例类(或单例模式),这是一个只构造一次的非静态类。它们在一开始很难创建,但是它们可以简化程序的架构。UVM的许多课程都是单例的。

单例模式是通过创建一个类的方法来实现的,该方法在类的新实例不存在时创建一个新实例。如果一个实例已经存在,它只返回该对象的句柄。要确保不能以任何其他方式实例化对象,必须使构造函数处于保护状态。不要将其设置为局部的,因为扩展的类可能需要访问构造函数。

8.9.3 配置数据库与静态参数化类

静态类在验证中的另一个好的用途是配置参数的数据库。在模拟的开始,你随机配置你的系统。在一个小型系统中,您可以简单地将它们存储在单个类中或类的层次结构中,并根据需要在testbench中传递它们。但在某些时候,这变得太复杂了,因为句柄在层次结构中上下传递。相反,创建一个全局参数数据库,以名称为索引,您可以访问testbench中的任何位置。UVM 1.0引入了这个概念,它是下面一组示例的基础。这段代码在数据库中有一个字符串索引,而像UVM这样的真实数据库可以有一个属性名、实例名和其他值。您可以将它们连接起来,创建一个更复杂的索引字符串。

数据库的一个问题是需要在单个数据库中存储不同类型的值,比如位向量、整数、实数、枚举值、字符串、类句柄、虚拟接口等等。虽然可以找到一些常见的类型,如位向量和公共基类,但也有一些类型,如virtual

接口是唯一的,因此没有简单的方法将它们存储在公共数据库中。早期版本的OVM和UVM建议创建一个围绕虚拟接口的类包装器,但这需要额外的编码,并且是常见的bug来源。

如果为每种数据类型创建不同的数据库会怎样?您可以使用以参数名称为索引的关联数组。一个真实的数据库可能也有一个实例名,但是对于这个简单的示例,您可以将所有的名称连接在一起,形成一个索引。示例8.41展示了由全局方法组成的整数数据库的代码。

示例8.41使用全局方法配置数据库

您可以使用8.8节中的概念将其概括为一个参数化类,如示例8.42所示。

示例8.42带有参数化类的配置数据库

现在可以为整数数据库、真实数据库等构造对象。最后一个问题是数据库的每个实例都是本地的

类被实例化。示例8.43中显示的解决方案是全局化,并使其成为一个静态类,即一个具有静态属性和方法的类。

示例8.43配置数据库与静态参数化类

您可以用示例8.44测试上述代码,看看参数化类如何为每种类型创建一个新数据库。

示例8.44 Testbench用于配置数据库

通过将单例对象实现为单实例而不是静态类成员,您可以惰性地初始化单例对象,只在需要时创建它。

UVM数据库允许通配符和其他正则表达式,这需要比关联数组更复杂的查找模式。

8.10. 创建测试注册表

在实际设计中,编译测试和DUT需要花费大量时间。如果要运行100个测试,每个测试都在单独的程序块中,则需要在每次测试之前重新堆100次。这是对CPU时间的浪费,因为大多数代码都没有改变。如果您创建了100个程序块,每个程序块都有一个测试,并连接模型中的所有这些程序,那么您需要一种方法来禁用除一个程序块以外的所有程序块。最好的解决方案是将所有测试和testbench包含在一个程序块中,用DUT编译一次。本节展示如何使用Verilog命令行开关为每次运行选择一个测试。

8.10.1 使用静态方法测试注册表

本书前面的例子有一个包含一个测试的程序。对于这种新方法,每个测试都是一个单独的类,它们都在一个单独的程序块中,或者从包中导入,或者在编译时包含。测试类被构造、注册到测试注册表中,然后,在运行时,您可以在运行时选择所需的测试。这遵循早期的VMM风格。

首先,您需要一个可以扩展您的测试的基测试类。示例8.45显示了一个抽象类,它包含一个Environment类的句柄和一个纯虚拟任务,该任务是包含测试代码的方法的占位符。

示例8.45基本测试类

test registry类的核心是所有测试句柄的关联数组,以测试名称为索引。示例8.46中显示的TestRegistry类是一个只有静态变量和方法的静态类,并且从来没有构造过。get_test()方法读取Verilog命令行参数,以确定要执行哪个测试。

示例8.46测试注册表类

示例8.47展示了如何扩展TestBase来创建一个运行所有环境阶段的简单测试。示例的最后一行是调用构造函数的声明,构造函数也注册了测试。构造了所有的测试对象,但只有一个运行。

示例8.47类中的简单测试

示例8.48中的程序现在只是向测试注册中心请求一个测试对象并运行它。测试类可以在包中声明并导入,也可以在程序块内部或外部声明。

示例8.48测试类的程序块

样例8.49展示了如何创建一个测试类,通过更改生成器的蓝图来创建错误的事务来注入新行为。

示例8.49将错误事务放入生成器的测试类

这个简短的示例允许您将许多测试编译到单个模拟可执行文件中,并在运行时选择您的测试,从而节省许多重新编译。当您从少量测试开始时,这种模式很好,但是下一节将展示更强大的方法。

8.10.2 使用代理类测试注册表

前一节的测试注册表可以很好地工作于较小的测试环境,但是对于真实的项目有一些限制。首先,您需要记住构造每个测试类,否则注册表无法定位它。其次,在模拟开始时构造每个测试,即使实际上只运行一个测试。

在验证一个大型设计时,可能会有数百个测试,因此构建所有这些测试会浪费宝贵的模拟时间和内存。

考虑这个比喻。当你想买车时,你可以去经销商那里看看有哪些选择。如果只有几个变种,白色或黑色,带或不带天窗,经销商可以以很少的开销来储备每一种型号。这就是您在前一节中所看到的,其中测试注册中心有每个测试类型的对象。

如果有许多不同的型号,每一种颜色都有一种,还有诸如收音机、天窗、空调、运动包和引擎之类的变体,那该怎么办?经销商永远不可能在他的lot中拥有一种类型,因为有数百种组合。相反,他会给你看一个有所有选择的目录。您可以选择您想要的选项,工厂将根据您的规范构建一个选项。同样,测试注册表可以有很多小类,每个小类都知道如何构建一个完整的测试。小类的开销很低,所以即使有一千个对象也不会消耗太多内存。现在,当您想要运行测试N时,想象一下快速浏览目录(测试注册表),直到您找到测试的图片,然后告诉工厂构建一个该类型的对象。

测试注册表需要一个从测试名称到对象的表(类似于上面的目录)。在8.10.1节中,这个表是一个TestBase句柄的关联数组,由一个字符串索引,如示例8.46所示。相反,如果您有一个参数化的类,它唯一的工作是构造一个测试,该怎么办?UVM使用一种称为代理类的设计模式,其唯一的作用是构建实际需要的类。proxy类是轻量级的,因为它只包含一些属性和方法,因此消耗的内存或CPU时间很少。它的作用类似于汽车经销商目录中的图片,保存着您可以构建的内容的表示。

接下来的几个代码示例展示了UVM类工厂是如何工作的。由于本书中的代码是真实的UVM类的简化版本,因此名称已更改为SVM, SystemVerilog Methodology,以便您不会将其与真实的东西混淆。希望您会发现这个简单工厂的解释比试图阅读UVM源代码更容易理解。

第一个是样例8.50,它有公共基类,其他所有东西都是从这个类构建的。它是一个抽象类,因为您永远都不应该构造这种类型的对象,而只应该构造从这个类型扩展而来的类。

样本8.50常见的支持向量机基类

接下来是示例8.51中的组件类。在UVM中,组件是形成testbench层次结构的耗时对象,类似于VMM事务处理程序。在这个简化的例子中,分层父句柄已经被移除。

示例8.51组件类

现在定义svm_object_wrapper,代理类的抽象公共基类,如示例8.52所示。它有纯虚方法来返回类类型的名称,并创建该类型的对象。

示例8.52代理类的公共基类

现在,对于示例8.53中显示的关键类svm_component_registry。这是一个轻量级的类,构造它的开销很小。它由测试类类型和名称参数化。一旦您有了这个类的实例,您的testbench可以在任何时候使用create_ object方法构造实际的测试类。这是一个单例类,因为您只需要一个副本就可以创建测试类的实例。在模拟开始时,通过调用get()方法初始化静态句柄me,该方法在需要时构造第一个实例。

示例8.53参数化代理类

最后一个主要类是svm_factory,它的核心只是一个包含数组m_type_names的单例类,用于从测试用例名转换到创建测试类实例的代理类。在示例8.54中的这个类中还有get_test方法,它从模拟运行命令行读取测试名,并构造测试类的一个实例。与示例8。46不同,您甚至可以进行一些自我检查。

示例8.54工厂类

最后是一个基本测试类,由示例8.55中所示的svm_component扩展而来。它使用宏svm_component_utils定义一个新的数据类型type_id,该数据类型指向代理类。宏对包含类名的标记T进行string化,并将其转换为包含T值的字符串,语法为:' "T ' "。

示例8.55基本测试类和注册宏

样本8.56测试程序

以下是使用命令行开关+SVM_TESTNAME=TestBase启动模拟时发生的步骤。

  • 使用宏svm_component_utils,类TestBase基于类svm_component_registry定义type_id类型,参数为TestBase和"TestBase"。因为这是一种新类型,所以模拟器通过调用实例化类的get方法初始化静态变量svm_component_registry::me。这个实例在工厂中注册。这一切意味着什么?现在有了一个可以构造TestBase类的对象,您可以通过工厂访问它。

  • 现在开始模拟,工厂的get_test方法从命令行读取测试名。此字符串用于注册中心的索引,以获取代理对象的句柄。这个对象的create_object方法构造了TestBase对象的一个实例。

  • 程序调用测试对象的run_test方法,该方法调用特定类的步骤。现在,示例8.55中的TestBase类没有做任何有趣的事情,但是将对svm_component_utils宏的调用添加到示例8.47和示例8.49中的测试类中,您就可以运行测试了。

现在您可以看到启动测试的基本UVM流。注册表包含一个可以构造测试对象的代理类列表。

8.10.3 8.10.3 UVM工厂建立

UVM工厂也可以为testbench中的任何类构造对象

在示例8.53中创建方法。示例8.57展示了如何构建驱动程序。

示例8.57 UVM工厂构建示例

上面的代码调用静态方法create来构造驱动类型的对象。在UVM中,第二个参数指向正在创建的组件的父组件。

UVM工厂允许你覆盖组件,这样当你构建一个组件时,你就会得到一个扩展的组件。

您可能已经注意到术语上的变化。在经典的OOP中,根据句柄类型调用新方法来“构造”一个类,并将地址分配给赋值语句左侧的句柄。使用UVM工厂模式,您可以通过调用静态create方法来“构建”一个对象。这可以使对象具有与句柄相同的类型,或者扩展类型。

8.11. 结论

继承的软件概念,新的功能是添加到现有的类,平行的硬件实践扩展设计的特点,每一代,同时仍然保持向后兼容性。

例如,你可以通过增加更大容量的磁盘来升级你的电脑。只要它使用与旧的相同的界面,您不必替换系统的任何其他部分,但整体功能得到了改进。

同样,您可以通过“升级”现有的驱动程序类来创建一个新的测试,以注入错误。如果您在驱动程序中使用一个现有的回调,您就不必更改任何testbench基础设施。

如果您想要使用这些OOP技术,您需要提前计划。通过使用虚拟方法并提供足够的回调点,您的测试可以修改行为

在不改变其代码的情况下。其结果是一个健壮的测试平台,它不需要预测你可能想要的每种类型的干扰(错误注入、延迟、同步),只要你留下一个钩子,测试就可以注入它自己的行为。

测试台比您以前构建的要复杂得多,但是它的回报是测试变得更小,更容易编写。测试台负责发送刺激和检查响应的艰苦工作,因此测试只需要做一些小的调整就可以产生专门的行为。额外的几行testbench代码可能会取代必须在每个测试中重复的代码。

最后,OOP技术允许重用类,从而提高了工作效率。例如,操作任何其他类(而不是单一类型)的堆栈的参数化类可以省去创建重复代码的麻烦。

8.12. 练习

  1. 给定下面的类,在扩展类ExtBinary中创建一个方法,将val1和val2相乘并返回一个整数。

  1. 从练习1的解决方案开始,使用ExtBinary类进行初始化

val1=15, val2=8,并打印出相乘的值。

  1. 从练习1的解决方案开始,创建一个扩展类Exercise3

这将约束val1和val2小于10。

  1. 从练习3的解决方案开始,使用Exercise3类进行随机化

val1和val2,并打印出相乘的值。

  1. 给定练习1中的类、下面的声明和扩展类ExtBinary,在执行每个代码片段a-d后,mc、mc2和b将指向什么,或者会发生编译错误吗?

a. mc = new(15,8);b = mc;

b = new(15, 8);

mc = b;

c - mc = new(15, 8);b = mc;

mc2 = b;

d. mc = new(15, 8);b = mc;如果美元投(mc2, b))

显示美元(“成功”);其他的

显示美元(“错误:不能分配”);

  1. 给定练习1中的类Binary和Ext Binary以及以下类Binary的复制函数,创建函数Ext Binary::copy。

  1. 从解决方案到练习6,使用copy函数将扩展类句柄mc所指向的对象复制到扩展类句柄mc2。

  2. 使用文本8.7.1和8.7.2小节中的代码示例8.26到示例8.28,添加将事务随机延迟到0到100ns之间的能力。

  3. 创建一个可以使用大小写相等操作符比较任何数据类型的类,

= = =和! = =。它包含一个compare函数,如果两个值匹配,则返回1,否则返回0。缺省情况下,比较两种4位数据类型。

  1. 使用练习9中的解决方案,使用comparator类来比较两个4位值expected_4bit和actual_4bit。接下来,比较color_t类型的两个值、expected_color和actual_color。如果发生错误,则递增错误计数器。

results matching ""

    No results matching ""