类的三要素:封装和继承还有虚方法
父类里面所有的变量和方法,子类都继承了,比如super.def
正是由于类的多态性,使得用户在设计和实现类时,不需要担心句柄指向的对象类型是父类还是子类,只要通过虚方法,就可以实现动态绑定(dynamic binding),或者在SV中称之为动态方法查找(dynamic method lookup)。
继承
继承允许从一个现在的类得到一个新的类并共享变量和子程序。原始类被称为基类或者超类,而新类因为它扩展了基类的功能,被称为扩展类。继承通过增加新的特性提供了可重用性,并且不需要修改基类。OOP真正强大的地方在于它使你能够继承现有类,例如一个事务类,并且可以通过替换其子程序有选择性地改变其部分行为,但是却不修改基础结构。
扩展基类
//事务基类
class Transaction;
rand bit[31:0]src,dst,data[8];//随机变量
bit[31:0]crc;//计算得到的CRC值
virtual function void calc_crc;
crc=src*dstAdata.xor;
endfunction
virtual function void display(input string prefix="");
$display("%sTr:src=%h,dst=%h,crc=sh",prefix,src,dst,crc);
endfunction
endclass
扩展的transaction类
class BadTr extends Transaction;
rand bit bad_crc;
virtual function void calc_crc;
super.calccrc();//计算正确的CRC
if(bad_crc)crc=~crc;//产生错误的CRC位
endfunction
virtual function void display(input string prefix="");
$write("%sBadTr:bad_crc=%b,",prefix,bad_crc);
super.display();
endfunction
endclass:BadTr
BadTr类可以直接访问Transaction原始类和其本身的所有变量,例如扩展类中的calc_crc函数通过使用super前缀调用基类中的calc_crc函数。
System Verilog 不允许用类似super.super.new的方式进行多层调用。因为这种调用风格跨越了不同的层次、不同的边界,自然也违反了封装的规则。
应该将类中的子程序定义成虚拟的,这样它们就可以在扩展类中重定义。这一点适用于所有的任务和函数,除了new函数。 因为new函数在对象创建时调用,所以无法扩展。System Verilog始终基于句柄类型来调用new函数。
更多的OOP术语
OOP中类的变量称为属性(property),而任务或者函数称为方法(method)。扩展一个类的时候,原始类(例如Transaction)被称为父类或者超类。扩展类(badTr)叫做派生类或者子类。基类是不从任何其他类派生得到的类。子程序的原型(prototype)是指明了参数列表和返回类型(如果存在)的第一行。当把子程序体移到类外的时候需要用到原型,在描述子程序如何跟其他子程序通信的时候也需要用到原型(在类之外定义方法)。
类型转换$cast
句柄能够指向一个类的对象或者任何它的扩展类的对象。所以当一个基类句柄指向一个扩展类对象的时候会发生什么?
使用$cast作类型向下转换
类型向下转换或者类型变换是指将一个指向基类的指针转换成一个指向派生类的指针;
class Transaction;
rand bit[31:0] src;
virtual function void display(input string prefix-");
$display("%s Transaction: src=%0d", prefix, src);
endfunction
endclass
class BadTr extends Transaction;
bit bad_crc;
virtual function void display(input string prefix="");
$display("%s BadTr: bad_crc=%b", prefix, bad_crc);
super. display(prefix);
endfunction
endclass
Transaction tr;
BadTr bad,bad2;
可以将一个派生类句柄赋值给一个基类句柄,并且不需要任何特殊的代码,如下例:
//将一个扩展类的句柄拷贝成基类句柄
Transaction tr;
BadTr bad;
bad=new(); //构建BadTr扩展对象
tr=bad; //基类句柄指向扩展对象
$display(tr.src); //显示基类对象的变量成员
tr.display; //调用BadTr::display
当一个类被扩展时,所有的基类变量和方法将被继承,所以整数变量src存在于扩展对象中。上例的第二个赋值探作是允许的,因为任何使用基类句柄tr的引用都是合法的,例如tr.src和tr.display。
但是若反方向的赋值,即将一个基类对象持贝到一个扩展类的句柄中时,这种操作会失败,因为有些属性仅存在于扩展类中,基类并不具备,例如bad_crc。System Verilog编译器对句柄类型作静态检查,因此不会被编译。
//将一个基类句柄拷贝到一个扩展类句柄
tr=new();//创建一个基类对象
bad=tr;//ERROR:这一行不会被编译
$display(bad.bad_crc);//基类对象不存在bad_crc成员
将一个基类句柄赋值给一个扩展类句柄并不总是非法的。当基类句柄确实指向一个派生类对象时是允许的,$cast子程序会检查句柄所指向的对象类型,而不仅仅检查句柄本身。一旦源对象跟目的对象是同一类型,或者是目的类的扩展类,你就可以从基类句柄tr中拷贝扩展对象的地址给扩展对象的句柄bad2了
//使用$cast拷贝句柄
bad-new();//构建BadTr扩展对象
tr=bad;//基类句柄指向扩展对象
$cast(bad2,tr);//如果成功,bad2就指向tx所引用的对象
虚方法
在为父类定义方法时,如果该方法日后可能会被覆盖或者继承,那么应该声明为虚方法,虚方法通过virtual离明,只需要声明一次即可。例如上面代码中,只需要将basic_test::test声明为vtrtual,而其子类则无需再次声明,当然再次声明来表明该方法的特性也是可以的。
class Transaction;
rand bit[31:0]src,dst,data[8];//变量
bit[31:0]crc;
virtual function void calc_crc();//异或所有的域
crc=src*dstAdata.xor;
endfunction
endclass:Transaction
class BadTr extends Transaction;
rand bit bad_crc;
virtual function void calc_crc();
super.ca1c_crc();//计算正确的CRC
if(bad_crc)crc=~crc;//产生错误的CRC位
endfunction
endclass:BadTr
//下面是不同类型句柄的一个代码块
Transaction tr;
Badrr bad;
initial begin
tr=new();
tr.calc_crc();//调用Transaction::calc_crc
bad=new();
bad.calc_crc();//调用Badrr::calc_crc
tr=bad;//基类句柄指向扩展对象
tr.calc_crc();//调用Badrr::calc_crec
end
当需要决定调用哪个虚方法的时候,System Verilog根据对象的类型,而非句柄的类型来决定调用什么方法。 在上例最后的语句中,tr指向一个扩展类对象(BadTr),所以调用的方法是BadTr::calc_crc。
如果没有对calc_crc使用virtual修饰符,SystemVerilog会根据句柄的类型tr,而不是对象的类型,就会导致最后那个语句调用Transaction:calc_crc。
回调函数
理想的验证环境是在被移植做水平复用或者垂直复用时,应当尽可能少地修改模块验证环境本身,只在外部做少量的配置,或者定制化修改就可以嵌入到新的环境中。
要做到这一点,一方面我们可以通过顶层环境的配置对象自顶向下进行配置参数传递,另外一方面我们可以在测试程序不修改原始类的情况下注入新的代码。
例如,当我们需要修改stimulator的行为时,有两种选择,一个是修改父类,但针对父类的会传播到其它子类;另外一个选择是,在父类定义方法时,预留回调函数入口,使得在继承的子类中填充回调函数,就可以完成对父类方法的修改。
可以将Driver::run定义为一个虚方法,然后在可能的扩展类MyDriver::run中覆盖其行为。这样做的硬点是如果你想增加新的行为,可能需要在新方法中重复原方法的所有代码。一旦对基类作了修改,需要记住将它传播到所有派生类中去。此外,你可以增加一个回调任务而无需修改构成原对象的代码。
创建一个回调函数:一个回调任务应该在顶层测试中创建,在环境中的最低级即驱动器中调用。驱动器无须知道关于测试的任何信息一它只需要使用一个可以在测试中扩展的通用类。驱动器使用一个队列来保存回调对象,这样就可以增加多个对象。回调基类是一个抽象类,使用前必须先进行扩展。
参数化的类
随着对类越来越熟悉,注意到一个执行一系列动作的数据结构(如一个堆栈或者一个发生器)只对一种数据类型有效。如何定义一个类以用于处理多种数据类型?
参数化的使用是为了提高代码的复用率。无论是设计还是验证,如果代码会被更多的人使用或者被更多的项目所采用,那么就需要考虑使用参数来提高复用率。参数的使用越合理,后期维护的成本就会相应降低。
在硬件设计中,参数往往是整形,例如端口数目或者位宽。在验证环境中,参数的使用更加灵活,可以使用各种类型来做类定义时的参数。
在SV中,可以为类增加若干个数据类型参数,并在声明类句柄的时候指定类型。
示例
//使用参数化发生器类的简单测试平台
program automatic test;
initial begin
Generator#(Transaction)gen;
mailbox gen2drv;
gen2drv=new(1);
gen=new(gen2drv);
fork
gen.run();
repeat(5)begin
Transaction tr;
gen2drv.peek(tr);//获取下一个事务
tr.display();
gen2drv.get(tr);//删除事务
end
join_any
end
endprogram// test
一些关于参数化的类的建议
在建立参数化类的时候,应当从非参数化类开始,仔细地调试,然后增加参效。这种分开的做法可以减少之后的调试时间。
宏是参数化类的一种替代形式。例如,可以为发生器定义一个宏,然后用它传递事务数据类型。宏相对于参数化的类来说更难调试。
如果需要定义若干相关的类使它们共享相同的事务类型,可以使用参数化类或者是一个大的宏。总之,传入类的类型比类的定义更加重要。
事务类中的通用虚方法集可以帮助创建参数化类。例如Generator 类使用copy方法,并总是使用相同的签名。类似地,当事务穿过测试平台组件时,display方法可以更方便地对它们进行调试。