续《SystemVerilog Testbench Lab5: broad spectrum verification(上)》
Generator Class
Generator类要做的事情:(1)通过随机的方式产生待发送的数据,即随机激励;(2)将产生的数据拷贝一份后放进信箱里,以便于和其他进程通信。
`ifndef INC_GENERATOR_SV
`defien INC_GENERATOR_SV
class Generator;
string name;
Packet pkt2send;
pkt_mbox out_box[]; // []声明的是一个含有多个元素的数组,此时为空
extern function new(string name = "Receiver");
extern virtual task gen();
extern virtual task start();
endclass
function Generator::new(string name);
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, name);
this.name = name; // 等式右边的name为入口参数,string类型
this.pkt2send = new(); // 为其对象pkt2send分配空间
this.out_box = new[16]; // 调用new[]来分配空间,同时传递数组的深度(即16)
foreach(this.out_box[i]) // 动态数组一一初始化
this.out_box[i] = new();
endfunction
// 用randomize方法随机化激励
task Generator::gen();
static int pkt_generated = 0;
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
this.pkt2send.name = $psprintf("rcvdPkt[%0d]", pkt_generated++);
if( !pkt2send.randomize() ) begin
$dispaly("\n%m\n[ERROR]%t Randomization Failed!\n", $realtime);
$finish;
end
endtask:gen
// 控制产生激励的个数,以及复制激励到信箱中
task Generator::start()
if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
fork
for(int i=0; run_for_n_packets<=0 || i < run_for_n_packets; i++) begin
this.gen();
begin
Packet pkt = new this.pkt2send; // 注意此处是this.pkt2send。光写pkt2send是不够的!!!
this.out_box[pkt.sa].put(pkt); // 每个入口有一个单独的数组用于存储源地址与自身相同的包
end
end
join_none
endtask:start
`endif
Generator::new()
中需要创建16个mailbox,用new[16]
为16个mailbox分配空间,并调用new()
方法一一初始化,否则在使用out_box时会报错并提示:使用之前未分配空间(见下图)。- 疑惑:其他进程中使用mailbox之前调用
new()
方法了吗?默认使用了new()
方法,原因参考下面这段话。用户可以自定义new
方法赋初值,SV默认为未自定义的对象分配空间和初始化。
SV默认会为每个类创建一个new函数,并初始化对象(二值变量为0,四值变量为X)。用户一般自定义new函数赋初值,分配空间可以交给SV来做。
- 由于动态数组在声明时未真正分配内存,分配内存的方法有:①使用
new[]
方法分配内存,再另外初始化;②直接列表初始化动态数组,隐含了内存分配内存。
Scoreboard Class
Scoreboard要做的事情是:(1)接收分别来自Drivers和Receivers的数据;(2)当Drivers数据的da与Receivers数据的da一致时,才将数据的尺寸和内容进行比较。
`ifndef INC_SCOREBOARD_SV
`ifndef INC_SCOREBOARD_SV
class Scoreboard;
string name;
event DONE;
Packet refPkt[$];
Packet pkt2send, pkt2cmp;
pkt_mbox driver_mbox, receiver_mbox;
extern function new(string name = "Scoreboard", pkt_mbox driver_mbox = null, recevier_mbox = null);
extern virtual task start();
extern virtual task check(); // compare
endclass
function Scoreboard::new(string name, pkt_mbox driver_mbox, recevier_mbox);
this.name = name;
if(driver_mbox == null) driver_mbox = new(); // 为入口参数driver_mbox分配空间
this.driver_mbox = diver_mbox;
if(receiver_mbox == null) receiver_mbox = new(); // 为入口参数recevier_mbox分配空间
this.receiver_mbox = receiver_mbox;
endfunction
task Scoreboard::start();
fork
forever begin
this.receiver_mbox.get(this.pkt2cmp);
while( this.driver_mbox.num() ) begin
Packet pkt;
this.driver_mbox.get(pkt);
this.refPkt.push_back(pkt);
end
this.check();
end
join_none
endtask:start
task Scoreboard::check();
int index[$];
string message;
static int pkt_checked = 0;
index = this.refPkt.find_first_index() with (item.da == this.pkt2cmp.da);
if(index.size() <= 0) begin
$display("\n%m\n[ERROR]%t %s not found in Reference Queue\n", $realtime, this.pkt2cmp.name);
this.pkt2cmp.display("ERROR");
$finish;
end
this.pkt2send = this.refPkt[inedx[0]];
this.refPkt.delete(index[0]);
if(!this.pkt2send.compare(this.pkt2cmp, message)) begin
$display("\n%m\n[ERROR]%t Packet #%0d %s\n", $realtime, pkt_checked++, message);
this.pkt2send.display("[ERROR]");
this.pkt2cmp.display("[ERROR]");
$finish;
end
$display("\n%m\n[NOTE]%t Packet #%0d %s\n", $realtime, pkt_checked++, message);
if(pkt_checked >= run_for_n_packets)
-> this.DONE;
endtask:check
`endif
- 信箱方法
mailbox.num()
:查询mailbox中数据的个数。 - 队列方法
queue.find_first_index() with (condition)
:返回队列中第一个满足某个条件的元素的索引值;queue.delete(n)
:删除队列中索引为n的元素,若n缺省则删除整个队列中的元素。队列索引值从0开始。 - Scoreboard的最终目的是比较来自divers与receivers的数据包,实现的过程是:Receiver类中的信箱在不停地接收从DUT发出的数据,Drivers类中的信箱在不停地将待传输的数据原样拷贝给Scoreboard;sb先从
receiver_mbox
中取出一个数据放到pkt2cmp
中,然后把driver_mbox
中的数据一个个取出,存到refPkt
队列里;在refPkt
队列中找到第一个与pkt2cmp
的目的地址相同的元素,如果该元素不存在的话则报错,否则将这第一个元素赋值给pkt2send
,并且从原参考队列中删除它;调用Packet的compare()
方法比较pkt2send
和pkt2cmp
,如果比较结果不正确则打印不一致的原因和两个数据包的sa、da、payload
并退出仿真,否则打印成功的信息;如果最后统计到的匹配的个数与发送的数据包的个数相同,则触发一个DONE event
来“优雅地”结束仿真。
testbench
test.sv中创建了16个Driver、16个Receiver、1个Generator、1个Scoreboard,并通过信箱将各个组件连在一起。
// test.sv
program automatic test();
int run_for_n_packets;
int TRACE_ON = 0;
`include "Packet.sv"
`include "router_test.h"
`include "DriverBase.sv"
`include "Driver.sv"
`include "ReceiverBase.sv"
`include "Receiver.sv"
`include "Generator.sv"
`include "Scoreboard.sv"
semaphore sem[]; // 16 semaphores for 16 drivers
Driver drvr[]; // 16 drivers
Receiver rcvr[]; // 16 receivers
Generator gen;
Scoreboard sb;
initial begin
run_for_n_packets = 2000;
sem = new[16];
drvr = new[16];
rcvr = new[16];
gen = new();
sb = new();
// function new(int keyCount = 0)
foreach(sem[i]) sem[i] = new(1);
// function new(string name, int port_id, semaphore sem[], pkt_mbox in_box, out_box, virtual rtouer_io.TB rtr_io);
foreach(drvr[i]) drvr[i] = new($sformatf("drvr[%0d]", i), i, sem, gen.out_box[i], sb.driver_mbox, rtr_io);
// function Receiver::new(string name, int port_id, pkt_mbox out_box, virtual router_io.TB rtr_io);
foreach(rcvr[i]) rcvr[i] = new($sformatf("rcvr[%0d]", i), i, sb.receiver_mbox, rtr_io);
reset();
gen.start();
sb.start();
foreach(drvr[i]) drvr.start(); // non-blocking assignment in task start
foreach(rcvr[i]) rcvr.start(); // non-blocking assignment in task start
wait(sb.DONE.triggered);
end
task reset();
if (TRACE_ON) $display("[TRACE]%t :%m", $realtime);
rtr_io.reset_n = 1'b0;
rtr_io.cb.frame_n <= '1; // 注意:这里缺省了位宽,如果误写成1,那么只有最低位被置1,其他位全为0!!!
rtr_io.cb.valid_n <= '1;
##2 rtr_io.cb.reset_n <= 1'b1;
repeat(15) @(rtr_io.cb);
endtask:reset
endprogram:test
- 疑惑1:
driver_mbox、receiver_mbox
各自只有一个,而Drivers、Receivers各有16个,怎么确保多个进程向同一个信箱放数据不会产生冲突?不会。虽然foreach执行16次start任务,但是个人认为数据进入mailbox中并不一定要是串行的,两封信件同时到达信箱就同时收下。信箱的作用是收信,只要信收到了就完成任务了,因而即使多个进程同时调用start()
内是blocking assignment
put()
方法也不会产生冲突。 - 疑惑2:16个drivers进程往同一个
dirver_mbox
里放数据时,究竟哪一个driver的数据被先放进去?放的顺序不会影响对比的结果,因为总是用pkt2cmp.da
将dirver_mbox
的数据分类,而且入口的sem
也限制了同一时刻不会发生多个输入端口往同一个输出端口发送数据的情况发生。SV是怎么存数据的? event
——事件:用来同步多个并发的进程(比如,某个进程等待着事件,而另一个进程触发这个事件)。- 在sb中,首先通过
event event_name
定义了一个事件,若所有数据都匹配,则通过-> event_name
触发该事件。 触发形式类似于边沿触发,触发沿到来时会结束当前所有由于等待这个事件而被阻塞的进程。 triggered()
是查询事件是否被触发的方法,即event_name.triggered()
,若被触发则返回1。等待事件触发用@event_name
或者wait(event_name.triggered())
。
- 在sb中,首先通过
- 最后验证的时候竟然因为
reset()
写错了而导致用例failed!!!具体错误原因是frame_n、valid_n
赋值时写成了rtr_io.cb.xxx_n <= 1'b1
,即只有最低位置1其他位全部置0,dut没有获得一个正确的初始工作状态,造成用例对比失败。
make bad
运行错误地rtl代码得到了错误的输出结果,log文件打印的错误提示如下图所示,可见有两组不同的数据包被送到了同一个目的端口da = 1
,源端口分别是sa = 15
和sa = 14
。而且sa = 15
先送到了da = 1
端口,而输出端口da = 1
接收到的数据实际上是由sa = 14
发送的。
- debug思路:sorceboard根据
pkt2cmp.da
从所有已经发送的pkt2send
中匹配到第一个有相同的da
的数据包,如果找到则将该pkt2send
从队列中删除。旗语的仲裁机制已经保证了两个不同的输入不可能同时驱动一个相同的输出,而现在仿真出现了这种情况,可能的原因有2个:- 旗语没有起到有效的仲裁作用?不应该。因为验证正确代码是可以PASS的,旗语的用法无误。
- 错误的
sa = 15, da = 1
的数据包没有被有效删除?回到log和波形中核对sa = 15, da = 1
和sa = 14, da = 1
对应的数据包分别什么时候出现,发现: - 在14050ns处
sa = 15
就向da = 1
端发送数据了,但是da = 1
一直未收到该笔数据,因而这笔数据一直在pkt2send
的队列里保存着;
da = 1
端口再接收到数据时,已经是由sa = 14
向在26450ns时刻向da = 1
发送的数据了。这个数据包被da = 1
正常接收且进入socreboard中参与比较。
- 综上,bad rtl错在
sa = 15
向da = 1
发送数据,但是数据并未被da = 1
接收到,因而要修改bad rtl中这部分的路由逻辑!