实验3和实验2的区别(以实验2的tb4和实验3的chnl_pkg和tb为例)
1、 chnl_trans变复杂了。
1.1 填充更多的数据。 lab2的是实验代码很简单,也没有随机化,只是通过generator进行赋值,并且每次只能发送一个数据。lab3每次发送的数据量和种类变多了,一次发送的数据用数组data[]来装,再加上附加的信息变量(ch_id、pkt_id、data_nidles、pkt_nidles),打成一个包pkt。
1.2 变量用rand进行声明,之后进行随机化赋值;
class chnl_trans;
int data;
int id;
int num;
endclass: chnl_trans
1.3 多了一个sprint的方法,通过把trans的内容写到字符串s,来打印当前trans的内容
function string sprint();
string s;
s = {s, $sformatf("=======================================\n")};
s = {s, $sformatf("chnl_trans object content is as below: \n")};
s = {s, $sformatf("obj_id = %0d: \n", this.obj_id)};
foreach(data[i]) s = {s, $sformatf("data[%0d] = %8x \n", i, this.data[i])};
s = {s, $sformatf("ch_id = %0d: \n", this.ch_id)};
s = {s, $sformatf("pkt_id = %0d: \n", this.pkt_id)};
s = {s, $sformatf("data_nidles = %0d: \n", this.data_nidles)};
s = {s, $sformatf("pkt_nidles = %0d: \n", this.pkt_nidles)};
s = {s, $sformatf("rsp = %0d: \n", this.rsp)};
s = {s, $sformatf("=======================================\n")};
return s;
endfunction
2、 lab3中对于数据的控制更加集中了。lab2中,agent控制着发送数据的数量,而数据的内容由generator控制;到了lab3,数据的数量和内容都由generator控制
3、initiator永远不会停下。
lab2里initiator没有自己的run任务,它的调用发生在agent里,会被重复调用ntrans次。
//lab2
class chnl_agent;
……
task run();
repeat(this.ntrans) this.init.chnl_write(this.gen.get_trans());//重复ntrans次
this.init.chnl_idle();
endtask
endclass: chnl_agent
而在lab3里initiator有自己的run任务,并且用上了forever,就像一个吃的停不下来的胖子,发现generator产生一个数据,就会往接口发送一个数据,永远运行。
//lab3
task run();
this.drive();
endtask
task drive();
chnl_trans req, rsp; //这俩句柄一开始没有实例化,是悬空的
@(posedge intf.rstn);
forever begin
this.req_mb.get(req);
this.chnl_write(req);
rsp = req.clone(); //产生一个和req内容一样的新的对象
rsp.rsp = 1;
this.rsp_mb.put(rsp);
end
endtask
一、随机约束
实验要求1.1:继承Lab2的大部分代码,基于Lab2对需要生成的数据包有了新的约束要求。数据类chnl_trans不再只局限于包含一个数据,而是多个数据,同时跟数据之间时间间隔的控制变量也在该类中声明为随机成员变量。
实验要求1.2:Could we put c.obj_id = this.obj_id here? and why?
把这一行加进去,你会发现结果并没有变。这个问题问的其实不是行不行,而是有没有意义。也就是说把这一行添加进去可以,但是没有意义。原因如下:
obj_id被设置为了静态变量,它处在一个独立的空间,既不在c里也不在this里。c和this都是通过访问静态变量的空间来获得obj_id的值。也就是说,加了这句话c.obj_id = this.obj_id,其实就相当于obj_id = obj_id,或者按类来索引写成,chnl_trans::obj_id = chnl_trans::obj_id,把自己赋值给自己,没有意义。
实验要求1.3:将原本在chnl_root_test类中用来结束仿真的$finish()变迁到generator中。
按照路桑在视频中讲的,我们不能直接把$finish()移动到generator里面,因为三个generator只要有一个finish,那么整个仿真就会退出。
其实在这里仿真的退出控制是generator在控制的。chnl_root_test运行时会调用agent的run,进而调用initiator和generator的run。而initiator是永不停止的,所以相当于generator数据产生完之后就会结束。也就是说是generator在控制仿真的退出。
实验要求1.4:使用“restart”命令多次重启,比对连续两次生成的随机数据。然后再在仿真器命令行处使用“vsim -novopt -solvefaildebug -sv_seed 0 work.tb1”来加载仿真。
1、仿真参数“-solvefaildebug”是为了调试随机变量的。如果仿真时随机化失败,会告诉你原因,比如约束产生冲突。
2、“-sv_seed NUM”则是为了传递随机的种子,如果不写,那么每次restart后的数据其实都一样,而不是随机化。
3、最后再使用“vsim -novopt -solvefaildebug -sv_seed random work.tb1”命令加载仿真,对比前后两次的数据是否相同。
这里直接用随机数random来作为种子号,所以每次随机的数据就不一样。
第一次:
第二次:
实验要求1.5:在仿真的最后,同学们可以发现最后一个打印出来的chnl_trans对象的obj_id值是1200,那么这代表什么含义?为什么会有1200个chnl_obj对象产生呢?整个仿真过程中,在同一时刻,最多的时候一同有几个chnl_trans对象在仿真器内存中存在呢?这么做对内存的利用是否合理?你是否还有更好的办法使得在同一时间chnl_trans对象的数量比代码中用到的更少呢?
①obj_id值是1200,意思是产生了1200个对象。为啥是1200个呢?因为在chnl_basic_test里ntrans = 200,而在initiator里发送数据时会调用clone,产生新的rsp对象,最后是因为有3个agent在发送数据,所以200 x 2 x 3 = 1200
②9个。
下面通过图像来解释。注意,gen和ini中的req、rsp句柄共有4个,是一直存在的。而我们讨论的是chnl_trans的对象。当我们调用new函数或者clone函数时,就会在资源池里产生新的对象。
a.刚开始,gen生成了一个对象req@1,经过req_mb来到ini,并调用了clone又生成了另一个对象rsp@1。而当ini将rsp@1经过rsp_mb返回给generator后,此时一共有两个对象,req@1、rsp@1。如下图:
b.当generator会新生成一个对象req@2,此时initiator里的还是req@1,因为initiator里的task drive()是在不断执行的,此时同时存在3个对象。如下图:
c.ini中的句柄req更新后会指向req@2,原来的对象req@1因为没有句柄指向了,所以被回收了。而且克隆产生了一个新的对象rsp@2。如下图:
d.之后rsp@2对应的句柄经过rsp_mb回到了gen,更新了之前指向rsp@1的句柄的值,因此rsp@1因为没有句柄指向了,也被回收。此时剩下2个对象,req@2和rsp@2。
所以说,对于一个agent,同一时刻最多存在3个对象。整个仿真过程中,在同一时刻,3个agent最多的时候一同有9个chnl_trans对象在仿真器内存中存在。
③这么做对内存的利用是合理的,通过req_mb和rsp_mb这种乒乓操作,使得req和rsp对象不断动态生成和回收,不至于使内存不断膨胀。
也就是说,当generator判断了rsp的标志位为1时,generator会产生新的req到req_mb中去,并且发送给initiator。此时initiator中的req句柄就指向了新的对象。而原来的对象由于没有句柄指向它了,因此该对象会被SV自动销毁,空间被回收。
二、更加灵活的测试控制
如果要实现不同的test类,例如chnl_basic_test、chnl_burst_test、chnl_fifo_full_test,那么对于不同的test需要对chnl_generator的随机变量做出不同的控制,继而进一步控制其内部随机的chnl_trans对象。也就是说,**随机化也是可以分层次的,例如在test层可以随机化generator层,而依靠generator被随机化的成员变量,再来利用它们进一步随机化generator中的chnl_trans对象,由此达到顶层到底层的随机化灵活控制。**从这个角度出发,需要将generator从agent单元中搬迁出来,并且搁置在test层中来方便test层的随机控制。
实验要求:
2.1. 将generator搬迁到test层次中,需要将gen和agent中组件的mailbox连接起来,方便gen与agent中init的数据通信。
this.agent[i].init.req_mb = this.gen[i].req_mb;
this.agent[i].init.rsp_mb = this.gen[i].rsp_mb;
2.2、2.3 实现在test中使用do_config()对gen[1] gen[2]进行随机化控制。
assert (gen[1].randomize() with {ntrans==50; data_nidles inside [1:2]; pkt_nidles inside [3:5];data_size==6;})
else $fatal("[RNDFAIL] gen[1] ramdomization failure!");
assert (gen[2].randomize() with {ntrans==80; data_nidles inside [0:1]; pkt_nidles inside [1:2];data_size==32;})
else $fatal("[RNDFAIL] gen[2] ramdomization failure!");
2.4 请按照代码中的具体要求,在chnl burst test:do config0任务中对三个generator进行随机控制。要求
// each channel send data packet number inside [80:100]
// data_nidles == 0, pkt_nidles == 1, data_size inside {8, 16, 32}
class chnl_burst_test extends chnl_root_test;
function new(string name = "chnl_burst_test");
super.new(name);
endfunction
//USER TODO
virtual function void do_config();
super.do_config();
foreach(gen[i]) begin
assert(gen[i].randomize() with {ntrans inside [80:100]; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[%0d] randomization failure!",i);
end
endfunction
endclass: chnl_burst_test
2.5 请按照代码中的具体要求,在chnl fifo full test:do config0任务中对三个generator进行随机控制。
2.6 在tb2.sv中,我们对于测试的选择将由仿真时的参数传递来完成。这意味着,以后的递归测试,即创建脚本命令,由仿真器读入,分别传递不同的随机种子和测试名称即可完成对应的随机测试,而这种方式即是回归测试的雏形。所以请同学们按照之前的仿真命令,在命令窗口中添加额外的命令"+TESTNAME=testname" ,这里的+TESTNAME=表示的仿真命令项,在由内部解析之后, testname会被捕捉并且识别,例如你可以传递命令"+TESTNAME=chnl burst test"来在仿真时运行测试chnl burst test,请同学充分理解要求2.6的代码部分,懂得如何捕捉命令,如何解析命令,最后如何选择正确的测试来运行。
解析:
①为什么要通过仿真命令项"+TESTNAME=testname"来进行仿真?
如果按照tb1的方式,那么当你下次想要运行burst_test时,就需要在initial块里面修改,然后再次编译。但是问题就在于需要重新编译tb,这需要花费多余的时间,对于较大的系统甚至要几个小时,不可取。
//tb1
initial begin
basic_test = new();
basic_test.set_interface(chnl0_if, chnl1_if, chnl2_if);
basic_test.run();
end
②"+TESTNAME=testname"能省去额外编译步骤的原理
将全部的test都进行句柄声明和实例创建,但是只会单独给用户传入的testname进行接口设置和运行,其他的test不运行。由于tb本身是不变的,因此不用因为传入的testname的变化而重新编译。
这一点在UVM里也是类似的。
module tb2;
……
import chnl_pkg2::*; //导入chnl_pkg2,让里面的类名可以被识别
chnl_intf chnl0_if(.*);
chnl_intf chnl1_if(.*);
chnl_intf chnl2_if(.*);
chnl_basic_test basic_test;
chnl_burst_test burst_test;
chnl_fifo_full_test fifo_full_test; //声明三个test句柄
chnl_root_test tests[string]; //声明一个用来存放句柄的关联数组
string name;
initial begin
basic_test = new(); //例化三个test
burst_test = new();
fifo_full_test = new();
tests["chnl_basic_test"] = basic_test;
tests["chnl_burst_test"] = burst_test;
tests["chnl_fifo_full_test"] = fifo_full_test;
if($value$plusargs("TESTNAME=%s", name)) begin //此系统函数用于在仿真时将输入的test参数传递至内部信号name
if(tests.exists(name)) begin //如果输入的test存在,则进行相应的接口设置并运行
tests[name].set_interface(chnl0_if, chnl1_if, chnl2_if);
tests[name].run();
end
else begin
$fatal($sformatf("[ERRTEST], test name %s is invalid, please specify a valid name!", name)); //不存在则报错
end
end
else begin
$display("NO runtime optiont TEST=[testname] is configured, and run default test chnl_basic_test");//没添加TESTNAME则默认执行chnl_basic_test
tests["chnl_basic_test"].set_interface(chnl0_if, chnl1_if, chnl2_if);
tests["chnl_basic_test"].run();
end
end
endmodule
三、测试平台的结构
最后一个实验部分即指导同学们认识验证环境的其它组件, monitor和checker,并且通过合理的方式来构成最终用来测试MCDT的验证环境,在这个环境中同学需要再回顾generator.initiator, monitor和checker各自的作用。**在顶层环境中,我们将checker置于test层中,而不是agent中,需要同学们思考这么做的好处在什么地方。**同时需要认识generator和Initiator有数据通信的同时,可以掌握monitor与checker之间的数据通信,还有checker如何针对MCDT利用内部的数据缓存进行数据比较。
要求3.1在chnl_monitor类和mcdt_monitor类各自的mon_trans)方法中需要采集正确的数据,将它们写入mailbox缓存,同时将捕捉的数据也打印出来,便于我们的调试。
task mon_trans();
mon_data_t m;
forever begin
@(posedge intf.clk iff (intf.mon_ck.ch_valid==='b1 && intf.mon_ck.ch_ready==='b1));
// USER TODO 3.1
// Put the data into the mon_mb and use $display() to print the stored
// data value with monitor name
m.data = intf.mon_ck.ch_data; //从接口拿数据
mon_mb.put(m); //把数据放入信箱
$display("%0t %s monitored channel data %8x",$time, this.name, m.data);
end
endtask
task mon_trans();
mon_data_t m;
forever begin
@(posedge intf.clk iff intf.mon_ck.mcdt_val==='b1);
// USER TODO 3.1
// Put the data into the mon_mb and use $display() to print the stored
// data value with monitor name
m.data = intf.mon_ck.mcdt_data;
m.id = intf.mon_ck.mcdt_id; //别漏掉
mon_mb.put(m);
$$display("%t %s monitored mcdt data %8x and id %0d.",$time, this.name, m.data, m.id);//漏了id
end
endtask
要求3.2在chnl_agent中,参考如何例化的initiator对象,也对chnl_monitor对象开始例化、传递虚接口和使其运行。
要求3.3在chnl_checker的任务do_compare)中,同学需要从checker自己的数据缓存mailbox中分别取得一个输出端的采集数据和一个输入端的采集数据,继而将它们的内容进行比较,需要注意的是,输出端的缓存只有一个,而输入端的缓存有三个,同学需要考虑好从哪个输入端获取数据与输出端缓存的数据进行比对
先从out_mb里拿数据,通过id值知道它数据从哪里来,再到相应的in_mb里拿数据,最后进行比对
要求3.4在顶层环境chnl_root_test类中,同学们需要对mcdt_monitor和chnl_checker进行例化、传递虚接口,并且将chnl_monitor, mcdt_monitor的邮箱句柄分别指向chnl_checker中的邮箱实例。
1
代码细品
A、generator和initiator是如何进行通信的?
为了更好地了解他们的通信,我们先来了解一下这里面的两个信箱。
在chnl_generator里声明了两个信箱句柄req_mb和rsp_mb,此时句柄是悬空的;
之后在new函数里进行例化,生成两个对象,此时句柄不悬空,在上图中画成一个圆柱状来表明这一点。
class chnl_generator;
……
mailbox #(chnl_trans) req_mb;//声明句柄
mailbox #(chnl_trans) rsp_mb;
function new(int ch_id, int ntrans);
……
this.req_mb = new(); //gen里的信箱例化了,所以句柄不悬空
this.rsp_mb = new();
endfunction
而在chnl_initiator里,同样声明了两个信箱句柄req_mb和rsp_mb,但是和chnl_generator不同的是,这两个信箱没有实例化,句柄一开始是悬空的,因此图中的initiator中的两个信箱用两个圆圈来表示。initiator没有自己的信箱对象。
也就是说,对于一个agent来说,只有两个信箱对象。
class chnl_initiator;
……
mailbox #(chnl_trans) req_mb; //这两个没有在new里例化,仅仅是句柄,所以是悬空的
mailbox #(chnl_trans) rsp_mb;
function new(string name = "chnl_initiator");
this.name = name;
endfunction
直到在chnl_agent里的task run()中进行句柄的赋值,也就是将chnl_generator里不悬空的信箱句柄赋值给chnl_initiator里的req_mb 和rsp_mb ,此时chnl_initiator里的信箱句柄也不悬空了,分别指向了chnl_generator里的req_mb 和rsp_mb 对象,为之后的握手通信做好准备。
class chnl_agent;
……
task run();
this.init.req_mb = this.gen.req_mb; //init里的req_mb和rsp_mb不悬空了
this.init.rsp_mb = this.gen.rsp_mb;
……
endtask
endclass: chnl_agent
那agent是如何利用俩信箱完成通信的咧?
先回顾一下lab2是如何发送数据的。(看下面两段代码)
首先是调用了chnl_generator里的get_trans()方法,产生数据并返回一个句柄,这个句柄被马上返回给chnl_initiator的chnl_write(),从而将数据发送到channel中去。
class chnl_generator;
chnl_trans trans[$];
……
function chnl_trans get_trans();
chnl_trans t = new();
t.data = 'h00C0_0000 + (this.id<<16) + this.num;//
t.id = this.id;
t.num = this.num;
this.num++;
this.trans.push_back(t);
return t;
endfunction
endclass: chnl_generator
class chnl_agent;
task run();
repeat(this.ntrans) this.init.chnl_write(this.gen.get_trans());
this.init.chnl_idle(); // set idle after all data sent out
endtask
endclass:chnl_agent
而对于lab3,则没有那么直接,因为多了两个信箱,这两个信箱用于存储数据,方便进行握手通信。具体如下:
在chnl_generator中的send_trans(),先声明了两个句柄req, rsp,并对req进行实例化,随机化,然后先把发送前的req指向的对象打印出来,方便之后比对;紧接着就把req句柄的值放到信箱req_mb里,等待initiator把句柄req取走然后返回一个rsp握手信号。
(注意,放进mailbox的都是句柄的值,也就是都是32位的数据,而不是对象。其实我们在代码里看到的都是句柄,对象存在于资源池里,在sv里是无法被用户直接操作的。我们拿着句柄就可以访问到对应的对象了,没必要去操作对象)
class chnl_generator;
int pkt_id;
int ch_id;
int ntrans;
int data_nidles;
mailbox #(chnl_trans) req_mb;
mailbox #(chnl_trans) rsp_mb;
function new(int ch_id, int ntrans);
this.ch_id = ch_id;
this.pkt_id = 0;
this.ntrans = ntrans;
this.req_mb = new(); //gen里的信箱例化了,所以句柄不悬空
this.rsp_mb = new();
endfunction
task run();
repeat(ntrans) send_trans();
endtask
// generate transaction and put into local mailbox
task send_trans();
chnl_trans req, rsp;
req = new();
assert(req.randomize with {ch_id == local::ch_id; pkt_id == local::pkt_id;})
else $fatal("[RNDFAIL] channel packet randomization failure!");
this.pkt_id++;
$display(req.sprint());//发送前打印一下
this.req_mb.put(req);
this.rsp_mb.get(rsp);
$display(rsp.sprint());//发送后再打印一下,可以通过rsp的不同
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);//如果rsp为1,说明发送正常,如果失败,rsp就还是0,所以报错
endtask
endclass: chnl_generator
为了更好理解,我们先把目光转向chnl_initiator那边,看initiator是如何把req取走然后返回一个rsp握手信号的。
class chnl_initiator;
……
mailbox #(chnl_trans) req_mb;
mailbox #(chnl_trans) rsp_mb;
……
task drive();
chnl_trans req, rsp; //这俩句柄一开始没有实例化,是悬空的
@(posedge intf.rstn);
forever begin
this.req_mb.get(req);
this.chnl_write(req);
rsp = req.clone(); //产生一个和req内容一样的新的对象
rsp.rsp = 1;
this.rsp_mb.put(rsp);
end
endtask
endclass: chnl_initiator
先声明了两个信箱句柄,这俩句柄在chnl_agent里已经完成赋值,指向了chnl_generator里的两个信箱对象了。因此initiator可以通过 this.req_mb.get(req)这一句将generator产生的数据get到,紧接着通过chnl_write发送出去。随后为了进行握手通信,initiator先通过rsp = req.clone()克隆了一个和req一模一样的对象,注意,这是一个新的对象,然后将rsp对象里面的rsp变量给置一。(因为rsp变量在chnl_trans里声明为bit变量,因此默认为0,又因为没有声明为rand,因此不能进行随机化,所以正常情况下rsp这个变量的值是不变的,为0,直到被改变)并且被放到了rsp_mb信箱中。
好了,让我们把视角再切回到generator这一边。
task send_trans();
……
$display(req.sprint());//发送前打印一下
this.req_mb.put(req);
this.rsp_mb.get(rsp);
$display(rsp.sprint());//发送后再打印一下,可以通过rsp的不同
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
我们从generator的rsp_mb信箱中get刚刚initiator放进来的rsp对象,查看里面的rsp变量。如果rsp为1,说明发送正常,如果失败,rsp就还没被修改,还是0,就会报错。
B、对chnl_pkg2里的chnl_generator的解析
1、chnl_generator中给pkt_id、ch_id等赋值-1是什么意思?
通过给do_config()给gen随机化时将-1改为其他非负数,从而触发条件,使req例化后的内容和chnl_generator保持一致。
拿ch_id来说明。ch_id的初始值是-1,因此在任务send_trans里,不满足ch_id >=0的条件,所以,ch_id = = local::ch_id不会执行。
而当外部声明chnl_generator时将ch_id设置为一个非负数,那么 ch_id = = local::ch_id就会执行。也就是说,如果chnl_generator的ch_id为非负数,就会使req在随机化时的值和chnl_generator保持一致。
class chnl_generator;
rand int pkt_id = -1;
rand int ch_id = -1;
rand int data_nidles = -1;
rand int pkt_nidles = -1;
rand int data_size = -1;
rand int ntrans = 10;
……
constraint cstr{
soft ch_id == -1;
soft pkt_id == -1;
soft data_size == -1;
soft data_nidles == -1;
soft pkt_nidles == -1;
soft ntrans == 10;
}
……
// generate transaction and put into local mailbox
task send_trans();
chnl_trans req, rsp;
req = new();
assert(req.randomize with {local::ch_id >= 0 -> ch_id == local::ch_id;
local::pkt_id >= 0 -> pkt_id == local::pkt_id;
local::data_nidles >= 0 -> data_nidles == local::data_nidles;
local::pkt_nidles >= 0 -> pkt_nidles == local::pkt_nidles;
local::data_size >0 -> data.size() == local::data_size;
})
else $fatal("[RNDFAIL] channel packet randomization failure!");
……
endtask
……
endclass: chnl_generator
2、send_trans中为什么要在xxx == local::xxx之前加上条件local::xxx >= 0?
要想知道local::xxx >= 0条件的作用,就像路桑说的,做减法,去掉它看结果。我把代码改成下面的情况
assert(req.randomize with {ch_id == local::ch_id;
pkt_id == local::pkt_id;
data_nidles == local::data_nidles;
pkt_nidles == local::pkt_nidles;
data.size() == local::data_size;
})
else $fatal("[RNDFAIL] channel packet randomization failure!");
跑仿真,发现pkt_id是从-1开始的,代入data[i] == 'hC000_0000 + (this.ch_id<<24) + (this.pkt_id<<8) + i;之后,就出现了以bf开头的数据了~
所以说,加上条件后,可以通过给do_config()给gen随机化时将-1改为其他非负数,使得条件可以被触发,使req例化后的内容和chnl_generator保持一致。
3、send_trans任务中的local::ch_id和ch_id属于谁?如果写成this.ch_id呢?
task send_trans();
chnl_trans req, rsp;
req = new();
assert(req.randomize with {local::ch_id >= 0 -> ch_id == local::ch_id;
local::pkt_id >= 0 -> pkt_id == local::pkt_id;
local::data_nidles >= 0 -> data_nidles == local::data_nidles;
local::pkt_nidles >= 0 -> pkt_nidles == local::pkt_nidles;
local::data_size >0 -> data.size() == local::data_size;
})
else $fatal("[RNDFAIL] channel packet randomization failure!");
this.pkt_id++;
$display(req.sprint());
this.req_mb.put(req);
this.rsp_mb.get(rsp);
$display(rsp.sprint());
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
endtask
注意
- local::ch_id指类chnl_generator的变量,而ch_id指当前对象req的变量。此处如果是this.ch_id,也指当前对象req的变量。所以local::ch_id和this.ch_id是不同的。
- 这里的randomize是一个嵌套,通过类chnl_generator的变量来影响产生的req的变量。
- 注意,local::只在randomize里出现,其他地方没有。
4、generator里的ch_id、pkt_id在哪里赋值?
有同学发现在do_config()里只配置了ntrans、data_nidles、pkt_nidles、data_size,而没有ch_id、pkt_id的信息,好奇这两个变量在哪里赋的值。
① 问这个问题的同学你们问的没毛病,原来的代码里缺少了对chnl_id的更新,所以跑仿真起来如下图:
就是是通道1、2的也是打印成0了。
所以,我们要在chnl_basic_test 里的do_config加上对应的ch_id的值。
class chnl_basic_test extends chnl_root_test;
function new(string name = "chnl_basic_test");
super.new(name);
endfunction
virtual function void do_config();
super.do_config();
assert(gen[0].randomize() with {ch_id==0; ntrans==100; data_nidles==0; pkt_nidles==1; data_size==8;})
else $fatal("[RNDFAIL] gen[0] randomization failure!");
// USER TODO 2.2
// To randomize gen[1] with
// ntrans==50, data_nidles inside [1:2], pkt_nidles inside [3:5],
// data_size == 6
assert(gen[1].randomize() with {ch_id==1; ntrans==50; data_nidles inside {[1:2]}; pkt_nidles inside {[3:5]}; data_size==6;})
else $fatal("[RNDFAIL] gen[1] randomization failure!");
// USER TODO 2.3
// ntrans==80, data_nidles inside [0:1], pkt_nidles inside [1:2],
// data_size == 32
assert(gen[2].randomize() with {ch_id==2; ntrans==80; data_nidles inside {[0:1]}; pkt_nidles inside {[1:2]}; data_size==32;})
else $fatal("[RNDFAIL] gen[2] randomization failure!");
endfunction
endclass: chnl_basic_test
改完之后就正常了,如下图。
ps.感谢评论区里老哥的提醒。我下次还是得先跑下仿真再来回答问题哈,不能想当然。
② 而pkt_id的赋值是在chnl_generator中的send_trans()进行。是通过pkt_id++的方式进行赋值的。
首先,pkt_id的默认值为-1,之后,每randomize 一个pkt后,就会加一,表示接下来随机化的是下一个pkt的数据了。而加1之后,pkt_id>=0的条件就被满足了,因此就可以触发语句local::pkt_id >= 0 -> pkt_id == local::pkt_id,将generator的pkt_id值赋给req。
task send_trans();
chnl_trans req, rsp;
req = new();
assert(req.randomize with {local::ch_id >= 0 -> ch_id == local::ch_id;
local::pkt_id >= 0 -> pkt_id == local::pkt_id;
local::data_nidles >= 0 -> data_nidles == local::data_nidles;
local::pkt_nidles >= 0 -> pkt_nidles == local::pkt_nidles;
local::data_size >0 -> data.size() == local::data_size;
})
else $fatal("[RNDFAIL] channel packet randomization failure!");
this.pkt_id++;
$display(req.sprint());
this.req_mb.put(req);
this.rsp_mb.get(rsp);
$display(rsp.sprint());
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
endtask
C、chnl_root_test的task run()中gen可以放在agents前吗?
在下面代码中,可以把包含agents的fork…join_none的进程和包含gen的fork…join进程对调吗?也就是先让gen产生数据,再让agents里的init发送数据?
virtual task run();
$display($sformatf("*****************%s started********************", this.name));
this.do_config();
fork
agents[0].run();
agents[1].run();
agents[2].run();
mcdt_mon.run();
chker.run();
join_none
// run first the callback thread to conditionally disable gen_threads
fork
this.gen_stop_callback();
@(this.gen_stop_e) disable gen_threads;
join_none
fork : gen_threads
gen[0].run();
gen[1].run();
gen[2].run();
join
run_stop_callback(); // wait until run stop control task finished
endtask
答案是不行。如果把fork…join放前面,那么程序就会一直停在gen的run这里。因为chnl_generator类的run函数包含下面两句
this.req_mb.put(req);
this.rsp_mb.get(rsp);
gen将数据放入信箱之后就会进入阻塞状态,等待和init进行握手。只有从init处得到了rsp之后才会继续往下执行。
所以包含init的agents组件必须要比gen先运行起来。
那如果还是先让gen运行起来,只不过把fork…join换成了fork…join_none行吗?这样好像就不会阻塞到initiator的运行了?
virtual task run();
$display($sformatf("*****************%s started********************", this.name));
this.do_config();
fork : gen_threads
gen[0].run();
gen[1].run();
gen[2].run();
join_none
fork
agents[0].run();
agents[1].run();
agents[2].run();
mcdt_mon.run();
chker.run();
join_none
fork
this.gen_stop_callback();
@(this.gen_stop_e) disable gen_threads;
join_none
run_stop_callback(); // wait until run stop control task finished
endtask
这里得分情况讨论。
如果是chnl_basic_test和chnl_burst_test,那么是可以的。原因如下:
三个fork…join_none,每一个都不会被阻塞,所以run的任务直接就跳到了run_stop_callback();
此处的run_stop_callback()是root_test里的任务。
跳到了run_stop_callback()后,就会等待每个gen运行后将钥匙run_stop_flags放回来,那3个gen都运行完了之后,就会归还3把钥匙,那么run_stop_callback就会结束。整个仿真也能顺利结束。
下面是仿真正常结束时的打印消息。
但如果是chnl_fifo_full_test,那么就不行。
这里出现了三个fork…join_none,每一个都不会被阻塞,所以run的任务直接就跳到了run_stop_callback();而此处的run_stop_callback()和root_test里同名的任务是不一样的。
在这个任务里,很容易就会满足ch_margin == 'h20的条件然后就退出了。最后退出的时候也只是产生了3个obj_id而已。
所以也是不行的。
D、chnl_fifo_full_test
1、run_stop_callback()的作用
用来停止运行。
1个gen运行完之后就会放回1把钥匙;3个运行完就3把,那么run_stop_callback就会结束,整个test就会结束。
class chnl_generator;
……
task run();
repeat(ntrans) send_trans();
run_stop_flags.put(); //1个gen运行完之后就会放回1把钥匙;3个运行完就3把,
endtask
……
endclass
virtual task run_stop_callback();
$display("run_stop_callback enterred");
// by default, run would be finished once generators raised 'finish'
// flags
$display("%s: wait for all generators have generated and tranferred transcations", this.name);
run_stop_flags.get(3);//得到3把钥匙后,run_stop_callback就会结束,整个test就会结束
$display($sformatf("*****************%s finished********************", this.name));
$finish();
endtask
2、gen_stop_callback()的作用
用来控制关闭generator。
注意到chnl_root_test里的gen_stop_callback()是空的,所以它的run会自动跳过
fork
this.gen_stop_callback();
@(this.gen_stop_e) disable gen_threads;
join_none
而在chnl_fifo_full_test里的gen_stop_callback()内容如下:
virtual task gen_stop_callback();
bit[2:0] chnl_ready_flags; //bit位不能写为bit[3]
$display("gen_stop_callback enterred");
@(posedge agents[0].vif.rstn);
forever begin
@(posedge agents[0].vif.clk);
chnl_ready_flags = this.get_chnl_ready_flags();
if($countones(chnl_ready_flags) <= 1) break;
end
$display("%s: stop 3 generators running", this.name);
-> this.gen_stop_e; //所有gen都停下
endtask
$countones()用来数1的个数(chnl_ready_flags有3位) ,而实际上$countones(chnl_ready_flags) <= 1是一定成立的,而且只能为1,因为3个ready信号不能同时拉低,所以满足if条件,break跳出forever。紧接着触发gen_stop_e,关掉了gen_threads线程。
而这也会产生新的问题。因为之前是通过generator运行完之后放回1把钥匙,集齐3把之后让run_stop_callback结束,整个test就会结束。而generator被关掉之后,这种仿真退出方式也就不能用了,因此需要在chnl_fifo_full_test里重写一下run_stop_callback来控制仿真的退出。
virtual task run_stop_callback();
$display("run_stop_callback enterred");
// since generators have been forced to stop, and run_stop_flag would
// not be raised by each generator, so no need to wait for the
// run_stop_flags any more
$display("%s: waiting DUT transfering all of data", this.name);
fork
wait(agents[0].vif.ch_margin == 'h20);
wait(agents[1].vif.ch_margin == 'h20);
wait(agents[2].vif.ch_margin == 'h20);
join
$display("%s: 3 channel fifos have transferred all data", this.name);
$display($sformatf("*****************%s finished********************", this.name));
$finish();
endtask
这里采用的方式是等所有的数据都发送完毕之后就退出仿真。
注意:这种方式只能在fifo_full_test里有,不能直接写在root_test里。因为如果有的test发送数据非常慢,也能在某一时刻满足ch_margin == 'h20的条件,这样就会退出。比如只给一个channel发数据,每发一个数据停一拍,那么随时有可能在某时刻三个ch_margin 都为’h20,因为数据发送得太慢了
其他可能会有疑惑的小点:
1、下面这一段代码中为什么ch_id是左移24位?为什么赋值用的是“==”而不是‘=’?
constraint cstr{
data.size inside {[4:8]};
foreach(data[i]) data[i] == 'hC000_0000 + (this.ch_id << 24) + (this.pkt_id << 8) + i;
soft ch_id == 0;
soft pkt_id == 0;
data_nidles inside {[0:2]};
pkt_nidles inside{[1:10]};
};
因为前面的’hC000_0000是十六进制,每个0都对应着二进制的4位,因此ch_id需要移动24位才能表示C0、C1、C2;
而用“==”而不用‘=’是在约束块的定义约定的。
在绿皮书6.4.3节 等效表达式 写了下面这段话:
因为在约束块里只能包含表达式,所以在约束块里不能进行赋值。相反,应该用关系运算符为随机变量赋一个固定的值,例如len == 42。也可以在多个随机变量之间使用复杂的关系表达式,例如len==header.addr_mode*4 +payload.size().
2、定义函数function时为什么要加上virtual?
- 用于OOP思想的继承使用。当定义了virtual时,在子类中调用某task/function时,会先查找在子类中是否定义了该task/function,如果子类没有定义,则在父类中查找。未定义virtual时,只在子类中查找,没有定义就是编译器报错。
- 如果某一class会被继承,则用户定义的task/function(除new(),randomized(),per_randomize(),pose_randomize()外),都应该加上virtual关键字,以备后续扩展。
原文链接:https://blog.youkuaiyun.com/immeatea_aun/article/details/89216857
3、$sformatf()的作用,和 $sformat()的区别?
这俩函数时用来整理格式的,区别主要是用法不同,可以看下面的代码。
string a;
string a_r;
$sformat(a , "a==%0d" , 1);
$display("1.%0s", a);
a_r=$sformatf("a_r==%0d" , 1);
$display("2.%0s", a_r);
打印的结果分别是
a == 1
a_r == 1
$sformat()会将整理好的数返回给第一个参数a,而 $sformatf()相比 $sformat()少了第一个参数,整个函数会返回整理好之后的字符串。
利用这一点可以方便地把它和display结合起来,写成
$display($sformatf("a_r == %0d",1));
为啥display也可以整理,还需要sformat这样的函数来帮忙整理呢?
因为有时候就是需要你提前先整理好字符串的。具体可以参考下面链接
参考:https://zhuanlan.zhihu.com/p/279618353
参考:
MCDF实验——Lab3(https://blog.youkuaiyun.com/qq_39794062/article/details/113781683)
路科讲义