第八章. 面向对象编程的高级技巧指南
8.1 继承简介
8.1.1 事务基类
可以定义一个事务基类,包含一些变量和子程序。calc_crc函数被定义为virtual,这样就可以在需要的时候重新定义(重写)。
class Transaction;
rand bit[31:0] src, dst, data[8];
bit [31:0] crc;
virtual function void calc_crc;
crc = src^dst^data.xor;
endfunction
virtual function void display(input string prefix="");
$display("%sTr:src=%h, dst=%h, crc=%h", prefix, src, dst, crc);
endfunction
endclass
如果要产生错误的crc,可以直接从事务基类中继承,调整calc_crc函数即可。
class BadTr extends Transaction;
rand bit bad_crc;
virtual function void calc_crc;
super.calc_crc();
if(bad_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的变量。但是SV不允许用super.super.new的方式进行多层调用。因为这种调用风格跨越了不同的层次,也跨越了不同的边界,自然也违反了封装的规则。重写适用于除了new函数外的其它函数,new函数在对象创建时调用,所以无法拓展。SV始终基于句柄类型调用new函数。
8.1.2 扩展类的构造函数
当启动扩展类时,需要牢记,如果基类构造函数有参数,那么扩展类必须有一个构造函数而且必须在其构造函数的第一行调用基类的构造函数。
class Base1;
int var;
function new(input int var);
this.var = ~var;
endfunction
endclass
class Extended extends Base1;
function new(input int var);
super.new(var);
endfunction
endclass
8.2 蓝图(Blueprint)模式
OOP中一种常用的技术,当你想要构建一个事务发生器的时候,在验证环境中预留一个父类Transaction的指针,通过子类Transaction继承自父类Transaction来修改Transaction中的约束和变量,在实例化验证平台时,可以根据业务需要实例化父类Transaction还是子类Transaction来调整Transaction的类型,这种技术就称为蓝图模式。可以简单理解为“换汤不换药”,框架(父类指针)不变,实例改变(父类实例或子类实例)。
class Generator;
mailbox gen2drv;
Transaction blueprint; //蓝图模式预留父类句柄
function new(input mailbox gen2drv);
this.gen2drv=gen2drv;
blueprint=new();
endfunction
task run;
Transaction tr;
forever
begin
assert(blueprint.randomize);
tr=blueprint.copy();
gen2drv.put(tr);
end
endtask
endclass
//env类
class Environment;
Generator gen;
Driver drv;
mailbox gen2drv;
function void build;
gen2drv=new();
gen=new(gen2drv);
drv=new(gen2drv);
endfunction
task run();
fork
gen.run();
drv.run();
join_none
endtask
endclass
//测试平台
program automatic test;
Environment env;
initial
begin
env=new();
env.build();
env.run();
end
endprogram
//使用拓展的Transaction类替换gen中的Transaction句柄blueprint指向的对象
program automatic test;
Environment env;
initial
begin
env=new();
env.build();
//使用BadTr类实例替换env中gen的blueprint句柄,发送bad类型对象
begin
BadTr bad=new();
env.gen.blueprint=bad;
end
env.run();
end
endprogram
//还可以使用拓展类Transaction修改约束
//Nearby继承自Transaction类型,修改约束
class Nearby extends Transaction;
constraint c_nearby
{
dst inside {[src-100:src+100]};
}
endclass
program automatic test;
Enviroment env;
initial
begin
env=new();
env.build();
//测试平台中使用Nearby实例替换blueprint指向的实例
begin
Nearby nb=new();
env.gen.blueprint=nb;
end
env.run();
end
endprogram
8.3 类型向下转换(downcasting)和虚方法
8.3.1 向上类型转换
派生类使用句柄赋值给基类句柄,并不需要任何特殊的代码(向上类型转换)。此时基类句柄可以访问基类的成员变量和成员方法,也可以访问派生类的成员变量和方法。
8.3.2 使用$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("%sBadTr:bad_crc=%b", prefix, bad_crc);
super.display(prefix);
endfunction
endclass
Transaction tr;
BadTr bad;
bad = new();
tr = bad; //基类句柄指向拓展对象
$display(tr.src);
tr.display;
tr = new();
bad = tr; //错误,将基类实例赋值给子类句柄
$display(bad.bad_crc); //基类不包含bad_crc
//使用$cast拷贝句柄
BadTr bad2;
bad = new();
tr = bad;
//tr句柄指向的类型必须是BadTr类型或者BadTr的子类型,否则$cast转换失败报错并返回0
if(!$cast(bad2, tr)) $display("cannot assign tr to bad2");
$display(bad2.bad_crc);
当你将$cast作为一个任务来使用的时候,SV会在运行时检查源对象类型,如果跟目的对象类型不匹配则报错。如果类型不匹配,则$cast函数返回0,**如果类型匹配(基类指针指向的是子类的对象或者子类对象的派生类时,此时基类指针做向下类型转换时成功)**则返回非0值。
8.3.3 虚方法
当需要决定调用哪个虚方法的时候,SV根据对象的类型,而非句柄的类型来决定调用什么方法。(拓展:当基类句柄指向基类实例和拓展类实例时会根据实例的类型选择调用方法,叫做多态)
当方法没有使用virtual修饰时,SV会根据句柄的类型,而不是对象的类型。
方法的签名:包括方法名,参数的类型和个数,返回值类型。实现虚方法必须具有相同的签名。
8.4 合成,继承和其他替代方法
包含是“has-a”关系。包含存在的问题是:使用包含方法增加了一个层次,每次引用增加一个额外的名字。
继承是“is-a”关系。继承的问题:类的设计,创建和调试难度增加。
SV中不支持多继承(子类继承自多个父类)。
8.5 对象的复制
class Transaction;
rand bit [31:0] src, dst, data[8];
bit[31:0] crc;
//copy函数创建了一个与函数名同名的copy Transactin类实例,将类中变量拷贝纸copy内,并返回copy实例(隐式返回,无return语句)
virtual function Transaction copy();
copy = new();
copy.src = src;
copy.dst = dst;
copy.data = data;
copy.crc = crc;
endfunction
endclass
//当拓展Transcation类来创建BadTr类的时候,copy函数仍然需要返回一个Transaction对象,这是因为扩展类的虚函数必须要跟基类的Transaction::copy函数相匹配,包括所有的参数和返回值类型
class BadTr extends Transaction;
rand bit bad_crc;
virtual function Transaction copy();
BadTr bad;
bad = new();
bad.src=src;
bad.dst=dst;
bad.data=data;
bad.crc=crc;
bad.bad_crc=bad_crc;
return bad;
endfunction
endclass
8.5.1 优化Copy函数
将copy哈数拆分成两个,创建一个独立的函数copy_data。这样每个类只负责拷贝其各自的局部变量,这使得copy函数更加健壮,重用性更高。
class Transaction;
rand bit [31:0] src, dst, data[8];
bit [31:0] crc;
virtual function void copy_data(input Transacion tr);
copy.src=src;
copy.dst=dst;
copy.data=data;
copy.crc=crc;
endfunction
virtual function Transaction copy();
copy=new();
copy_data(copy);
endfunction
endclass
class BadTr extends Transaction;
rand bit bad_crc;
virtual function void copy_data(input Transaction tr);
BadTr bad;
super.copy_data(tr); //复制基类数据
$cast(bad, tr); //将基类句柄向下转换为子类句柄
bad.bad_crc=bad_crc; //复制子类数据
endfunction
virtual function Transaction copy();
BadTr bad;
bad = new();
copy_data(bad); //传入一个子类实例,使用copy_data进行数据复制
return bad; //返回复制过数据的子类实例
endfunction
endfunction
8.5.2 指定复制的目标
8.5.1中的copy函数总会创建一个新的对象。另一种copy函数的改建方法是指定复制对象的存放地址。当你要重用一个现有的对象而不是分配一个新的对象时,可以使用此方法。
class Transaction;
virtual function Transaction copy(Transaction to = null);
if(to == null) copy = new();
else copy = to;
//copy_data方法与8.5.1中相同
copy_data(copy);
endfunction
endclass
此种方法唯一的不同之处就是,用于指定目标的额外参数。如果没有传入任何值(默认情况),就会创建一个新的对象,否则就会使用现有的对象。
//含有新copy函数的扩展事务类
class BadTr;
virtual function Transaction copy(Transaction to = null);
BadTr bad;
if(to == null) bad=new();
else assert($cast(bad, to));
copy_data(bad);
return bad;
endfunction
endclass
8.6 抽象类和纯虚方法
验证的目标就是创建可以为多个项目共享的代码。OOP语言,如SV允许你使用两种构造方法来创建一个可以共享的基类。
第一种是抽象类,即可以被扩展但是不能被直接实例化的类,使用virtual关键字定义。一个由抽象类拓展而来的类只有在所有虚方法都有实体的时候才能被例化。
第二种即纯虚方法(pure virtual),这是一种没有实体的方法原型。关键字pure表明一个方法声明是原型定义,而不仅仅是空的虚方法。纯虚方法只能在抽象类中定义,但是抽象类中也可以定义非纯方法。
//使用纯虚方法的抽象类
virtual class BaseTr;
static int count;
int id;
function new();
id=count++;
endfunction
pure virtual function bit compare(input BaseTr to);
pure virtual function BaseTr copy(input BaseTr to = null);
pure virtual function void display(input string prefix="");
endclass
可以声明BaseTr类型的句柄,但是不能创建该类型的对象。需要首先扩展该类并对所有的纯虚方法体用具体实现。
//Transaction类继承自虚类BaseTr,并实现BaseTr类内所有的纯虚方法
class Transaction extends BaseTr;
rand bit[31:0] src, dst, crc, data[8];
extern virtual function bit compare(input BaseTr to);
extern virtual function BaseTr copy(input BaseTr to = null);
extern virtual function void copy_data(input Transaction copy);
extern function new();
endclass
8.7 回调
本教材想给出一个最主要的建议就是如何创建一个可以不做任何更改就鞥在所有的测试中使用的验证平台。要做到这一点关键就是在测试平台必须提供一个“钩子”,以便测试程序在不修改原始类的情况下注入新的代码。你的驱动类可能想做一下事情:
- 注入错误
- 放弃事务
- 延迟事务
- 将本事务跟其他事务同步
- 将本事务放进记分板
- 收集功能覆盖率数据
与其试图预测所有的可能的错误,延迟或者事务流程中的干扰,不如使用回调的方法,这使驱动器仅需要“回调”一个在顶层测试中定义的子程序。回调的好处在于回调方法可以在每个测试中做不同的定义,这样测试就可以使用回调来为驱动器增加新的功能而不需要编辑Driver类。
可以在在Driver::run的前回调(pre_callback)和后回调(post_callback)函数中拓展Driver::run的行为。
//回调虚基类
virtual class Driver_cbs;
virtual task pre_tx(ref Transacton tr, ref bit drop);
endtask
virtual task post_tx(ref Transaction tr);
endtask
endclass
//driver类
class Driver;
Driver_cbs cbs[$];
task run();
bit drop;
Transaction tr;
forever
begin
drop = 0;
agt2drv.get(tr);
foreach(cbs[i])
begin
cbs[i].pre_tx(tr, drop);
if(!drop) continue;
` transmit(tr);
end
foreach(cbs[i]) cbs[i].post_tr(tr);
end
endclass
//继承回调基类,实现100个事务随机丢弃一个功能
class Driver_cbs_drop extends Driver_cbs;
virtual task pre_tx(ref Transaction tr, ref bit drop);
//没100个事务中随机丢弃1个
drop = ($urandom_range(0,99) == 0);
endtask
endclass
//test类,调用driver,实例化Driver_cbs_drop类
program automatic test;
Environment env;
initial
begin
env = new();
env.gen_cfg();
env.build();
begin
Driver_cbs_drop dcd = new();
env.drv.cbs.push_back(dcd);
end
env.run();
env.wrapup();
end
endprogram
8.8 参数化的类
SV中,可以为类增加一个数据类型参数,并在声明类句柄的时候指定类型。这样做类似于参数化的模块(module)。SV中的类参数化类似C++中的模板(STL模板)。参数化的类需要在类定义是增加一个数据类型的参数(#(type T = int)),并在声明类句柄的时候指定类型。
//参数化的堆栈类
class Stack # (type T=int);
local T stack[100];
local int top;
//从顶部压入栈
function void push(input T i);
stack[++top]=i;
endfunction
//从顶部出栈
function T pop();
return stack[top--];
endfunction
endclass
//使用参数化的堆栈类
initial
begin
//定义real类型的堆栈,将Stack中的T类型换为real类型
Stack #(real) rStack;
rStack = new();
for(int i=0;i<5;i++) rStack.push(i*2.0);
for(int i=0;i<5;i++) $display("%f", rStack.pop());
end
//使用蓝图模式的参数化的类
class Generator #(type T=BaseTr);
mailbox gen2drv;
T blueprint;
function new(input mainbox gen2drv);
this.gen2drv=gen2drv;
blueprint=new();
endfunction
task run();
T tr;
forever
begin
assert(blueprint.randomize);
tr=blueprint.copy();
gen2drv.put(tr);
end
endtask
endclass
8.1.1 关于参数化的建议
在建立参数化的类时,可以从非参数化类开始,仔细调试,然后增加参数,这种分开的做法可以减少你之后的调试时间。
宏是参数化类的一种替代形式。宏相对于参数化的类来说更难调试。
参考文献:
SystemVerilog验证 测试平台编写指南(原书第二版)张春 麦宋平 赵益新 译