SystemVerilog的面向对象的特性基本上和C/C++一样,学过Java、C++等语言再学习SystemVerilog会发现非常的轻松。所有我只对那些异同点和重要的点做笔记。
一、面向对象编写测试平台
在OOP中,事务就是测试平台的焦点。发生器创建事务并且将它们传给下一级,驱动器和设计进行会话,设计返回的事务将被监视器捕获,记分板会将捕获的结果跟预期的结果进行比对。测试平台则应该分成若干块,然后定义它们相互之间如何通信。那么在哪里定义类呢?可以把类定义在program、module、package中,或者在这些块之外的任何地方。类可以在程序和模块中使用。
二、创建新对象
- Verilog模块是在代码被编译的时候例化的。而一个SystemVerilog类是在运行中测试平台需要的时候才被创建。
- Verilog的例化是静态的,就像硬件一样在仿真的时候不会变化,只有信号值在改变。而SystemVerilog中,激励对象不断地被创建并且用来驱动DUT,检查结果。最后这些对象所占用的内存可以被释放,以供新的对象使用。
- Verilog的顶层模块是不会被显式地例化,但是SV类在使用前必须先例化。
- Verilog的实例名只可以指向一个实例,SV的句柄可以指向多个对象,当然一次只能指向一个。
三、对象的解除和分配
如果不回收内存,长时间的仿真会将内存耗尽,或者运行得越来越慢。垃圾回收是一种自动释放不再被引用地对象地过程。SV分辨对象不再被引用地办法就是记住指向它地句柄地数量,当最后一个句柄不再引用某个对象了,SV就会释放该对象地空间。
- SV地句柄只能指向一种类型,即所谓的”安全类型“,在C中一个无类型指针只是内存中的一个地址,可以将它设为任何数值;
- SV不允许对句柄作和C类似的改变,也不允许将一种类型的句柄指向类一种类型的对象;
- SV中自动回收垃圾,但是不能回收一个被句柄引用的对象。C/C++中指针可以指向一个不再存在的对象,垃圾回收是手动的;
- SV中如果对象包含有从一个线程派生出来的程序,那么只要该线程仍在运行,这个对象的空间就不会被释放,同样任何一个子线程所使用对象在该线程没有结束之前不会被解除分配;
四、在类之外定义方法
在SystemVerilog中你可以将方法的原型定义(方法名和参数)放在类的内部,而方法的程序体放在类的后面定义。使用关键字extern
块外方法声明
class A;
...
extern function void fun1();
endclass
function void A::fun1();
...
endfunction
五、作用域规则
- 作用域是一个代码块,例如一个模块、一个程序、任务、函数、类、begin-end块,for和foreach循环自动创建一个块;
- 可以在块中定义新的变量;
- 名字可以相对于当前作用域,也可以用绝对作用域表示,例如以$root开始,对于一个相对的名字,SV查找作用域内的名字清单,直到找到匹配的名字。
- 当你使用一个变量名的时候,SV将先在当前作用域内寻找,接着在上一级作用域内寻找,直到找到该变量为止。当作用域层数过多时,可以使用this明确地引用类一级的对象;
名字作用域
int limit; //$root.limit
program automatic p;
int limit; //$root.p.limit
class Foo;
int limit,array[]; //$root.p.Foo.limit
function void print(int limit);
for(int i = 0; i < limit; i++)
...
endfunction
endclass
initial begin
int limit = $root.limit;
Foo bar;
bar = new;
bar.array = new[limit];
bar.print(limit);
end
endprogram
六、理解动态对象
一个测试平台在仿真过程中可能产生了数千次事务的对象,但是仅有几个句柄在操纵它们。每一个对象都有一个句柄,有些句柄可能存储在数组或者队列中,或者在另一个对象中,例如链表。对于保存在邮箱(mailbox)中的对象,句柄就是SystemVerilog的一个内部结构。
将对象传递给方法
- 当你调用方法的时候,传递的是对象的句柄而非对象本身。
- 当你调用一个带有标量变量(不是数组,也不是对象)的方法并且使用ref关键词的时间,SV传递该标量的地址,所以方法也可以修改标量变量的值。如果你不使用ref关键词,SV将该标量的值复制到参数变量中,对该参数变量的任何改变不会影响原变量的值。这里可以类比理解为C、C++中的值传递和引用传递的区别。
//将包传送到一个32位总线上
task transmit(Transaction t);
CBbus.rx_data <= t.data;
t.state.stateT = $time;
...
endtask
Transaction t;
initial begin
t = new();
t.data = 42;
transmit(t); //如果transmit试图改变句柄,初始化块将不会看到结果,因为参数t没有使用ref修饰符。
end
/*
方法可以改变一个对象,即使方法的句柄参数没有使用ref修饰符。
如果不想让对象在方法中被修改,那么就传递一个对象的拷贝给方法。
*/
在程序中修改对象
- 在测试平台中一个常见的错误就是忘记为每一个事务创建一个新的对象;
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
/*
仅仅创建了一个Transaction,每一次的循环会在发送事务对象的同时又修改了它的内容。
如果transmit的线程需要耗费几个周期完成发送,你可能会发现$display会显示不同的addr的值,
但是所有被传送的Transaction都有相同的addr值,这是因为对象的内容在传送的期间被重新随机化了。
为了避免这种错误,应该在每次循环的时候都创建一个新的Transaction对象
*/
句柄数组
- 类比于C/C++的指针数组;
- 句柄数组是由句柄构成,而不是由对象构成,所以在使用它们之前创建所有对象,没有任何办法可以调用new函数为整个句柄数组创建对象;
- 不存在对象数组这个说法;
- 句柄数组里面的多个句柄也有可能指向了同一个对象;
task generator();
transmit tarray[10];
foreach(tarray[i])
begin
tarray[i] = new();
transmit(tarray[i]);
end
endtask
七、对象的复制
- 使用new操作符复制一个对象
class A;
...
endclass
A a1,a2;
initial begin
a1 = new;
a2 = new a1;
end
- 如果类中包含一个指向另一个类的句柄,那么只有最高一级的对象被new操作符复制,下层的对象都不会被复制。
class Transaction;
...
Statistics stats;
function new;
stats = new();
id++;
endfunction
endclass
Transaction src,dst;
initial begin
src = new();
src.stats.startT = 42;
dst = new src;
dst.stats.startT = 96;
$display(src.stats.startT); //"96",因为dst,src都指向同一个Statistics对象。
end
/*
当调用new函数进行复制的时候,Transaction对象被拷贝,两个对象都具有相同的id值,但是Statistics对象没有被复制,两个Transaction对象都指向同一个Statistics对象。
*/
- 使用流操作符从数组到打包对象,或者从打包对象到数组
/*
某些协议,如ATM协议每次传输一个字节的控制或者数据值。
在送出一个Transaction之前,需要将对象中的变量打包成一个字节数组,在接收到一个字节串之后,也需要将它们解包到一个Transaction对象中。
你需要编写自己的pack函数,仅打包你所选择的成员变量。
*/
class Transaction;
bit[31:0] addr,crc,data[8]; //实际数据
static int count = 0; //不需要打包的数据
function new();
id++;
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 tr1, tr2;
bytes 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
八、公有和私有
在SV中所有成员都是公有的,除非标记为local或者protected。一个测试平台需要能够注入错误,以便测试硬件是如何处理错误的。OPP的核心概念是把数据和相关方法封装成一个类,在一个类中数据默认被定义为私有。
例如一个简单的DUT监视器可能只在接口上采样几个数值,但不要将它们简单地保存在整数变量中然后传递给下一级,这样可能一开始节省一点时间,但最终还是需要将这些数值组合到一起来构成一个完整的事务,这些事务中的几个可能需要被组合成更高级别的事务,如DMA事务。
九、建立一个测试平台
分层的测试平台
- 图中的Generator、Agent、Driver、Monitor、Checker、Scoreboard都是类被建模成事务处理器(transactor),它们在Environment类内部例化。
- Test处在最高层,即处在例化Environment类的程序中。
- 功能覆盖(Function Coverage)的定义可以放在Environment类的内部或者外部。
事务处理器有一个简单的循环构成,这个循环从前面的块接收事务对象,经过变换后送给后续块。有一些块如Generator没有上游块的话,该事务处理器就创建和随机复制每一个事务。
//基本的事务类
class Transaction; //通用类
Transaction tr;
task run;
forever begin
//从前一个块中获取事务
...
//做一些处理
...
//发送到下游块中
...
end
endtask
endclass