如果一个程序员所接触的第一门语言是Verilog的话,那么这一章我们讲述的面向对象编程OOP(object-oriented programming)就要改变Verilog这种面向结构化的编程语言的思路。Verilog语言中没有数据结构,只有向量和数组。
面向对象编程OOP使用户能够创建复杂的数据类型,并且将它们跟使用这些数据类型的程序紧密地结合在一起。
5.1概述
面向对象编程(OOP)使用户可以在更加抽象的层次上建立测试平台和系统级模型,通过调用函数来执行一个动作而不是改变信号的电平。当使用事务处理器来代替信号翻转的时候,我们就可以变得很高效。
测试平台跟设计细节分开了,它们变得更加可靠,更加易于维护,在将来的项目中可以重复使用。
传统的测试平台要做的操作:创建一个事务、发送、接收、检查结果、然后产生报告。而在OOP中,你需要重新考虑测试平台的结构,以及每部分的功能。发生器(generator)创建事务并且将它们传给下一级,驱动器(driver)和设计进行会话,设计返回的事务将被监视器(monitor)捕获,计分板(scoreboard)会将捕获的结果跟预期的结果进行比对。因此,测试平台应该分成若干块(block),然后定义它们相互之间如何通信。
5.2类(class)
类封装了数据和操作这些数据的子程序。下面的例子是一个通用数据包类。这个数据包包含了地址、CRC和一个存储数值的数组。
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:Transaction
类可以定义在program module package中,或者在这些块之外的任何地方。类可以在程序和模块中使用。可以使用SystemVerilog的包(package)将一组相关的类和类型定义捆绑在一起。
5.3 OOP术语
- 类(class):包含变量和子程序的基本构建块。
Verilog中与之对应的是模块(module)。 - 对象(object):类的一个实例。
Verilog中,你需要实例化一个模块才能使用它。 - 句柄(handle):指向对象的指针。一个OOP句柄就像一个对象的地址。
Verilog中,你通过实例名在模块外部引用信号和方法。 - 属性(property):储存数据的变量。
Verilog中,就是寄存器(reg)或者线网(wire)类型的信号。 - 方法(method):任务或者函数中操作变量的程序性代码。
Verilog中的任务和函数。 - 原型(prototype):程序的头,包括程序名、返回类型和参数列表。程序体则包含了执行代码。
5.4 创建新对象
Verilog和OOP中,都存在例化的概念。Verilog的例化是静态的,就像硬件一样在仿真的时候不会变化,只有信号值在改变。而SystemVerilog中,激励对象不断地被创建并且用来驱动DUT,检查结果。最后这些对象所占的内存可以被释放,以供新的对象使用。类在使用之前必须例化,句柄可以指向很多对象,但是一次只能指向一个。
使用new函数来分配并初始化对象。
Transaction tr; //声明一个句柄
tr=new(); //为一个Transaction对象分配空间
- 声明句柄tr的时候,它被默认初始化null。
- 调用new()函数来创建Tansaction对象,并分配空间,将变量初始化为默认值(二值变量为0,四值变量为X),并返回保存对象的地址。
5.4.1 定制构造函数(constructor)
用户可以自行设置初始值。
设置成固定的值
class Transaction;
bit [31:0] addr,crc,data[8];
function new;
addr=3;
foreach(data[i])
data[i]=5;
endfunction
endclass:Transaction
你也可以设计成可以选择使用默认值或者固定值。
class Transaction;
bit [31:0] addr,crc,data[8];
function new(logic[31:0] a=3,d=5);
addr=a;
foreach(data[i])
data[i]=d;
endfunction
initial
begin
Transaction tr;
tr=new(10); //addr=10,data=5(默认值)
end
endclass:Transaction
- addr和data的默认值是3和5,可以对它们的值进行改变,没有明确指出时就使用默认值。
- 将声明和创建分开。
- new[]和new(),两者都是申请内存并初始化变量。
不同之处:1、new()函数仅创建了一个对象,new[]则建立一个含有多个元素的数组。2、new()可以使用参数设置对象的数值,而new[]只需要使用一个数值来设置数组的大小。
5.4.2 为对象创建一个句柄
通过声明一个句柄来创建一个对象。在一次仿真中,一个句柄可以指向很多对象,但是一次只能指向一个。
Transaction t1,t2;
t1=new();
t2=t1;
t1=new();
t2指向第一个Transaction对象,t1指向第二个Transaction对象。
5.4.3 对象的解除分配
t=new(); //分配第一个Transaction对象
t=new(); //分配第二个Transaction对象,释放第一个
t=null; //解除分配第二个
5.4.4使用对象
如果已经分配了一个对象,可以使用“.”符号来引用变量和子程序。
Transaction t;
t=new();
t.addr=32'h42; //设置变量的值
t.display(); //调用一个子程序
5.5 静态变量和全局变量
每个对象都有自己的局部变量,这些变量不和任何其他对象共享。如果有两个Transaction对象,则每个对象都有自己的局部变量。但是有时候你需要一个某种类型的变量,被所有的对象所共享。
5.5.1 简单的静态变量
在SystemVerilog中,可以在类中创建一个静态变量。该变量将被这个类的所有实例所共享,并且它的使用范围仅限于这个类。
class Transaction;
static int count=0;
int id;
function new();
id=count++;
endfunction
endclass:Transaction
Transaction t1,t2;
initial
begin
t1=new();
t2=new();
$display("Second id=%d,count=%d",t2.id,t2.count);
end
- t1,第一个实例,id=0,count=1;
- t2,第二个实例,id=1,count=2;
- count保存在类中而不是对象中,对t1和t2都是同一个count;
- id不是静态变量,t1和t2都有不同的id值。
- 通常在声明时初始化变量。
SystemVerilog不能输出对象的地址,但是可以考虑创建ID域来区分对象。考虑创建一个类的静态变量,它能自给自足,对外部的应用越少越好。
5.5.2 通过类名访问静态变量
我们可以使用在类名加上::来引用静态变量。
class Transaction;
static int count=0;
endclass
initial
begin
run_test();
$display("%d transaction were created",Transaction::count);
end
5.6 类的方法
类中的程序也称为方法,也就是在类的作用域内定义的内部task或者function。
class Transaction;
bit [31:0] addr,crc,data[8];
function void display();
......
endfunction
endclass:Transaction
Transaction t;
initial
begin
t=new();
t.display(); //调用Transaction的方法
end
5.7 在类之外定义方法
为了增强程序的可读性,我们一般讲class搭配endclass在同一页面中。但是如果内容太多的话,可以将方法名和参数放在类的内部,而方法的程序体(过程代码)放在类的定义后面。
class Transaction;
bit [31:0] addr,crc,data[8];
extern function void display();
endclass:Transaction
function void Transaction::display();
......
endfunction
- 在class定义里加入关键词extern
- 将function移到类的后面,并注意function void Transaction::display()的命名格式。
5.8 作用域规则
在编写测试平台的时候,需要创建和引用许多变量。SystemVerilog采用与Verilog基本相同的规则。
作用域是一个代码块,例如一个module、program、task、function、class、begin-end块。for和foreach循环自动创建一个块。
类应当在program和module外的package中定义。
package Mistake;
class Bad;
logic[31:0] data[];
function void dispaly;
......
endfunction
endpackage
program test;
int i;
import Mistake::*;
......
endprogram
当你使用一个变量名的时候,SystemVerilog将会在当前作用域寻找,接着在上一级作用域内寻找,直到找到该变量为止。这一点与Verilog相似。
这里介绍一种直接将局部变量赋给类一级变量的方法。
class Scoping;
string oname;
function new(string oname); //function的局部变量oname
this.oname=oname; //将类变量oname=局部变量oname
endfunction
endclass
- 采用this直接将局部变量赋给类一级的变量。
5.9 在一个类内使用另一个类
通过使用指向对象的句柄,一个类内部可以包含另一个类的实例。这就如同Verilog中,在一个模块内部包含另一个模块实例,以建立设计的层次结构。
class Statistics;
.......
endclass
class Transaction;
.......
Statistics stats; // 例化的类的句柄
function new();
stats=new();
endfunction
task create_packet();
.......
stats.start(); //分层调用使用Statistics里的变量
endtask
endclass
- 最外层的Transaction可以通过分层调用语法来调用Statistics类中的成员
- 在上层构造函数中,完成对调用类的例化
注意在调用类的过程中,我们通常有一个编译顺序的问题,如果调用的类在后面,则需要提前声明。如果上例中两个class的顺序颠倒一下,则需要声明typedef class Statistics。
5.10 理解动态对象
在OOP中,可能有很多对象,但是只定义了少量的句柄。一个测试平台在仿真过程中可能产生了数千次事务对象,但是仅有几个句柄在操作它们。如果你之前一直在用Verilog代码,你一定要习惯这种情况。
5.10.1 将对象传递给方法
当你调用方法的时候,传递的是对象的句柄而不是对象本身。
task transmit (Transaction t);
......
endtask
Transaction t;
initial
begin
t=new();
t.addr=42;
transmit(t);
end
-
初始化块先产生一个Transaction对象,并且调用transmit任务,transmit任务的参数是指向该对象的句柄。通过句柄,transmit可以读写对象中的值。
-
如果transmit试图改变句柄,初始化块将不会看到结果,因为参数t没有ref修饰符。
task transmit ( ref Transaction tr); ...... endtask Transaction t; initial begin t=new(); transmit(t); $display(t.add); end
-
transaction修改了参数tr,使用ref关键词,否则的话,只是对tr做了修改,调用块中的句柄t仍为null。
5.10.2 在程序中修改对象
在测试平台中,一个常见的错误就是忘记为每个事物创建一个对象。
task generator_bad(int n);
Transaction t;
t=new();
repeat (n)
begin
t.addr=$random();//修改变量初始值
$display("%0h",t.addr);
transmit(t); //将它发送到DUT
end
endtask
-
上面的代码仅创建了一个对象,所以每一次循环generor_bad在发送事务对象的同时又修改了它的内容。
-
当你运行这段代码的时候,display出不同的addr值,但是transmit的t都具有相同的addr值。
-
为了避免这种错误,你需要在每次循环的时候创建一个新的Transaction对象。
task generator_bad(int n); Transaction t; repeat (n) begin t=new(); t.addr=$random();//修改变量初始值 $display("%0h",t.addr); transmit(t); //将它发送到DUT end endtask
5.10.2 句柄数组
在写测试平台的时候,可能需要保存并且引用许多对象。你可以创建句柄数组,数组的每一个元素指向一个对象。
task generator();
transmit tarray[10];
foreach (tarray[i])
begin
tarray[i]=new();
transmit(tarray);
end
endtask
- tarray数组是由句柄组成而不是对象,所以在使用时,必须像普通句柄创建对象一样。
5.11 对象的复制
可以使用简单的new函数的内建拷贝功能,也可以为更复杂的类编写专门的对象拷贝函数。下面我们就来一一介绍一下。
5.11.1 使用new操作符复制一个对象
使用new复制一个对象简单而且可靠,它创建了一个新的对象,并且复制了现在对象的所有变量。
class Transaction;
bit[31:0] addr,crc,data[8];
endclass
Transaction src,dst;
initial
begin
src=new();
dst=new src;
end
-
这是一种简易的复制,只有最高一级的对象被new操作符复制,下层的对象都不会被复制。
如果Transaction类包含了一个指向Statistics类的句柄,那么我们又该如何处理呢?class Transaction; bit[31:0] addr,crc,data[8]; static int count=0; int id; Statistics stats; function new; stats=new(); id=count++; endfunction endclass Transaction src,dst; initial begin src=new(); src.stats.startT=42; dst=new src; dst.stats.startT=96; //src.stats.startT=dst.stats.startT=96 end
-
Transaction对象被拷贝,但是Statistics对象没有被复制;
-
两个Transaction对象都具有相同的id值;
-
两个Transaction对象都指向同一个Statistics对象。
5.11.2 编写自己的简单复制函数
如果有一个简单的类,它不包含任何对其他类的引用,那么编写copy函数非常容易。
class Transaction;
bit[31:0] addr,crc,data[8];
function Transaction copy;
copy=new();
copy.addr=addr;
copy.crc=crc;
copy.data=data;
endfunction
endclass
Transaction src,dst;
initial
begin
src=new();
dst=src.copy;
end
5.11.3 编写自己的深层次复制函数
你自己的copy函数需要确保所有用户域(id)保持一致。创建自定义copy函数的最后阶段需要在新增变量的同时更新它们。
class Transaction;
bit[31:0] addr,crc,data[8];
static int count=0;
int id;
Statistics stats;
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
Transaction src,dst;
initial
begin
src=new(); //id=0
dst=src.copy; //id=1
end
- 不仅复制了Transaction,而且复制了Statistics,不同的Transaction对应不同的Statistics。
5.12 使用流操作符从数组到打包对象,或者从打包对象到数组
按照需要编写自己的pack函数,仅打包你所选的成员变量。
class Transaction;
bit[31:0] addr,crc,data[8];
static int count=0;
int id;
function new;
id=count++;
endfunction
function void pack (ref byte bytes[40]);
bytes={>>{addr,crc,data}};
endfunction
function void unpack (ref byte bytes[40]);
{>>{addr,crc,data}}=bytes;
endfunction
endclass
Transaction tr,tr2;
byte b[40];
initial
begin
tr=new(); //创建对象并填满数据
tr.addr=32'h0; //id=0
tr.crc=32'h0; //id=1
foreach(tr.data[i])
tr.data[i]=i;
tr.pack(b);
tr2.unpack(b);
end