文章目录
前言
结构化编程语言:Verilog和C语言
面向对象编程(OOP)能创建复杂的数据类型,并使用这些数据类型的程序紧密结合。用户可以在更加抽象的层次建立测试平台和系统级模型,调用函数来执行一个动作而不是改变信号的电平。使用事务代替信号反转的时候,你就会更高效。而且测试平台和设计细节分开了,他们变得更加可靠,易于维护,在将来的项目中可以重复使用。
事务就是测试平台的焦点
一、OOP术语
- 类(class):包含变量和子程序的基本构建块。 对应verilog中的模块
- 对象(object):类的一个实例。verilog中,你需要实例化一个模块才能使用它。
- 句柄(handle):指向对象的指针(像一个对象的地址)。在verilog中,通过实例名在模块外部引用信号和方法。
- 属性(property):存储数据的变量。在verilog中,是reg或者wire类型的信号
- 方法(method):任务或函数中操作变量的程序性代码。在verilog 模块除了initial 块和 always块,还有任务和函数
- 原型(prototype):程序的头:包含程序名、返回类型和参数列表。程序体则包含了执行代码。
在verilog中,通过创建模块并逐层例化;
在OOP中创建类并例化创建的对象,你可以得到一个相似的层次结构。
1. 创建新对象
verilog:的实例名只可以指向一个实例,例化是在代码被编译的时候,是静态的,在仿真时不会变化,只有信号值在改变。
OOP:对象不断地被创建,也可以被释放,在运行测试平台的时候创建。
声明和使用一个句柄;
transaction tr; //声明句柄,初始化为特殊值null
tr = new(); //调用new函数创建transaction对象,为其分配空间,
//将变量初始化为默认值(二值变量为0,四值变量为X),并返回保存对象的地址。
2. 定制构造函数(constructor)
SV会为四值变量使用更多的内存,并会保存一定的信息,如对象的类型;
构造函数 :会分配内存,和初始化变量。new也是构造函数,new函数不能有返回值,返回一个指向类对象的句柄,其类型就是类本身。
2.1. 带有参数的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] = 5;
endfunction
endclass
initial begin
Transaction tr;
tr = new(10);//addr=10, data采用默认值5
end
2.2 调用正确的new()函数
class Transaction;
...
endclass
class driver;
Transaction tr;
function new(); //driver 的 new 函数
tr = new(); // transaction的new函数
endfunction
class:driver
3. 将声明和创建分开
避免 使用 Transaction tr = new(); 要分开使用上文形式。
原因:你应该是希望先声明和初始化为特殊值null,再创建对象和分配空间,初始化变量; 但是合并在一起写,你不能控制这个顺序,如果忘记了使用automatic存储空间,构造函数将在仿真开始时,而非进入块的时候调用
4. new( ) 和new[ ] 区别
共同点:都申请内存并初始化变量
区别:
new[ ] 设置动态数组大小,通过写一个数字设置数组大小;
new() 函数创建一个对象, 通过使用参数设置对象的数值;
5. 为对象创建一个句柄
一个句柄可以指向多个对象;
Transaction tr1,tr2;
tr1 = new(); // 为第一个transaction 对象分配地址
tr2 = tr1; // tr1 和 tr2 都指向该对象
tr1 = new(); // 为第二个transaction 对象分配地址
这样tr1指向第二个transaction对象,
tr2指向第一个transaction对象。
SV:在你需要的时候动态创建对象,能够创建任意数量的事务。
Verilog:固定的数组,数组能够容纳最大数量的事务; 模块的实例和他的名字是固定绑定在一起的,即使仿真过程中产生和自动注销的automatic变量,名字和内存总是绑定在一起的。
6. 对象的解除分配 deallocation
因为 一个事务产生并发送到DUT,得知发送事务成功,也统计到结果,那么这个事务就没什么用了。否则,长时间的仿真会将内存消耗尽,或者运行的慢。
一旦没有句柄指向对象,对象会自动消失。
SV不能回收一个被句柄引用的对象,而且在线程没有结束前也不能回收对象。
transaction t; // 创建一个句柄
t = new(); // 分配一个新的transaction
t = new(); // 分配第二个,并释放第一个t,因为第一个没有句柄指向它了
t = null; // 解除分配第二个,实质在清除t句柄
7. 使用对象
使用对象的变量和子程序:
transaction t; // 声明一个句柄
t = new(); // 创建一个transaction 对象
t.addr = 32'h42; // 给变量赋值
t.display(); // 调用子程序
和Verilog一样用“.”引用变量和子程序
OOP规定 只能通过对象的共有方法访问对象的变量,如get(), put() 。 因为直接访问变量会限制以后对代码的修改。
如果变量隐藏在无数的方法中,难以做到最大限度地控制所有变量,以产生最广泛地激励。
坚持变量公有化,以便测试平台在任何地方都可以访问他们。
8. 静态变量和全局变量
8.1 SV 在类中创建一个静态变量,该变量将被这个类所有实例共享,适用范围仅限于这个类。
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
无论创建多少个transaction对象,全局变量count 只有一个,count保存在类中而非对象中,
变量id不是静态的,所以每个transaction都有自己的id变量,
这样就不需要为count创建一个全局变量。
当你打算建一个全局变量的时候,考虑创建一个类的静态变量。
一个类是最好是独立的,尽量减少外部引用。
8.2 通过类名访问静态变量
8.1中使用了句柄引用静态变量,
可以使用类名加上::,即类作用域操作符,引用静态变量
class transaction;
static int count = 0; // 已创建的对象地数目
...
endclass
transaction t1, t2;
initial begin
....
$display("Second id = %d, count=%d",t2.id, transaction::count);
end
8.3 静态变量的初始化
- 通常在声明时初始化。也可以用初始化块。
- 不能在类的构造函数中初始化静态变量,因为每个新的对象都会调用构造函数。
8.4 静态方法
当类的每一个实例 都需要从同一个对象获取信息的时候。例如,transaction类可能需要从配置对象获取模式位。
如果
class transaction;
static Config cfg; // 使用静态存储的句柄
MODE_E mode;
function new();
mode = cfg.mode;
endfunction
endclass
Config cfg;
initial begin
cfg = new(MODE_ON);
Transaction::cfg = cfg;
...
end
当静态变量增多后,可以在类中创建一个静态方法以用于读写静态变量,甚至在第一个实例产生之前读写静态变量。
显示静态变量的静态方法
class transaction;
static Config cfg;
static int count = 0;
int id;
// 显示静态变量的静态方法
static function void display_statics();
$display("Transaction cfg.mode = %s, count = %0d", cfg.mode.name(), count);
endfunction
endclass
Config cfg;
initial begin
cfg = new(MODE_ON);
transaction::cfg = cfg;
transaction::display_statics(); //调用静态方法
end
9. 类的方法
类中的程序也叫方法,即类中的task和function
根据句柄类型调用正确的方法
类名 句柄名;
句柄名 = new(); // 创建类名的对象
句柄名.类中方法();//调用类名中的方法
类中的方法时默认自动存储的,不必担心忘记使用automatic修饰符
10. 在类之外定义方法
如果方法过程代码太长,无法在一页内读完整个类或者方法。
方法的原型定义放在类的内部,方法的过程代码放在类的后面定义。
例如:
class transaction;
extern function void display();
endclass
function void transaction::display();
endfunction
11. 作用与规则
当前作用域
类应当在program或者 module外的package中定义。
绝对作用域以$root开始
名字作用域:
int limit; // $root.limit
program automatic p;
int limit; // $root.p.limit
class FO;
int limit; //$root.p.Fo.limit
function void print(int limit); // $root.p.Fo.print.limit
endfunction
endclass
initial begin
int limit = $root.limit;
endprogram
11.1 this是什么
使用一个变量名时,SV会在当前域寻找,接着在上一级作用域内寻找,直到找到变量为止。和Verlog相同。
但是如果你在类的很深的底层作用域,想明确的引用类一级的对象:this 可以高速SV。
使用this指针指向类一级变量
class Scope;
string oname;
function new(string oname);
this.oname = oname; // 类变量oname=局部变量oname
endfunction
endclass
12. 在一个类里使用另一个类
通过使用指向对象的句柄,一个类内部可以包含另一个类的实例。
Verilog中,一个模块内部包含另一个模块的实例,以建立设计的层次结构。 这样包含的目的是重用和控制复杂度。
Statistics类的声明
class Statistics;
time startT, stopT; //事务的时间
static int ntrans= 0; //事务的数目
static time total_elapsed_time = 0;
function time how_long;
how_long = stopT-startT;
ntrans++;
total_elapsed_time += how_long;
endfunction
function void start;
startT = $time;
endfunction
endclass
可以在另一个类里使用 Statistics类
封装Statistics类
class transaction;
bit [31:0] addr,crc,data[8];
Statistics stats; // 声明一个Statistics句柄
function new();
stats = new(); // 创建stats实例
endfunction
task create_packet();
//填充数据
stats.start(); //Statistics句柄调用Statistics的成员
//传送数据包
endtask
endclass
12.1 类该做成多大
看类的使用频率以及类的例化次数。
12.2 编译顺序的问题
编译一个类时,里边包含着一个尚未定义的类。声明这个被包含的类的句柄将会引起错误。
你应该需要使用typedef语句声明一个类名。
typedef class Statistics; //定义低级别类
class Transaction;
Statistics stats; // 使用Statistics类
...
endclass
class Statistics; // 定义Statistics类
....
endclass
13. 理解动态对象
在静态分配内存的语言,如Verilog,每个数据都有一个变量与之关联。
OOP中不存在这种对应关系。可能有很多对象,之定义少量句柄。很多次事务的对象,只有几个句柄。
对于保存在邮箱(mailbox)中的对象,句柄就是SV的一个内部结构。
13.1 将对象传递给方法
在调用方法时,传递的是对象的句柄而不是对象本身。
当你调用一个带有标量变量(不是数组,也不是对象)的方法并且使用ref关键词的时候,SV会传递该标量的地址,所以方法可以修改标量变量的值。
13.2 在任务中修改句柄
function void create(Transaction tr); // 错误,缺少ref,应该为 ref Transaction tr
tr = new();
tr.addr = 42;
//初始化其他域
...
endfunction
Transaction t;
initial begin
create(t); //创建一个transaction
$display(t.addr);//失败,因为 t= null
end
create修改了参数tr,但是t.addr调用块中的句柄t仍为null。
13.3 在程序中修改对象
测试平台中,常见的一个错误是 忘记为每个事务创建一个新的对象。
- 错误的发生器只创建了一个对象:
task generateor_bad(int n);
Transaction t;
t = new(); // 创建一个新对象
repeat (n) begin
t.addr = $random(); // 变量初始化
$display("Sending addr = %h", t.addr);
transmit(t); // 将它发送给DUT
end
endtask
仅创建了一个Transaction, 每循环一次,generator_bad在发送事务对象的同时又修改了它的内容。display会显示很多个addr值,但是发送的其实都是一个相同的addr值。
- 正确的发生器,创建了多个对象:
task generateor_good(int n);
Transaction t;
repeat (n) begin
t = new(); // 创建一个新对象
t.addr = $random(); // 变量初始化
$display("Sending addr = %h", t.addr);
transmit(t); // 将它发送给DUT
end
endtask
13.4 句柄数组
写测试平台时,需要保存并且引用许多对象。
创建句柄数组,让每个句柄都指向一个对象。
tarray 就是句柄数组
task generator();
transmit tarray[10];
foreach (tarray[i])
tarray[i] = new; // 创建一个句柄为i的对象
transmit(tarray[i]);
endtask
14 对象的复制
有时需要复制要给对象,防止对象的方法修改原始对象的值,或者在一个发生器中保留约束。
14.1 使用new 操作符复制一个对象
使用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
这是shallow copy, 复制了对象的值,只有最高级一级的对象被new操作符复制,下层的对象不会被复制。
**使用new复制一个复杂类**
class transaction;
bit [31:0] addr,crc,data[8];
static int count=0;
int id;
Statistics stats; // 指向Statistics对象的句柄
function new;
stats = new(); //构造一个新的Statistics对象
id = count++;
endfunction
endclass
transaction src, dst;
initial begin
src = new(); // 创建一个transaction对象
src.stats.startT = 42;
dst = new src; // 使用new操作符进行复制src到dst
dst.stats.startT = 96; //改变dst和src的stats
end
问题:src和dst的id都为0,是因为new操作符只复制了对象的变量和句柄的值,而不是重新构建一个对象,两个transaction对象都指向同一个Statistics对象。
14.2 编写自己的简单复制函数
如果有一个不包含任何对其他类的引用地简单类,那么编写copy函数非常容易
class transaction;
bit [31:0] addr,crc,data[8]; //没有Statistic句柄
function transaction copy;
copy = new(); // 创建目标对象
copy.addr = this.addr; // 填入数值
copy.crc = this.crc;
copy.data = this.data // 复制数组
endfunction
endclass
transaction src, dst;
initial begin
src = new(); //创建第一个对象
dst = src.copy; //复制对象
end
14.3 编写自己的深层复制函数
非简单地类,调用类所包含的所有对象地copy函数,左右给深层地拷贝。
typdef class Statistics;
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
function transaction copy;
copy = new(); // 创建目标
copy.addr = this.addr; // 填入数值
copy.crc = this.crc;
copy.data = this.data // 复制数组
copy.stats = stats.copy(); //调用Statics::copy函数, 该copy函数调用了构造函数new(),所以每个对象都有一个唯一的id,
id = count++;
endfunction
endclass
class Statistics;
time startT,stopT; //
...
function Statistics copy();
copy = new();
copy.startT = startT;
copy.stopT = stopT;
endfunction
endclass
transaction src, dst;
initial begin
src = new(); //创建第一个对象
src.stats.startT = 42; //设置其实时间
dst = src.copy(); //深层复制对象
dst.stats.startT = 96; //进改变dst地stats值
end
id = 0, id = 1;
14.4 使用流操作符 从数组到打包对象,或者从打包对象到数组
某协议如ATM协议,每次传输一个字节地控制或者数据值。
送出一个transaction之前,需要将对象中的变量打包成一个字节数组。
在接收到一个字符串之后,也需要将它们解包到一个transaction对象中。
你不能将整个对象送入流操作符,因为这样会包含所有地成员,包括 数据成员 和 其他额外信息,如时间戳和你不想要打包的自检信息。
**含有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 = %0d, addr = %x, crc = %x", id, 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); //打包对象到字节数组
foreach (b[i])
$write("%h", b[i]);
$display;
tr2 = new();
tr2.unpack(b);
tr2.display();
15 公有和私有
OOP的核心概念是 把数据和相关的方法封装成一个类。
数据默认是类私有的,防止其他类对内部数据成员的随意访问。
但在SV中,所有成员都是共有的,除非为 local 或 protected。因为这样可以保证你的成员对DUT行为的最大程度的控制。例如,CRC公有 将使你能够轻易地往DUT中注入错误。被CRC是局部的,需要编写额外的代码来避开数据隐藏机制,最终使测试平台变得更大更复杂。