Lab4采用面向对象封装的方式来搭建验证平台,但是前面的几个Lab都只给16个端口的其中一个灌激励。Lab5将多个组件都封装成类,使得测试平台能够同时驱动所有的端口。
Driver Class
Driver类是由DriverBase基类扩展而来的,基类中(1)定义接口信号、sa/da/payload、Packet类对象等;(2)定义与Driver相关的函数,比如前面的Lab中涉及到的send()
任务。因为每个端口都要发送数据,所以基类中定义了发送数据相关的property和method。
// DriverBase.sv
`ifndef INC_DRIVERBASE_SV
`define INC_DRIVERBASE_SV
class DriverBase;
virtual router_io.TB rtr_io;
string name;
bit[3:0] sa,da; // global variables
bit[7:0] payload[$];
Packet pkt2send;
extern function new(stirng name="DriverBase", virtual router_io.TB rtr_io);
extern virtual task send();
extern virtual task send_addrs();
extern virtual task send_pad();
extern virtual task send_payload();
endclass:DriverBase
// 相当于为DriverBase类创建空间,做一部分的初始化工作:初始化name和interface
function DriverBase::new(stirng name, virtual router_io.TB rtr_io);
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, name);
this.name = name;
this.rtr_io = rtr_io;
//this.pkt2send = new(); // DriverBase中未用到pkt2send,个人认为应为pkt2send分配空间并初始化
endfunction
// send 调用类中的其他方法
task DriverBase::send();
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
this.send_addrs();
this.send_pad();
this.send_payload();
endtask:send
task DriverBase::send_addrs();
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
this.rtr_io.cb.frame_n[this.sa] <= 1'b0;
for(int i=0; i<4; i++) begin
this.rtr_io.cb.din[this.sa] <= this.da[i];
@(this.rtr_io.cb);
end
endtask:send_addr
task DriverBase::send_pad();
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
this.rtr_io.cb.valid_n[this.sa] <= 1'b1;
this.rtr_io.cb.din[this.sa] <= 1'b1;
repeat(5) @(this.rtr_io.cb);
endtask:send_pad
task DriverBase::send_payload();
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
foreach(this.payload[index]) begin
for(int i=0; i<8; i++) begin
this.rtr_io.cb.din[this.sa] <= this.payload[index][i];
this.rtr_io.cb.valid_n[this.sa] <= 1'b0;
this.rtr_io.cb.frame_n[this.sa] <= ( index == (this.payload.size()-1) ) && ( i==7 );
@(this.rtr_io.cb);
end
end
this.rtr_io.cb.valid_n[this.sa] <= 1'b1;
endtask:send_payload
`endif
- 注意:sa、da、payload定义为全局变量,对于该类中的method都是可见的,在类中使用时用句柄指向该变量即
this.xxx
而不是其本身!
Driver类要做的事情有2件,一是将来自Gen的数据包送到Dvr,由Dvr发送到DUT端(由DriverBase类中的send()
实现);二是将Dvr刚发出去的数据包原样给Scb一份,用于和DUT输出端的数据包作比较。在Gen、Dvr、Scb之间通信的媒介是mailbox,数据被一个进程发送到另一个mailbox中,而另外一个进程可以从mailbox中获得该数据。
// router_test.h
typedef class Packet;
typedef mailbox #(Packet) pkt_mbox; // 参数化信箱定义:typedef mailbox #(type) mailbox_name;
mailbox
——信箱:允许两个线程之间进行数据交换,实现进程之间的同步。参考link- 可以把它看成是一个先进先出的存储数组。 通常有一个/多个进程把数据写入一个 mailbox, 有一个/多个进程从 mailbox 读出数据。写数据方法是
put()
;读数据方法是get()/peek()
,前者读取数据并且将其从队列中删除,后者复制数据且不删除队列中的原数据。 - mailbox可以创建成有界的队列,也可以是无界的队列。new的参数缺省即为无界:
new()
,本例中使用的参数为16:,本Lab的new[16]
,即队列的边界为16gen()
中创建了16个mailbox,每个都是无界的。当一个有界的mailbox所存储的数据达到其边界数目时,试图向已经满了的mailbox放置数据的进程会被阻塞,直到mailbox队列中有数据被读出,使得mailbox又有了足够的空间;而无界mailbox永远也不会阻塞一个发送操作进程。 - 本例中使用的是参数化信箱,用
typedef
的方式定义了Packet类型的信箱,表明其只存取Packet类型的数据。建议使用参数化信箱,防止因数据类型不一致而出差错。
- 可以把它看成是一个先进先出的存储数组。 通常有一个/多个进程把数据写入一个 mailbox, 有一个/多个进程从 mailbox 读出数据。写数据方法是
// Driver.sv
`ifndef INC_DRIVER_SV
`define INC_DRIVER_SV
`include "DriverBase.sv"
// Driver派生自基类DriverBase
class Driver extends DriverBase;
pkt_mbox in_box; // pass Packets object from Gen to Dvr
pkt_mbox out_box; // pass Packets object from Dvr to Scb
semaphore sem[]; // semaphore array
extern function new(string name = "Driver", int port_id, semaphore sem[], pkt_mbox in_box, out_box, virtual rtouer_io.TB rtr_io);
extern virtual task start();
endclass:Driver
`endif
// 将constructor的参数列表中的5个变量一一对应到类对象(包括继承自基类的对象)
function Driver::new(string name, int port_id, semaphore sem[], pkt_mbox in_box, out_box, virtual rtouer_io.TB rtr_io);
super.new(name, rtr_io);
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
this.sa = port_id;
this.sem = sem;
this.in_box = in_box;
this.out_nox = out_box;
endfunction
// 关键的一步的动作是从in_box中取出要发送的数据包,送到pkt2send中
// 再将pkt2send的属性传递到基类的全局变量中,最后通过send任务发送
task Driver::start();
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
fork // 个人理解此处fork并发执行的是各个dvr的start(结合test.sv来看)
forever begin
this.in_box.get(this.pkt2send);
if(this.pkt2send.sa != this.sa) continue; // 跳过后面的语句,继续下一轮循环
this.da = this.pkt2send.da;
this.payload = this.pkt2send.payload;
this.sem[this.da].get(1);
this.send();
this.out_box.put(this.pkt2send);
this.sem[this.da].put(1);
end
join_none
endtask:start
`endif
new()
与super.new()
:SV中的class通过new()
来创建实例,即创建类的对象;在创建对象的过程中,可以做一些初始化的工作。super.new()
是父类的new()
函数,子类通过super
来索引父类的同名函数:
子类在定义new函数时,应该首先调用父类的new函数即super.new( )。如果父类的new函数没有参数,子类也可以缺省该调用,而系统会在编译时自动添加super.new( )函数。
子类中使用this.xxx是先访问子类有没有xxx的变量或者方法;而super.xxx直接访问父类的xxx变量和方法,不会访问子类。
semaphore
——旗语:通过仲裁实现对共享资源的访问。- 如果多个进程要向同一个端口发送数据,每个进程必须等待权限到来后才能发送。本例中Gen产生的数据包是随机的,很可能出现多个输入想要驱动一个端口的情况。每当出现多个输入竞争同一个输出端口的时候,必须通过仲裁来决定哪个端口能够获得这个共享的资源。
- 当为semaphore分配内存的时候,可以认为会创建一个带有一定数目key的存储桶(从test.sv中得知,本例中key=1)。使用semaphore的进程在继续执行之前必须先从存储桶中获得一个key,获得的方式是使用get方法。所有其他进程必须等待,直到其需要的足够数目的key被放回到存储桶中,放回的方式是使用put方法。
- Driver的行为逻辑:不停地从Gen产生的
in_box
中获取数据包,放到pkt2send
里;如果获取的数据包正好是要在此Dvr口this.sa
发送的,则将pkt2send
的属性传递到基类的全局变量this.da/this.payload
中,否则重新获取数据包;如果这时该数据包的输出端口this.da
没有被占用即获得了key,则调用基类的send()
把数据发送给DUT,否则等待输出端口被释放;接着将发送的数据包pkt2send
原样放一份到out_box
里;最后释放输出端口,将key放回存储桶中。
Receiver Class
Receiver类派生自ReceiverBase基类。基类需要做的事情时定义与待接收的数据包相关的信号和接收数据包的具体任务。
// ReceiverBase.sv
`ifndef INC_RECEIVERBASE_SV
`define INC_RECEIVERBASE_SV
class ReceiverBase;
virtual router_io.TB rtr_io;
bit[3:0] da;
bit[7:0] pkt2cmp_payload[$];
Packet pkt2cmp;
extern function new(string name = "ReceiverBase", virtual router_io.TB rtr_io);
extern virtual task recv();
extern virtual task get_payload();
endclass:ReceiverBase
// new函数用于创建对象,在创建对象的过程中做一部分初始化工作
function ReceiverBase::new(string name, virtual router_io.TB rtr_io);
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, name);
this.name = name;
this.rtr_io = rtr_io;
this.pkt2cmp = new(); // 调用new方法分配空间和初始化
endfunction
task ReceiverBase::recv();
static int pkt_cnt = 0;
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
this.get_payload();
this.pkt2cmp.da = this.da;
this.pkt2cmp.payload = this.pkt2cmp_payload;
this.pkt2cmp.name = $psprintf("rcvdPkt[%0d]", pkt_cnt++);
endtask:recv
task ReceiverBase::get_payload();
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
this.pkt2cmp_payload.delete();
fork // 被称为guard fork,用于限制disable fork的作用范围
begin:wd_timer_fork
fork:frameo_wd
@(negedge this.rtr_io.cb.frameo_n[this.da]);
begin
repeat(10000) @(this.rtr_io.cb);
$finish;
end
join_any:frameo_wd
disable fork; // kill掉与之同一个层次的所有fork,在本例中就是wd_timer_fork
end:wd_timer_fork
join
forever begin
logic[7:0] datum;
for(int i=0; i<8; ) begin
if(!this.rtr_io.cb.valido_n[this.da])
datum[i++] = this.rtr_io.cb.dout[this.da];
if(this.rtr_io.cb.frameo_n[this.da])
if(i==8) begin
this.pakt2cmp_payload.push_back(datum);
return; // 任务立即退出而不需要执行到末尾
end
else begin
$display("\n%m\n[ERROR]%t Packet payload not byte aligned!\n", $realtime);
$finish;
end
@(this.rtr_io.cb);
end
this.pkt2cmp_payload.push_back(datum);
end
endtask:get_payload
`endif
Receiver做的事情是调用基类中的recv()
任务,再拷贝一份基类中的数据包,放进out_box信箱中。
// Receiver.sv
`ifndef INC_RECEIVER_SV
`define INC_RECEIVER_SV
`include "ReceiverBase.sv"
// 派生自ReceiverBase,不需要再次定义name和interface
class Receiver extends ReceiverBase;
pkt_mbox out_box; // Scoreboard mailbox
extern function new(string name = "Receiver", int port_id, pkt_mbox outbox, virtual router_io.TB rtr_io);
extern virtual task start();
endclass
// 将constructor的参数列表中的变量一一对应到类对象
function Receiver::new(string name, int port_id, pkt_mbox out_box, virtual router_io.TB rtr_io);
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
super.new(name, rtr_io);
this.da = port_id;
this.out_box = out_box;
endfunction
task Receiver::start();
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
fork
forever begin
this.recv(); // 注意要用this.recv()而不是recv()!
begin
Packet pkt = new this.pkt2cmp; // 浅拷贝一份pkt2cmp
this.out_box.put(pkt);
end
end
join_none // 注意是join_none,不是join!
endtask
`endif
- 通过
new xxx
来实现浅拷贝。浅拷贝又叫句柄拷贝,上述代码中的pkt
只拷贝了pkt2cmp
的数据变量,未拷贝pkt2cmp
的数据操作(如类的对象和方法,此例中是compare()和display
函数)以及其中定义的其他类的句柄,可采用“引用”的方式访问。 - 特别注意最后应该使用fork…join_none,不要误写未fork…join!!!fork…join不能实现non-blocking并发。
more
后续见《SystemVerilog Testbench Lab5: broad spectrum verification(下)》