引言
本文简单介绍 SystemVerilog 的类。
前文链接:
类
啥叫类 ?
类是一种用户定义的数据类型,是一种面向对象的结构,可以用于数据(参数)、操作数据的任务/函数(方法)的封装。示例如下:
上例中有几个关键点:
- function new() 称为构造函数,在创建对象时自动调用;
- this 关键字用于指向当前类,通常在类的内部使用指向它自己的参数/方法;
- display() 是一个立即执行的函数,不消耗仿真时间;
- function new() 参数具有缺省值,因此第6行代码,(空白行不算)(下图)将创建一个值为[3‘h1,0,2’h5,1]的包对象
如何获取类内信号 ?
为此,您必须创建类的一个对象,该对象可用作其属性和方法的句柄。
仿真:
如何创建类数组 ?
这和 int 类型数组方式类似。
由于每个myPacket对象在构造函数new()中没有参数,因此应用了缺省值。
啥是继承 ?
假设您希望拥有一个包含 myPacket 所有属性/方法的类,并且能够在不更改 myPacket 的情况下向其中添加更多内容,最好的方法是通过继承。在下面的示例中,networkPacket 使用 extend 关键字继承 myPacket 的属性/方法。要调用基类(myPacket)的函数,使用 super 关键字。
抽象/虚拟类是啥 ?
如果使用 virtual 关键字创建抽象类,则不能创建类的对象。如果您不希望其他人创建类的对象,而是强制用户保留抽象类作为基类并扩展它以创建子类以满足其目的,则这是很有用的。
类的句柄和对象
类的句柄是啥 ?
像下面的 pkt 这样的类变量只是知道该对象的名称。它可以保存类Packet的对象的句柄,但在分配到某个东西之前,它始终为空(null)。此时,类对象还不存在。
类句柄示例
类的对象是啥 ?
只有在调用类的new()函数时才会创建该类的实例。要再次引用该特定对象,需要将它的句柄分配给类型为Packet的变量。
类对象示例
仿真log :
两个句柄指向同一个对象会发生什么 ?
如果将 pkt 赋值给 pkt2 ,新变量也会指向与 pkt 相同的内容。
仿真log :
现在有两个句柄,pkt 和 pkt2 指向Packet 类的同一个实例。因为我们还没有为 pkt2 创建实例,只是指定了 pkt 的一个实例句柄。、
类的构造函数
构造函数可以为特定数据类型创建对象。
构造函数
当显式定义类构造函数时
C/C++需要复杂的内存分配技术,不适当的释放可能会导致内存泄漏和其他行为问题。SystemVerilog虽然不是一种编程语言,但能够简单地构造对象和自动垃圾回收。
仿真log:
在上面的示例中,变量声明创建了一个类Packet的对象,并将自动调用类中的new()函数。new()函数称为类构造函数,是使用某个值初始化类变量的一种方式。请注意,它没有返回类型,并且是非阻塞的。
当显式调用类构造函数时
如果类内没有显示定义 new() 函数,将会自动提供一个 new方法供创建对象时使用。这种情况下,addr 被初始化为0,因为bit类型的数据初值为0。
继承类的行为
派生类的方法 new 将首先使用 super.new() 调用其父类构造函数。一旦基类构造函数完成,派生类中定义的每个属性都将被初始化为默认值,之后将执行 new 中的其余代码。
仿真结果:
在上面的示例中,当创建一个子类对象时,它将首先调用子类的new()函数,当被super关键字调用时,它将从那里分支到baseClass的new()方法。接下来将初始化数据,控制返回给子类。new 方法将在id初始化为4之后完成。
当 new 函数被声明为 静态/虚拟 函数
构造函数可以声明为 local 或者 protected,但不可声明为 static 或 virtual。否则仿真器会报错,示例如下:
类型化的构造函数
这里的不同之处在于,您可以调用子类的new()函数,但在单个语句中将其分配给基类的句柄。这是通过使用范围操作符 :: 引用子类的new()函数来实现的,如下所示。
基类C的变量c现在引用了一个新构造的类型为D的对象。这实现了与下面给出的代码相同的效果。
this 指针
this 关键字用于引用当前实例的类属性、参数和方法。它只能在非静态方法、约束和覆盖组中使用。这基本上是一个预定义的对象句柄,引用用于调用使用它的方法的对象。
示例
使用它的一种非常常见的方式是在初始化块中。
除非赋值中有歧义,否则通常不需要使用此关键字来指定对方法中的类成员的访问。
super 关键字
super关键字从子类内部使用来引用基类的属性和方法。如果属性和方法已被子类覆盖,则必须使用super关键字来访问它们。
示例
super关键字只能在派生自基类的类范围内使用。下面显示的代码将有编译错误,因为 extPacket不是Packet的子级。请注意,new方法是为每个类定义隐式定义的,因此我们不需要在基类包中定义新的定义。
现在让我们看看当extPacket是类Packet的派生时的输出:
访问基类方法
在下面的示例中,基类的display方法是使用super关键字从子类的display方法中调用的。
typedef 类
有时编译器出错是因为在声明类本身之前使用了一个类变量。例如,如果两个类需要彼此的句柄,则会弹出一个经典的谜题,即鸡肉还是鸡蛋先出现。这是因为编译器处理第一个类时,它发现第二个类的引用是尚未声明的类。
编译错误
在这种情况下,您必须使用 typedef 关键字为第二个类提供一个正向声明。当编译器看到一个typedef 类时,它将知道稍后将在同一文件中找到该类的定义。
应用
通过使用typedef,DEF被声明为类 类型,后来被证明是相同的。没有必要在typedef语句中指定DEF是类 类型。
用 typedef 定义参数化的类
也可以在具有如下所示的参数化端口列表的类上使用typedef。
继承
继承是OOP中的一个概念,它允许我们扩展一个类来创建另一个类,并从新类对象的句柄访问原始父类的所有属性和方法。此方案背后的思想是允许开发人员在向新类添加新属性和方法的同时仍保持对原始类成员的访问。这允许我们在根本不接触基类的情况下进行修改。
示例
ExtPacket是扩展的,因此是Packet的子类。作为子类,它从父类继承属性和方法。如果父类和子类中都存在同名的函数,则其调用将取决于用于调用该函数的对象句柄的类型。在下面的示例中,Packet和ExtPacket都有一个名为Display()的函数。当这个函数被子类句柄调用时,子类Display()函数将被执行。如果这个函数被父类句柄调用,则父类Display()函数将被执行。
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_class ();
CLASS_PARENT CLASS_PARENT_OBJ;
CLASS_CHILD CLASS_CHILD_OBJ;
initial
begin
CLASS_PARENT_OBJ = new(32'h00112233);
CLASS_PARENT_OBJ.display1();
CLASS_CHILD_OBJ = new(32'h44556677,32'h8899AABB);
CLASS_CHILD_OBJ.display1();
end
endmodule
Questa Sim仿真结果:
当您试图将子类实例分配给基类句柄时,就会变得更加棘手。
多态性
多态允许使用基类类型的变量来保存子类对象,并直接从超类变量引用这些子类的方法。如果父类方法本质上是虚的,它还允许子类方法具有与其父类不同的定义。
父类子类赋值
类句柄其实就是一个存放子类或者父类对象的容器。了解父类如何处理保存子对象,子类如何处理保存父对象,这一点很重要。
子类赋值给基类
以上例中的示例,将子类的实例赋值给基类句柄。
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_class ();
/* CLASS_PARENT CLASS_PARENT_OBJ;
CLASS_CHILD CLASS_CHILD_OBJ;
initial
begin
CLASS_PARENT_OBJ = new(32'h00112233);
CLASS_PARENT_OBJ.display1();
CLASS_CHILD_OBJ = new(32'h44556677,32'h8899AABB);
CLASS_CHILD_OBJ.display1();
end*/
// 子类实例赋值给基类句柄测试
CLASS_PARENT CLASS_PARENT_OBJ1;
CLASS_CHILD CLASS_CHILD_OBJ1;
initial
begin
CLASS_CHILD_OBJ1 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ1 = CLASS_CHILD_OBJ1;
CLASS_PARENT_OBJ1.display1();
CLASS_CHILD_OBJ1.display1();
end
endmodule
Questa Sim执行仿真:
尽管基类指针指向子类实例,但当从基类对象调用 display1() 函数时,它仍然调用基类中的display1()函数。这是因为该函数是基于句柄的类型而不是句柄所指向的对象类型来调用的。现在,让我们尝试通过基类句柄引用一个子类成员,这样会出现编译错误。
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_class ();
/* CLASS_PARENT CLASS_PARENT_OBJ;
CLASS_CHILD CLASS_CHILD_OBJ;
initial
begin
CLASS_PARENT_OBJ = new(32'h00112233);
CLASS_PARENT_OBJ.display1();
CLASS_CHILD_OBJ = new(32'h44556677,32'h8899AABB);
CLASS_CHILD_OBJ.display1();
end*/
// 子类实例赋值给基类句柄测试
/* CLASS_PARENT CLASS_PARENT_OBJ1;
CLASS_CHILD CLASS_CHILD_OBJ1;
initial
begin
CLASS_CHILD_OBJ1 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ1 = CLASS_CHILD_OBJ1;
CLASS_PARENT_OBJ1.display1();
CLASS_CHILD_OBJ1.display1();
end*/
// 基类句柄引用子类成员
CLASS_PARENT CLASS_PARENT_OBJ2;
CLASS_CHILD CLASS_CHILD_OBJ2;
initial
begin
CLASS_CHILD_OBJ2 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ2 = CLASS_CHILD_OBJ2;
$display("data = %0h",CLASS_PARENT_OBJ2.data);
end
endmodule
编译时报错:
基类赋值给子类
将一个超集类型变量赋值给其子类类型变量是不合法的,编译时会报错。
测试代码:
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_class ();
/* CLASS_PARENT CLASS_PARENT_OBJ;
CLASS_CHILD CLASS_CHILD_OBJ;
initial
begin
CLASS_PARENT_OBJ = new(32'h00112233);
CLASS_PARENT_OBJ.display1();
CLASS_CHILD_OBJ = new(32'h44556677,32'h8899AABB);
CLASS_CHILD_OBJ.display1();
end*/
// 子类实例赋值给基类句柄测试
/* CLASS_PARENT CLASS_PARENT_OBJ1;
CLASS_CHILD CLASS_CHILD_OBJ1;
initial
begin
CLASS_CHILD_OBJ1 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ1 = CLASS_CHILD_OBJ1;
CLASS_PARENT_OBJ1.display1();
CLASS_CHILD_OBJ1.display1();
end*/
// 基类句柄引用子类成员
/* CLASS_PARENT CLASS_PARENT_OBJ2;
CLASS_CHILD CLASS_CHILD_OBJ2;
initial
begin
CLASS_CHILD_OBJ2 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ2 = CLASS_CHILD_OBJ2;
$display("data = %0h",CLASS_PARENT_OBJ2.data);
end*/
// 基类变量赋值给子类成员
CLASS_PARENT CLASS_PARENT_OBJ3;
CLASS_CHILD CLASS_CHILD_OBJ3;
initial
begin
CLASS_PARENT_OBJ3 = new(32'h44556677);
CLASS_CHILD_OBJ3 = CLASS_PARENT_OBJ3;
CLASS_PARENT_OBJ3.display1();
end
endmodule
但是,如果超类句柄引用的对象与子类变量的赋值兼容,则 $cast() 可用于将超类句柄分配给子类类型的变量。
示例如下:
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_class ();
/* CLASS_PARENT CLASS_PARENT_OBJ;
CLASS_CHILD CLASS_CHILD_OBJ;
initial
begin
CLASS_PARENT_OBJ = new(32'h00112233);
CLASS_PARENT_OBJ.display1();
CLASS_CHILD_OBJ = new(32'h44556677,32'h8899AABB);
CLASS_CHILD_OBJ.display1();
end*/
// 子类实例赋值给基类句柄测试
/* CLASS_PARENT CLASS_PARENT_OBJ1;
CLASS_CHILD CLASS_CHILD_OBJ1;
initial
begin
CLASS_CHILD_OBJ1 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ1 = CLASS_CHILD_OBJ1;
CLASS_PARENT_OBJ1.display1();
CLASS_CHILD_OBJ1.display1();
end*/
// 基类句柄引用子类成员
/* CLASS_PARENT CLASS_PARENT_OBJ2;
CLASS_CHILD CLASS_CHILD_OBJ2;
initial
begin
CLASS_CHILD_OBJ2 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ2 = CLASS_CHILD_OBJ2;
$display("data = %0h",CLASS_PARENT_OBJ2.data);
end*/
/*// 基类变量赋值给子类成员
CLASS_PARENT CLASS_PARENT_OBJ3;
CLASS_CHILD CLASS_CHILD_OBJ3;
initial
begin
CLASS_PARENT_OBJ3 = new(32'h44556677);
CLASS_CHILD_OBJ3 = CLASS_PARENT_OBJ3;
CLASS_PARENT_OBJ3.display1();
end*/
// 基类变量赋值给子类成员 ( $cast() )
CLASS_PARENT CLASS_PARENT_OBJ3;
CLASS_CHILD CLASS_CHILD_OBJ3;
initial
begin
CLASS_PARENT_OBJ3 = new(32'h44556677);
$cast(CLASS_CHILD_OBJ3,CLASS_PARENT_OBJ3);
CLASS_PARENT_OBJ3.display1();
end
endmodule
这样使用 $cast() 后,可以通过编译,但是运行时会报错。这是因为基类指针没有指向与子类相兼容的对象。
下面我们尝试将基类指针指向另一个子类对象,然后做与上相同的事情。此时的基类指针就像一个承载器。
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_class ();
/* CLASS_PARENT CLASS_PARENT_OBJ;
CLASS_CHILD CLASS_CHILD_OBJ;
initial
begin
CLASS_PARENT_OBJ = new(32'h00112233);
CLASS_PARENT_OBJ.display1();
CLASS_CHILD_OBJ = new(32'h44556677,32'h8899AABB);
CLASS_CHILD_OBJ.display1();
end*/
// 子类实例赋值给基类句柄测试
/* CLASS_PARENT CLASS_PARENT_OBJ1;
CLASS_CHILD CLASS_CHILD_OBJ1;
initial
begin
CLASS_CHILD_OBJ1 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ1 = CLASS_CHILD_OBJ1;
CLASS_PARENT_OBJ1.display1();
CLASS_CHILD_OBJ1.display1();
end*/
// 基类句柄引用子类成员
/* CLASS_PARENT CLASS_PARENT_OBJ2;
CLASS_CHILD CLASS_CHILD_OBJ2;
initial
begin
CLASS_CHILD_OBJ2 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ2 = CLASS_CHILD_OBJ2;
$display("data = %0h",CLASS_PARENT_OBJ2.data);
end*/
/*// 基类变量赋值给子类成员
CLASS_PARENT CLASS_PARENT_OBJ3;
CLASS_CHILD CLASS_CHILD_OBJ3;
initial
begin
CLASS_PARENT_OBJ3 = new(32'h44556677);
CLASS_CHILD_OBJ3 = CLASS_PARENT_OBJ3;
CLASS_PARENT_OBJ3.display1();
end*/
// 基类变量赋值给子类成员 ( $cast() )
/* CLASS_PARENT CLASS_PARENT_OBJ3;
CLASS_CHILD CLASS_CHILD_OBJ3;
initial
begin
CLASS_PARENT_OBJ3 = new(32'h44556677);
$cast(CLASS_CHILD_OBJ3,CLASS_PARENT_OBJ3);
CLASS_PARENT_OBJ3.display1();
end*/
// 基类变量赋值给子类成员 ( $cast() + 指针转移 )
CLASS_PARENT CLASS_PARENT_OBJ3;
CLASS_CHILD CLASS_CHILD_OBJ3,CLASS_CHILD_OBJ4;
initial
begin
CLASS_PARENT_OBJ3 = new(32'h44556677);
CLASS_CHILD_OBJ3 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ3 = CLASS_CHILD_OBJ3;
$cast(CLASS_CHILD_OBJ4,CLASS_PARENT_OBJ3);
CLASS_CHILD_OBJ4.display1();
$display("data = %0h",CLASS_CHILD_OBJ4.data);
end
endmodule
Questa Sim 仿真结果:
虚拟方法
父类中的方法可以声明为虚拟方法,这样支持其子类以不同的定义覆盖此虚拟方法。但是包含返回类型和表达式的原型应该保持相同。
虚拟方法
在继承中,我们看到指向子类实例的基类句柄调用的方法最终将执行基类方法,而不是子类中的方法。如果基类中的函数被声明为虚的,那么只有子类方法将被执行。
示例
没有virtual
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_virtual ();
// 子类实例赋值给基类句柄测试
CLASS_PARENT CLASS_PARENT_OBJ1;
CLASS_CHILD CLASS_CHILD_OBJ1;
initial
begin
CLASS_CHILD_OBJ1 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ1 = CLASS_CHILD_OBJ1;
CLASS_PARENT_OBJ1.display1();
end
endmodule
仿真结果:
有virtual
// 基类定义
class CLASS_PARENT ;
int addr;
function new (int addr);
this.addr = addr;
endfunction
virtual function void display1();
$display("[Base Class] addr = 0x%0h",addr);
endfunction
endclass
// 子类定义
class CLASS_CHILD extends CLASS_PARENT;
int data;
function new (int addr,data);
super.new(addr);
this.data = data;
endfunction
function void display1();
$display("[Sub Class] addr = 0x%0h , data = 0x%0h",addr,data);
endfunction
endclass
// TestBench
module test_virtual ();
// 子类实例赋值给基类句柄测试
CLASS_PARENT CLASS_PARENT_OBJ1;
CLASS_CHILD CLASS_CHILD_OBJ1;
initial
begin
CLASS_CHILD_OBJ1 = new(32'h44556677,32'h8899AABB);
CLASS_PARENT_OBJ1 = CLASS_CHILD_OBJ1;
CLASS_PARENT_OBJ1.display1();
end
endmodule
仿真结果:
由上面的仿真对比可以看出,当基类的 display1() 函数声明为 virtual 时,子类的 display1() 函数被执行。
关键的要点是,您应该始终将基类方法声明为虚拟的,以便已经存在的基类句柄现在将引用子类中的函数覆盖。
静态变量/函数
每一个类的实例都会拷贝一份其内部变量。
静态变量
当类中的变量被声明为静态时,该变量将是所有类实例中唯一的副本。为了演示示例,我们将比较静态计数器和非静态计数器。静态计数器使用static关键字声明并命名为static_ctr,而普通计数器变量则命名为ctr.这两个计数器都将在new()函数中递增,以便每次创建对象时都会更新它们。
仿真结果:
您将看到静态计数器在所有类对象 p1、p2 和 p3 之间共享,因此在创建三个包时将递增到3。另一方面,普通计数器变量 ctr 没有声明为静态,因此每个类对象都有自己的副本。这就是为什么在创建所有三个对象后 ctr 仍然是1的原因。
如果您想知道在特定时间之前生成的数据包总数,则将变量声明为静态变量非常有用。
静态函数
静态方法遵循所有类作用域和访问规则,但唯一的区别是它可以在类外调用,而不需要类实例化。静态方法(方法)不能访问非静态成员,但它可以直接访问静态类属性或调用同一类的静态方法。此外,静态方法不能是虚的。使用类名的静态函数调用需要通过作用域运算符 :: 进行
现加入一个非静态成员 mode ,并且从静态函数中调用它。会编译出错。
对象拷贝
浅拷贝
当pkt与新对象的new()构造函数一起使用时,pkt中的内容将被复制到pkt2中。
这种方法被称为浅复制,因为所有变量都是跨整数、字符串、实例句柄等复制的,但嵌套对象不是完全复制的。只有它们的句柄将被分配给新对象,因此两个包将指向相同的嵌套对象实例。为了说明这一点,我们来看一个例子。
// ---- ---- 类定义
class Header;
int id;
function new (int id);
this.id = id;
endfunction
function void show_id();
$display("id = 0x%0h",id);
endfunction
endclass
class Packet;
int addr;
int data;
Header Header_OBJ;//类内嵌套 Header 类
function new(int addr,int data,int id);
Header_OBJ = new(id);
this.addr = addr;
this.data = data;
endfunction
function void display(string name);
$display("[%s] addr = 0x%0h data = 0x%0h id = %0d",name,addr,data,Header_OBJ.id);
endfunction
endclass
// ---- ---- TEST BENCH
module TEST_SHALLOW_COPY();
Packet p1,p2;
initial
begin
p1 = new(32'h11223344,32'h55667788,30);
p1.display("p1");
p2 = new p1;// 将 p1 浅拷贝至 p2
p2.display("p2");
// 修改 p1 的内容
p1.addr = 32'hAABBCCDD;
p1.data = 32'hEEFF0011;
p1.Header_OBJ.id = 60;
p1.display("p1");
// 此时 p2 嵌套类的对象 Header_OBJ 已经指向 p1 的 Header_OBJ,但是 p2 的 addr 和 data 均未改变。
p2.display("p2");
end
endmodule
Questa Sim编译/仿真结果:
类包包含一个名为Header的嵌套类。首先,我们创建了一个名为p1的包,并为其赋值。然后,使用浅层复制方法将p2创建为p1的副本。为了证明只复制句柄而不是整个对象,修改了p1包的成员,包括嵌套类中的成员。当打印p2中的内容时,我们可以看到嵌套类中的id成员保持不变。
深拷贝
深度复制是复制所有内容(包括嵌套对象)的地方,通常需要特定代码来实现此目的。
让我们在上面给出的示例中的数据包类中添加一个名为copy()的定制函数。
// ---- ---- 类定义
class Header;
int id;
function new (int id);
this.id = id;
endfunction
function void show_id();
$display("id = 0x%0h",id);
endfunction
endclass
class Packet;
int addr;
int data;
Header Header_OBJ;//类内嵌套 Header 类
function new(int addr,int data,int id);
Header_OBJ = new(id);
this.addr = addr;
this.data = data;
endfunction
function void display(string name);
$display("[%s] addr = 0x%0h data = 0x%0h id = %0d",name,addr,data,Header_OBJ.id);
endfunction
// 定制 copy 函数
function void copy (Packet p);
this.addr = p.addr;
this.data = p.data;
this.Header_OBJ.id = p.Header_OBJ.id;
endfunction
endclass
// ---- ---- TEST BENCH
module TEST_DEEP_COPY();
Packet p1,p2;
initial
begin
p1 = new(32'h11223344,32'h55667788,30);
p1.display("p1");
p2 = new(1,2,3);
p2.copy(p1);// 将 p1 深拷贝至 p2
p2.display("p2");
// 修改 p1 的内容
p1.addr = 32'hAABBCCDD;
p1.data = 32'hEEFF0011;
p1.Header_OBJ.id = 60;
p1.display("p1");
// 此时 p2 嵌套类的对象 Header_OBJ 已经指向 p1 的 Header_OBJ,但是 p2 的 addr 和 data 均未改变。
p2.display("p2");
end
endmodule
仿真结果:
注意,我们在这里调用了定制的Copy()函数,而不是浅层的复制方法,因此Header对象的内容也应该被复制到p2中。
请注意,即使更改了p1的id字段,对象p2的id仍然保持先前的值。
参数化的类
为啥需要参数化类?
有时,编写一个泛型类(通用类)会容易得多,它可以以多种方式实例化,以实现不同的数组大小或数据类型。这避免了需要为大小或类型等特定特性重写代码,而是允许为不同的对象使用单一规范。这是通过将 SystemVerilog 的参数机制扩展到类来实现的。
参数类似于指定类的本地常量。允许类具有类实例化期间可以覆盖的每个参数具有默认值。
语法
示例
参数化的类
下面给出的是一个参数化类,它的大小是可以在实例化期间更改的参数。
将数据类型作为参数传递
在这种情况下,数据类型是参数化的,并且可以在实例化期间被覆盖。在前一种情况中,我们定义了具有特定值的参数。
请注意,任何类型都可以作为参数提供,包括用户定义的类型,如类或结构体。
extern
类定义可能会变得很长,因为 class 和 endclass 之间有很多行。这使得很难理解类中存在的所有函数和变量,因为每个函数和任务都占用了相当多的行。
在方法声明中使用 extern 限定符表示实现是在这个类的主体之外完成的。
示例
local
声明为本地成员的成员仅对同一类的方法可用,并且不能由子类访问。但是,访问本地成员的非本地方法可以由子类继承和重写。
示例
在下面的示例中,我们将声明两个变量-一个是公共的,另一个是本地的。当从类外部的某个地方访问类的本地成员时,我们预计会看到一个错误。这是因为关键字 local 用于使成员仅在同一个类内可见。
不出所料,编译器会发出一个编译错误,指向从类外部访问本地成员的行。
在上面的示例中,我们可以删除导致编译错误的行,并看到我们得到了良好的输出。唯一访问本地成员的其他函数是display()函数。
当被子类访问时
在本例中,让我们尝试从子类中访问本地成员。我们预计会在这里看到错误,因为本地成员对子类也是不可见的。
抽象类
SystemVerilog禁止直接实例化声明为虚的类,它被称为抽象类。
语法
然而,这个类可以被扩展以形成其他子类,然后这些子类可以被实例化。这对于强制测试用例开发人员总是扩展一个基类来形成另一个类以满足他们的需要很有用。因此,基类通常被声明为虚的,尽管这不是强制的。
正常类的示例
抽象类的示例
让我们将基类声明为虚拟类,使其成为一个抽象类,看看会发生什么。
仿真器报告编译错误,如下所示,因为不允许实例化抽象类。
扩展抽象类
抽象类可以像任何其他SystemVerilog类一样使用如下所示的扩展关键字进行扩展。
从下面的仿真输出可以看出,扩展抽象类以形成可以使用new()方法实例化的其他类是完全有效的。
纯虚拟方法/函数
抽象类中的虚方法可以用关键字 pure 声明,称为纯虚拟方法。这样的方法只需要在抽象类中指定一个原型,而实现则留给子类中的定义。
示例
纯虚方法原型及其实现应该具有相同的参数和返回类型。
随机化
为啥需要随机化?
开发定向测试需要很长时间,因为您必须考虑所有可能的情况来验证不同的功能。您很有可能会遗漏某些角例。因此,我们希望能够生成落在有效范围内的随机值,并将这些随机值应用于我们感兴趣的信号。
为什么我们不能有任意随机值?
简单地运行随机化测试没有多大意义,因为会有许多无效的情况。我们创建有效配置的随机化测试的方法是使用约束。这种验证风格通常被称为约束随机验证(CRV)。
SV中随机化怎么做到?
要在变量上启用随机化,您必须将变量声明为 rand 或 randc 。这两者之间的区别在于,randc 本质上是循环的,因此在随机化之后,只有在应用了所有其他值之后才会再次选取相同的值。如果随机化成功,randomize()将返回1,否则返回0。
我们可以通过使用 assert() 函数确保随机化成功。这将避免运行模拟垃圾值,除非我们仔细查看,否则无法计算出这些值。
请注意,Mode 的随机化出现了重复值,而对于 Key ,这些值本质上是循环的(3,4,5,6是一个完整的集合)。
有哪些不同的约束风格?
您可以用多种方式编写约束。约束不应该相互冲突,否则随机化将在运行时失败。
如下给出简单示例:
在下一篇博文,详细说明约束的写法。