1. 简介
面向对象(object-oriented programming,OOP)就是把数据及对数据的操作方法放在一起,作为一个相互依存的整体。在SystemVerilog中这样做的附加好处是测试平台和设计细节分开了,他们变得更加可靠,更加易于维护并且在将来的项目中可以重复使用。
2. 类
类封装了数据和操作这些数据的子程序。类可以在program,module和package中定义,或者在这些块之外的任何地方。一般推荐在program和module外的package中定义。
//简单的类
class Transaction;
bit [31:0] addr,crc,data[8];
function void display;
$display("Transaction:%h",addr);
endfunction:display
function void calc_crc;
crc=addr^data.xor;
endfunction:calc_crc
endclass
3.对象的创建与使用
Verilog和OPP都具有例化的概念,但在细节方面有一些区别。一个Verilog模块是在代码被编译的时候例化的,而一个SystemVerilog的类是在运行测试中需要的时候才被创建。
3.1 句柄
句柄指向创建的一个类型对象。
Transaction tr;//声明一个句柄
tr=new(); //为一个Transaction对象分配空间
在声明句柄tr的时候,它的初始值为特殊值null,接下俩来需要调用new()函数来创建Transaction对象。new函数为Transaction分配空间,将变量初始化为默认值(二值变量为0,四值变量为X),并返回保存对象的地址。
3.2 定制构造函数
new函数又被称作构造函数,因为它创建对象。
//简单的用户定义的new()函数
class Transaction;
logic [31:0] addr,crc,data[8];
function new;
addr=3;
foreach(data[i])
data[i]=5;
endfunction
endclass
//一个带参数的new()函数
class Transaction;
logic [31:0] addr,crc,data[8];
function new(logic [31:0] a=3,d=5);
addr=a;
foreach(data[i])
data[i]=d;
endfunction
endclass
initital begin
Transaction tr;
tr=new(10); //data使用默认值5
end
3.3 为对象创建句柄
首先要理清对象和句柄的关系。我们通常通过声明一个句柄来创建一个对象,在一次仿真中,一个句柄可以指向很多对象,当然一次只能指向一个。
//为多个对象分配地址
Transaction t1,t2; //声明两个句柄
t1=new(); //为第一个Transaction对象分配地址
t2=t1; //t1和t2都指向该对象
t1=new(); //为第二个Transaction对象分配地址
3.4 对象的解除分配
当事务成功完成后,就不需要保留这些对象占用内存了,这时候可以将对象释放防止内存耗尽导致的运行变慢。SystemVerilog分辨对象不再被引用的方法就是记住指向它句柄的数量,当最后一个句柄不再引用某个对象了,SystemVerilog就释放该对象的空间。
Transaction t; //创建一个句柄
t=new(); //分配一个新的Transaction
t=new(); //分配第二个,释放第一个
t=null; //解除分配第二个
3.5 使用对象
可以对对象使用"."符号来引用变量和子程序。
//使用对象的变量和子程序
Transaction t; //声明一个Transaction句柄
t=new(); //创建一个Transaction对象
t.addr=32'h42; //设置变量的值
t.display(); //调用一个子程序
4.静态变量
4.1 简单的静态变量
每个对象都有自己的局部变量,这些变量不能和其他对象共享。多个对象想共享一个变量,需要创建一个静态变量,该变量的使用访问仅限于这个类。
在该例中,不管创建了多少个Transaction对象,静态变量count只存在一个。可以认为count保存在类中而非对象中的。变量id不是静态的,所以每个Transaction对象都有自己的id。
静态变量通常在声明时初始化,不能简单的在类的构造函数中初始化静态变量,因为每一个新的独对象都会调用构造函数。
//含有一个静态变量的类
class Transaction;
static int count=0; //已创建的对象的数目
int id; //实例的唯一标志
function new();
id=count++; //设置标志,count递增
endfunction
endclass
Transaction t1,t2;
initial begin
t1=new(); //第一个实例,id=0;count=1;
t2=new(); //第二个实例,id=1,count=2;
$display("Second id=%d,count=%d",t2.id,t2.count);
end
4.2 通过类名访问静态变量
除了通过句柄访问静态变量之外,还可以直接通过类名加上"::"类作用域操作符进行访问。
//类作用域操作符
class Transaction;
static int count=0;
...
endclass
initial begin
run_test();
$display("%d tansaction were created",Transaction::count);
end
5.作用域规则
5.1 作用域
作用域是一个代码块,例如一个模块,一个程序、任务、函数、类或者begin-end块。变量名字可以相对于当前作用域,也可以用绝对在作用域表示,例如以$root开始。
//名字作用域
int limit; //$root.limit
program automatic p;
int limit; //$root.p.limit
class Foo;
int limit,array[]; //$root.p.Foo.limit
//$root.p.Foo.print.limit
function void print(int limit);
for(int i=0;i<limit;i++)
$display("%m:array[%0d]=%0d",i,array[i]);
endfunction
endclass
initial begin
int limit=$root.limit;
Foo bar;
bar=new;
bar.array=new[limit]; //为动态数组分配空间
bar.print(limit);
end
endprogram
5.2 this的用法
当使用一个变量名的时候,SystemVerilog会现在当前作用域内寻找,接着在上一级作用域寻找,直到找到该变量为止。但是如果在类的很深的底层作用域却想明确的引用类一级的对象就需要用到this关键词。
//使用this指针指向类一级变量
class Scoping;
string oname;
function new(string oname);
this.oname=oname; //类变量oname=局部变量oname
endfunction
endclass
6.理解动态对象
在静态分配内存的语言中,每一块数据都有一个变量与之关联。在OOP中,不存在这种一一对应关系,可能有很多对象,但是只定义了少量句柄。
6.1 将对象传递给方法
当调用方法的时候,传递的是对象的句柄而非对象本身。
当调用一个带有标量变量(不是数组,也不是对象)的方法并且使用ref关键词的时候,SV传递该标量的地址,所以方法也可以修改标量变量的值。如果不使用ref关键词,SV将该标量的值复制到参数变量中,对该参数变量的任何改变不会影响原变量的值。
//将包传送到一个32位总线上
task transmit(Transaction t);
CBbus.rx_data<=t.data;
t.status.startT=$time;
...
endtask
Transaction t;
initial begin
t=new(); //为对象分配空间
t.addr=42; //初始化数值
transmit(t); //将对象传递给任务
end
在该例子中,初始化块先产生了一个Transaction对象,并且调用transmit任务,transmit任务的参数是指向该对象的句柄。通过使用句柄,transmit可以读写对象中的值。但是如果transmit试图改变句柄,初始化块将不会看到结果,因为参数t没有使用ref修饰符。
6.2 在任务中修改句柄
下例中,尽管create修改了参数tr,调用块中的句柄t仍为null。需要将参数tr声明为ref。
//错误的事务生成器,句柄缺少关键词ref
function void create(Transaction tr); //错误,缺少ref
tr=new();
tr.addr=42;
...
endfunction
Transaction t;
initial begin
create(t); //创建一个transaction
$display(t.addr); //失败,因为t=null
end
//正确的事务发生器,参数是带有ref的句柄
function void create(ref Transaction tr);
...
endfunction
6.3 在程序中修改对象
该例中错误的发生器仅创建了一个对象,每一次循环,在发生事务对象的同时又修改了它的内容,因此所有被传送的Transaction都有相同的addr数值。
//错误的发生器,只创建了一个对象
task generator_bad(int n);
Transaction t;
t=new();
repeat(n) begin
t.addr=$random(); //变量初始化
$display("Sending addr=%h",t.addr);
transmit(t); //将它发送到DUT
end
endtask
//正确的事务发生器
task generator_good(int n);
Transaction t;
repeat(n) begin
t=new()
t.addr=$random(); //变量初始化
$display("Sending addr=%h",t.addr);
transmit(t); //将它发送到DUT
end
endtask
6.4 句柄数组
写测试平台的时候,可能需要保存并且引用许多对象。可以创建数组,数组的每一个元素指向一个对象。
//使用数组句柄
task generator();
transmit tarrary[10];
foreach(tarray[i])
begin
tarray[i]=new(); //创建每一个对象
transmit(tarray[i]);
end
endtask
7.对象的复制
7.1 使用new操作符复制对象
使用new操作符不仅可以创建一个新的对象,还可以复制现有对象的所有变量给新创建的对象。
//使用new操作符复制一个简单类
class Transaction;
bit [31:0] addr,crc,data[8];
endclass
Transaction src,dst;
initial begin
src=new(); //创建一个对象
dst=new src;//使用new操作符进行复制
end
但是如果类中包含指向另一个类的句柄,那么只有最高一级的对象被new操作符复制,下层的对象都不会被复制。
//使用new操作符复制一个复杂类
class Transaction;
bit [31:0] addr,crc,data[8];
static int count=0;
int id;
Statistics stats; //指向Statistics对象的句柄
function new;
stats=new();
id=count++;
endfunction
endclass
Transaction src,dst;
initial begin
src=new(); //创建一个Transaction对象
src.stats.stratT=42;
dst=new src; //用new操作符将src拷贝到dst中
dst.stats.startT=96; //改变dst和src的stats
$display("src.stats.startT");//"96"
end
当调用new函数进行复制的时候,Transaction对象被拷贝,但是Statistics对象没有被复制。这是因为当你使用new操作符复制一个对象的时候,它不会调用自己的new函数。相反的,变量和句柄的值被复制,所以现在两个Transaction对象都具有相同的id值。此时两个Transaction对象都指向同一个Statistics对象,所以使用src句柄修改StartT会影响dst句柄可以看到的值。
7.2 编写简单复制函数
如果类中不包含对其他类的引用,那么编写copy函数十分容易。
//含有copy函数的简单类
class Transaction;
bit [31:0] addr,crc,data[8];
function Transaction copy;
copy=new(); //创建目标对象
copy.addr=addr; //填入数值
copy.src=crc;
copy.data=data; //复制数组
endfunction
endclass
//使用copy函数
Transaction src,dst;
initial begin
src=new(); //创建第一个对象
dst=src.copy; //复制对象
end
7.3 编写深层复制函数
对于非简单的类,可以创建自己的copy函数。通过调用类所包含的所有对象的copy函数,做一个深层拷贝。
//复杂类的深层复制函数
class Transaction;
bit [31:0] addr,crc,data[8];
Statistics stats;
static int count=0;
int id;
function new;
stats=new();
id=count++;
endfunction
function Transaction copy;
copy=new();
copy.addr=addr;
copy.crc=crc;
copy.data=data;
copy.stats=stats.copy();
id=count++;
endfunction
endclass
//使用new操作符操作复杂类
Transaction src,dst;
initial begin
src=new(); //创建一个Transaction对象
src.stats.stratT=42;
dst=new src; //用new操作符将src拷贝到dst中
dst.stats.startT=96; //仅改变dst的stats值
$display("src.stats.startT");//"42"
end
7.4 流操作符
某些协议每次传输一个字节的控制或者数据值。在送出一个transaction之前,需要将对象中的变量打包成一个字节数组。类似的,在接收到一个字符串之后,也需要将它们解包到一个transaction对象中。
将整个对象送入流操作符会包含许多不必要的自检信息,可以通过编写pack函数,打包所选择的成员变量。
//含有pack和unpack函数的Transaction类
class Transaction;
bit [31:0] addr,crc,data[8]; //实际数据
static int count=0; //不需要打包的数据
int id;
function new();
id=count++
endfunction
function void display();
$write("Tr:id=%d,addr=%x,crc=%x",addr,crc);
foreach(data[i]) $write("x",data[i]);
$display;
endfunction
function void pack(ref byte bytes[40]);
bytes={>>{addr,crc,data}};
endfunction
function Transaction unpack(ref byte bytes[40]);
{>>{addr,crc,data}}=bytes;
endfunction
endclass:Transaction
//使用pack和unpack函数
Transaction tr,tr2;
byte b[40]; //addr+crc+data=40字节
initial begin
tr=new();
tr.addr=32'ha0a0a0a0;//填满对象
tr.crc='1;
foreach(tr.data[i])
tr.data[i]=i;
tr.pack(b); //打包对象到字节数组
$write("Pack results:");
foreach(b[i])
$write("%h",b[i]);
$display;
tr2=new();
tr2.unpack(b);
tr2.display();
end
end