会逐步将遇到的一些问题或者从视频中学到的点弄懂,发上来。持续更新
一、接口的使用
要求1.1.channel_initiator发送的数据例如valid和data与时钟clk均在同一个变化沿,没有任何延迟。这种0延迟的数据发送不利于波形的查看,要在之前的代码基础上使用intf.ck的方式来做数据驱动,并且再观察波形,查看驱动的数据与时钟上升沿的延迟。
时钟块代码:
clocking drv_ck @(posedge clk);
default input #1ns output #1ns;
output ch_data, ch_valid;
input ch_ready, ch_margin;
endclocking
要查看利用时钟块进行驱动的波形,那么我们要选中其中一个接口的实例,添加波形之后运行。
可以看出,采用了时钟块的之后,data在clk上升沿之后的1ns才被驱动,与时钟块代码符合。如果没有选择时钟块里的信号,那么data会在clk上升沿时变化,看起来就不那么友好。
要求1.2.为了更好地控制相邻数据之间的空闲间隔,引入了一个变量idle_cycles,它表示相邻有效数据之间的间隔。之前的代码会使得有效数据之间保持固定的一个空闲周期,需要使用idle_cycles来灵活控制有效数据之间的空闲周期。
引入变量idle_cycles:
function automatic void set_idle_cycles(int n);
idle_cycles = n;
endfunction
通过repeat控制空闲周期数:
repeat(idle_cycles) chnl_idle();
二、仿真的结束
实验要求
1.实现burst_test()方法,使得每个chnl_initiator的idle_cycles设置为0,同时发送500个数据,最后结束测试。
2.实现fifo_full_test()方法,使得无论采取什么数值的idle_cycles,也无论发送多少个数据,只要各个chnl_initiator的不停发送使得对应的channel缓存变为满标志(ready拉低),那么可以在三个channel都拉低ready时(不必要同时拉低,可以先后拉低即可),便可以立即结束测试。
task automatic fifo_full_test();
chnl0_gen.initialize(0);
chnl0_gen.initialize(1);
chnl0_gen.initialize(2);
chnl0_init.set_name("chnl0_init");
chnl1_init.set_name("chnl1_init");
chnl2_init.set_name("chnl2_init");
chnl0_init.set_idle_cycles(0);
chnl1_init.set_idle_cycles(0);
chnl2_init.set_idle_cycles(0);
$display("fifo_full_test initialized components");
// send data
fork: fork_all_run
forever chnl0_init.chnl_write(chnl0_gen.get_data());//不停发送数据,直到写满
forever chnl1_init.chnl_write(chnl1_gen.get_data());
forever chnl2_init.chnl_write(chnl2_gen.get_data());
join_none
$display("fifo_full_test: 3 initiators running now");
$display("fifo_full_test: waiting 3 channel fifos to be full");
fork
wait(chnl0_init.intf.ch_margin == 0);
wait(chnl1_init.intf.ch_margin == 0);
wait(chnl2_init.intf.ch_margin == 0);
join
$display("fifo_full_test: 3 channel fifos have reached full");
$display("fifo_full_test: stop 3 initiators running");
disable fork_all_run; //FIFO都满了,就停止发送
$display("fifo_full_test: set and ensure all agents' initiator are idle state");
fork
chnl0_init.chnl_idle();//让initiator处于空闲状态
chnl1_init.chnl_idle();
chnl2_init.chnl_idle();
join
$display("fifo_full_test: waiting DUT transferring all of data");
fork
wait(chnl0_init.intf.ch_margin == 'h20);//等待FIFO里的数据被送走
wait(chnl1_init.intf.ch_margin == 'h20);
wait(chnl2_init.intf.ch_margin == 'h20);
join
$display("fifo_full_test: 3channel fifos have transferred all data");
$display("fifo_full_test finished testing DUT");
endtask
备注:
通过设置断点的方式(当然也可以直接看transcript窗口的打印信息),可以很方便地看出三个test运行的时间段。分清了之后,看波形就会清楚很多。当然,可能具体的数值会有点不一样,所以需要结合波形来确定边界。
三、类的例化和类的成员
此部分会将之前的module中的数据和内容都移植到class里。tb2和tb3的区别如下:
区别1
module可以声明端口,将接口的实例intf直接传递进去。而class没有端口,不能直接像module一样这么写,而是将接口作为class的成员变量写出,并且在之后通过set_interface将指针传递到组件里。并且class里的idle_cycles初始化是放在new里面,而module没有new。
module chnl_initiator(chnl_intf intf);
string name;
int idle_cycles = 1;
……
endmodule
class chnl_initiator;
local string name;
local int idle_cycles;
virtual chnl_intf intf;
function new(string name = "chnl_initiator");
this.name = name;
this.idle_cycles = 1;
endfunction
……
endclass
区别2
module里默认生命周期是static,要声明为动态的就需要写明是automatic,而class里默认就是automatic,所以函数和任务都不用注明;
区别3
module里不能封装(local、protected)、而class可以。
区别4
module里的chnl_write发送的数据是单一变量data,而class发送的是chnl_trans,数据类型更为丰富。
class chnl_trans;
int data;
int id;
int num;
endclass
chnl_trans也可换成结构体,但是放在class里更好,方便以后添加函数或任务来对数据进行处理,比如复制、克隆等操作。
//module
task automatic chnl_write(input logic[31:0] data);
@(posedge intf.clk);
intf.drv_ck.ch_valid <= 1;
intf.drv_ck.ch_data <= data;
wait(intf.ch_ready === 'b1);
$display("%t channel initiator [%s] sent data %x", $time, name, data);
repeat(idle_cycles) chnl_idle();
endtask
//class
task chnl_write(input chnl_trans t);
@(posedge intf.clk);
intf.drv_ck.ch_valid <= 1;
intf.drv_ck.ch_data <= t.data;
@(negedge intf.clk);
wait(intf.ch_ready === 'b1);
$display("%t channel initiator [%s] sent data %x", $time, name, t.data);
repeat(this.idle_cycles) chnl_idle();
endtask
区别5
组件需要在module里例化(不用在initial里例化),而在class里则是需要在initial块里实例化
//module
chnl_intf chnl0_if(.*);
chnl_intf chnl1_if(.*);
chnl_intf chnl2_if(.*);
chnl_initiator chnl0_init(chnl0_if);
chnl_initiator chnl1_init(chnl1_if);
chnl_initiator chnl2_init(chnl2_if);
chnl_generator chnl0_gen();
chnl_generator chnl1_gen();
chnl_generator chnl2_gen();
//class
initial begin
chnl0_init = new("chnl0_init");
chnl1_init = new("chnl1_init");
chnl2_init = new("chnl2_init");
chnl0_gen = new(0);
chnl1_gen = new(1);
chnl2_gen = new(2);
……
end
要求3.1
将chnl_initiator和chnl_generator从module改造为class,同时定义一个用来封装发送数据的类chnl_trans。要求3.1要求在initial块中分别例化3个已经声明过的chnl_initiator和chnl_generator。
chnl0_init = new("chnl0_init");
chnl1_init = new("chnl1_init");
chnl2_init = new("chnl2_init");
chnl0_gen = new(0);
chnl1_gen = new(1);
chnl2_gen = new(2);
需要注意new里面的参数有所不同,可根据chnl_initiator和chnl_generator里的new函数确定。
要求3.2
每一个chnl_initiator都需要使用接口chnl_intf来发送数据,在发送数据之前我们需要确保chnl_initiator中的接口不是悬空的,即需要由外部被传递。所以接下来的实验要求需要通过调用chnl_initiator里的方法来完成接口传递。
要求3.3
调用三个test任务进行测试。
要求3.4
观察chnl_generator在例化chnl_trans t时,有没有不恰当的地方?如果有,会有什么问题?
class chnl_generator;
chnl_trans trans[$];
int num;
int id;
chnl_trans t;
function new(int n);
this.id = n;
this.num = 0;
//t = new();位置1
endfunction
function chnl_trans get_trans();
t = new();//位置2
t.data = 'h00C0_0000 + (this.id<<16) + this.num;
t.id = this.id;
t.num = this.num;
this.num++;
this.trans.push_back(t);//将对象的句柄t放到存放句柄的数组里
return t;
endfunction
endclass
实例化有点问题。
如果放在new函数里(位置1),那么chnl_generator就只会在 chnl0_init = new(“chnl0_init”);实例化1次,产生1个对象;
但实际上我们是想要产生多个实例的,因此需要把它放在get_trans()里(位置2),这样通过下面的代码就可以生成多个实例。
repeat(100) chnl0_init.chnl_write(chnl0_gen.get_trans());
四、包的定义和类的继承
tb4和tb3的区别
1.tb4多了agent结构,并且把chnl_generator和chnl_initiator的实例化放在了agent里的new函数进行
//tb4
class chnl_agent;
chnl_generator gen;
chnl_initiator init;
local int ntrans;
local virtual chnl_intf vif;
function new(string name = "chnl_agent", int id = 0, int ntrans = 1);
this.gen = new(id);
this.init = new(name);
this.ntrans = ntrans;
endfunction
……
endclass: chnl_agent
// tb3
initial begin
#10 rstn <= 0;
repeat(10) @(posedge clk);
rstn <= 1;
end
chnl_intf chnl0_if(.*);
chnl_intf chnl1_if(.*);
chnl_intf chnl2_if(.*);
chnl_initiator chnl0_init;
chnl_initiator chnl1_init;
chnl_initiator chnl2_init;
chnl_generator chnl0_gen;
chnl_generator chnl1_gen;
chnl_generator chnl2_gen;
initial begin
chnl0_init = new("chnl0_init");
chnl1_init = new("chnl1_init");
chnl2_init = new("chnl2_init");
chnl0_gen = new(0);
chnl1_gen = new(1);
chnl2_gen = new(2);
chnl0_init.set_interface(chnl0_if);
chnl1_init.set_interface(chnl1_if);
chnl2_init.set_interface(chnl2_if);
$display("*****************all of tests have been finished********************");
basic_test();
burst_test();
fifo_full_test();
$finish();
end
要求1
从chnl_pkg中引入其中定义的类,让tb4能够识别类的句柄。
import chnl_pkg::*
要求2
实现chnl_burst_test和chnl_fifo_full_test
test的父类:
class chnl_root_test;
chnl_agent agent[3];
protected string name;
function new(int ntrans = 100, string name = "chnl_root_test");
foreach(agent[i]) begin
this.agent[i] = new($sformatf("chnl_agent%0d",i), i, ntrans);
end
this.name = name;
$display("%s instantiate objects", this.name);
endfunction
task run();
$display("%s started testing DUT", this.name);
fork
agent[0].run();
agent[1].run();
agent[2].run();
join
fork
wait(agent[0].vif.ch_margin == 'h20);
wait(agent[1].vif.ch_margin == 'h20);
wait(agent[2].vif.ch_margin == 'h20);
join
$display("%s: 3 channel fifos have transferred all data",this.name);
$display("%s finished testing DUT",this.name);
endtask
function void set_interface(virtual chnl_intf ch0_vif, virtual chnl_intf ch1_vif, virtual chnl_intf ch2_vif);
agent[0].set_interface(ch0_vif);
agent[1].set_interface(ch1_vif);
agent[2].set_interface(ch2_vif);
endfunction
endclass
class chnl_basic_test extends chnl_root_test;
function new(int ntrans = 200, string name = "chnl_basic_test");
super.new(ntrans, name);
foreach(agent[i]) begin
this.agent[i].init.set_idle_cycles($urandom_range(1, 3));//包括1-3
end
$display("%s configured objects", this.name);
endfunction
endclass: chnl_basic_test
class chnl_burst_test extends chnl_root_test;
//USER TODO
function new(int ntrans = 500, string name = "chnl_burst_test");
super.new(ntrans, name);
foreach(agent[i]) begin
this.agent[i].init.set_idle_cycles(0);
end
$display("%s configured objects",this.name);
endfunction
endclass: chnl_burst_test
chnl_burst_test和chnl_basic_test 基本一致,只是改一下idle_cycles的大小。
ps:我发现把idle_cycles设为$ urandom_range(1, 3)时,仿真了几次我看到的基本都是idle_cycles=3;后来我修改成$ urandom_range(1, 2),结果看到的基本都是idle_cycles=1了。可能这就是仿真器的特点哈
chnl_fifo_full_test单独拿出来说,因为有点不同。chnl_burst_test和chnl_basic_test可以直接继承chnl_root_test的所有变量和方法/任务,其中就包括task run。但是chnl_fifo_full_test 的run和chnl_root_test不同,因此采用同名的办法进行覆盖重写
class chnl_fifo_full_test extends chnl_root_test;
// USER TODO
function new(int ntrans = 500, string name = "chnl_fifo_full_test");
super.new(ntrans,name);
foreach(agent[i]) begin
this.agent[i].init.set_idle_cycles(0);
end
$display("%s configured objects",this.name);
endfunction
task run();
$display("fifo_full_test started testing DUT");
fork: fork_all_run
agent[0].run();
agent[1].run();
agent[2].run();
join_none
$display("fifo_full_test: 3 initiators running now");
$display("fifo_full_test: waiting 3 channel fifos to be full");
fork
wait(agent[0].vif.ch_margin == 0);
wait(agent[1].vif.ch_margin == 0);
wait(agent[2].vif.ch_margin == 0);
join
$display("fifo_full_test: 3 channel fifos have reached full");
$display("fifo_full_test: stop 3 initiators running");
disable fork_all_run;
$display("fifo_full_test: set and ensure all agents' initiator are idle state");
fork
agent[0].init.chnl_idle();
agent[1].init.chnl_idle();
agent[2].init.chnl_idle();
join
$display("fifo_full_test: waiting DUT to transfer all data");
fork
wait(agent[0].vif.ch_margin == 'h20);
wait(agent[1].vif.ch_margin == 'h20);
wait(agent[2].vif.ch_margin == 'h20);
join
$display("%s: 3 channel fifos have transferred all data",this.name);
$display("%s finished testing DUT",this.name);
endtask
function void set_interface(virtual chnl_intf ch0_vif, virtual chnl_intf ch1_vif, virtual chnl_intf ch2_vif);
agent[0].set_interface(ch0_vif);
agent[1].set_interface(ch1_vif);
agent[2].set_interface(ch2_vif);
endfunction
endclass: chnl_fifo_full_test
一个有意思的地方:
大家可以看到在chnl_fifo_full_test中,将余量为’h20的fifo写满用了’h2f + 1个数(算上0这个数),那么这多出来的’h0f + 1其实就是被DUT读走的数,所以可以推测出写和读的速率比为’h2f + 1:'h0f + 1 =48:16=3:1
要求3
例化已经声明过的三个test组件。(要注意,声明是声明,例化是例化,不能混淆。声明完之后,还需要在initial块里例化)
basic_test = new();
burst_test = new();
fifo_full_test = new();
要求4
完成从test一层的接口传递任务,使得其内部各个组件都可以得到需要的接口
错误写法:
basic_test.set_interface(ch0_vif,ch1_vif,ch2_vif);
burst_test.set_interface(ch0_vif,ch1_vif,ch2_vif);
fifo_full_test.set_interface(ch0_vif,ch1_vif,ch2_vif);
错误原因:
ch0_vif是class chnl_root_test的内部变量,而我们要在test层传递接口,就得用最外层的实例化接口chnl_if(chnl0_if,chnl1_if,chnl2_if)。
chnl_intf chnl0_if(.*);
chnl_intf chnl1_if(.*);
chnl_intf chnl2_if(.*);
正确写法:
basic_test.set_interface(chnl0_if,chnl1_if,chnl2_if);
burst_test.set_interface(chnl0_if,chnl1_if,chnl2_if);
fifo_full_test.set_interface(chnl0_if,chnl1_if,chnl2_if);
要求5
调用各个test的方法,展开测试。
其他部分笔记:
1.这一段代码有啥用?
fork
wait(chnl0_init.intf.ch_margin == 'h20);
wait(chnl1_init.intf.ch_margin == 'h20);
wait(chnl2_init.intf.ch_margin == 'h20);
join
有的小伙伴不知道为啥要在每个test的最后都来上这一段。为了解释这个问题,我把在burst_test里的这一段给去掉了。之后正常进行仿真。
你会在transcript窗口看到这样的情况。也就是fifo_full_test只是发送了一个数据,然后整个test就匆匆结束了。这是为啥咧?
其实就是因为:上面提到的那段代码相当于是把FIFO里的数据给清空。如果我们把这段代码从burst_test里删去,那么可以从下图看出,退出burst_test后,余量仅为1了,那么当fifo_full_test发往FIFO里送了一个数据之后,就FIFO也就满了,就退出这个测试了。这样的话这个测试就显得有点鸡肋。所以说,退出一个test前,最好是把FIFO给清空,等待余量为’h20再退出。
2.
待补充
task chnl_write(input chnl_trans t);
@(posedge intf.clk);
// USER TODO 1.1
// Please use the clocking drv_ck of chnl_intf to drive data
intf.drv_ck.ch_valid <= 1;
intf.drv_ck.ch_data <= t.data;
@(negedge intf.clk); //避免采样时的竞争问题
wait(intf.ch_ready === 'b1);
$display("%t channel initiator [%s] sent data %x", $time, name, t.data);
repeat(this.idle_cycles) chnl_idle();
endtask
@(negedge intf.clk)的作用:避免采样时的竞争问题。
- 仿真的时候会存在时钟和信号的时序竞争问题。
- 默认情况下,时钟对组合逻辑添加一个无限小的时间延迟,称为delta cycle。delta cycle比最小时间精度还要小,是一个相对的概念。
- 注意,valid和data是时序逻辑,而ready是组合逻辑,因此存在一个delta cycle。
- valid、data和ready在同一个delta cycle,就是在上升沿处。如果不加@(negedge intf.clk),本来ready是打算变为1的,结果因为delta cycle的存在使得ready采样采到了0,也就是采错了。
- 解决方法:加延迟。比如像代码里在clk下降沿再采,其实就是添加了一个延迟。另一个方法是直接添加延迟#1 ps,但不推荐,一般建议不加固定的延时,因为不利于维护和看代码。(在clocking块里就要直接添加固定的延迟)
为什么valid、data用了时钟块,而ready却没用?
- valid、data是时序逻辑部分,而ready是组合逻辑(因为ready信号要立即给valid和data反馈,接受还是不接受,而不是等到下一拍,因此是ready采用组合逻辑)
- 时钟块是为了模拟时序逻辑里的建立保持时间,只在时序逻辑中出现,组合逻辑不用。
- 要观测实时的信号就不用clocking
参考:
MCDF实验——Lab2