前言: 在实验1,通过A产生动态数组并赋值给B,B再传给c,完成了激励的发送。总体来说很容易上手。而在实验2产生数据的方式、赋值的方式变为了抽象的概念,对于初学者可能不是很好理解,但你看完这篇文章,不会都难。
种一棵树最好的时间是十年前,其次是现在。不是吗?
实验2需要用到接口、类、包这几个主要知识,大家可以先去学习哦!在实验1使用到的大多是module,在实验2把这些模块修改为了类,并使用包来包装class。
结构框图如下:
与上次的框图相比主要有这几点变化:
-
接口:将之前传输数据的几条线改为了一t条总线ch_if。
-
实验1产生数据发送数据在一个模块中,实验2将这两部分抽象的分开。
将产生什么样的数据放在了generator这个类中;将怎么按照时序传输放在了initiator这个类中;将传输数据的数量放在在了chnl_root_test这个类中。 -
类:将之前定义传输数据方法的模块chnl_init,修改为了类initiator。
-
包: 将generator和initiator装在一个包中。
下面是实验2的代码,将实验代码分成了几个部分,每一步都做了详细的讲解,想学的话有点耐心,看完包会。
源代码
1. interface
第一个部分是接口interface。将传输数据时用到的时序集中放到interface中,并使用时钟块clocking驱动。
`timescale 1ns/1ps
interface chnl_intf(input clk, input rstn);
logic [31:0] ch_data;
logic ch_valid;
logic ch_ready;
logic [ 5:0] ch_margin;
clocking drv_ck @(posedge clk);
default input #1ns output #1ns;
output ch_data, ch_valid;
input ch_ready, ch_margin;
endclocking
endinterface
2. 定义包
将仿真需要测试全部放到包中,将之前使用的模块修改为了类。其中包括chnl_trans、chnl_initiator、chnl_generator、chnl_agent、chnl_root_test、chnl_burst_test、chnl_basic_test、chnl_fifo_full_test。
package chnl_pkg;
endpackage
2.1 chnl_trans
可以理解为一个空白的类,先往下看。
class chnl_trans;
int data;
int id;
int num;
endclass: chnl_trans
2.2 chnl_initiator
在chnl_initiator中定义了function :new、set_idle_cycles、set_name,task:set_interface、chnl_idle。
class chnl_initiator;
local string name;//名字
local int idle_cycles;//循环时间
local virtual chnl_intf intf;//与接口相接
2.21 function new
当调用initiator中的new函数时,修改name。initiator.new (“无敌”),此时名字为无敌。
function new(string name = "chnl_initiator"); // 将上边的变量初始化
this.name = name;
this.idle_cycles = 1;
endfunction
2.22 function set_idle_cycles
当调用 set_idle_cycles,可以将idle_cycles改为设定的值。例如initiator.set_idle_cycles (1),此时idle_cycles为1。
function void set_idle_cycles(int n);//可以设定时间的函数
this.idle_cycles = n;
endfunction
2.23 function set_name
set_name可以修改name的function。
function void set_name(string s);//可以设定名字的函数
this.name = s;
endfunction
2.24 function set_interface
set_interface 用来实现initiator与interface的连接。
function void set_interface(virtual chnl_intf intf); //将interface做了例化,当成功例化,initior的intf会和接口相接。
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
2.25 task chnl_write
chnl_write是发送数据的任务。
- chnl_write会把chnl_trans中的data发送出去。
- 每次发送数据时需要考虑时序,将这些时序放在任务chnl_write中,这样每次发送数据只需要写入数据即可。
- 发送数据完成后,进入循环repeat。其中循环次数为上文中的idle_cycles,循环的内容为另一个task chnl_idle。循环为了产生几次空拍,也可以不要空拍。
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
2.26 task chnl_idle
chnl_idle中将valid和data都置为0,意思是此时不传输数据,也就是空拍。repeat(this.idle_cycles) chnl_idle(); = repeat(循环次数)空拍,这句话用来循环几次空拍。
task chnl_idle();
@(posedge intf.clk);
// USER TODO 1.1
// Please use the clocking drv_ck of chnl_intf to drive data
intf.drv_ck.ch_valid <= 0;
intf.drv_ck.ch_data <= 0;
endtask
2.3 chnl_generator
定义了function new、get_trans。
在generator中用到了之前说的空白的类,声明了句柄trans并定义为队列,这个队列用来干嘛呢?先不管它。
class chnl_generator;
chnl_trans trans[$]; //例化chnl_trans
int num;
int id;
2.31 function new
调用new()给igeneration 中的变量id赋值 ,并初始化num。
function new(int n);
this.id = n;
this.num = 0;
endfunction
2.32 function get_trans
get_trans是在对象中产生数据,并把句柄传到上边产生的队列trans[$]中。
- 首先第二行是声明句柄t,并例化开辟动态空间。t.data = 'h00C0_0000 + (this.id<<16) +this.num;是将产生的数据传入动态空间。
- t.id = this.id;将generation的id 传入动态空间。
- t.num = this.num;将generation中的num传入动态空间。最后返回值为句柄t。
动态空间也就是对象。
function chnl_trans get_trans();
chnl_trans t = new();//声明句柄t并例化
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: chnl_generator
2.4 chnl_agent
定义了function new、set_ntrans、set_interface和task run。
并将initiator,generator例化在了类agent里。
class chnl_agent;
chnl_generator gen;
chnl_initiator init;//将initiator,generator例化在类agent里
local int ntrans;
virtual chnl_intf vif;
2.41 function new
通过agent new 把int id、string name、int ntrans传给 gen的new函数、init的new函数和ntrans。
function new(string name = "chnl_agent", int id = 0, int ntrans = 1);
this.gen = new(id);
this.init = new(name);
this.ntrans = ntrans;
endfunction
2.42 function set_ntrans
通过set_ntrans来改变ntrans的值。
function void set_ntrans(int n);
this.ntrans = n;
endfunction
2.43 function set_interface
调用set_interface,可以实现agent 连接interface,并通过调用initiator中的set_interface,让initiator也连接interface。
function void set_interface(virtual chnl_intf vif);
this.vif = vif;
init.set_interface(vif);
endfunction
2.44 task run
把task的行为用中文写出来大概就是:
repeat(发送多少次数据)发送数据的task chnl_write(产生的数据的task get_trans)。调用init.产生空拍的任务。
- 在get_trans这个任务中,把数据通过句柄t放入到了动态空间里,返回值为句柄t。
- task chnl_write这个任务中输入就是get_trans的返回值t,并将t.data发送出去。
- 最后调用产生数据空拍的任务。
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
2.5 chnl_root_test
在chnl_root_test中,定义了function new、set_interface,task run。
在agent这个类中已经将initiator和generator包在了一起,需要用到3个agent。
class chnl_root_test;
chnl_agent agent[3];//3个agent
protected string name;
2.51 function new
root_test中的new函数可以定义ntrans,name。并通过freach遍历agen[0]、agen[1]、agen[2],同时调用了agent中的new函数。
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
简单代入i=0,
this.agent[0] = new($sformatf("chnl_agent%0d",0), 0, 100);
$sformatf(“chnl_agent%0d”,0)的意思等同于(“chnl_agent0”),有人可能会想那直接写后边的(“chnl_agent0”)不就行了?为啥还要弄变量i?弄这么复杂?
如果写(“chnl_agent0”)那肯定还需要写(“chnl_agent1”)(“chnl_agent2”),需要三行, $sformatf(“chnl_agent%0d”,i)引入了变量只需要写一行,100个agent也是一行搞定。
this.agent[0] = new("chnl_agent0", 0, 100);
agent的new函数调用了gen和init的new函数。
this.gen = new(0);
this.init = new("chnl_agent0");
this.ntrans = 100;
在gen的new函数中会把0调用,此时gen中的id为0。function get_trans中的id也会使用传入来的id 即0。
function new(0);
this.id = 0;
this.num = 0;
endfunction
在init的new函数中name变为了"chnl_agent0",name在init中主要用在打印上。下面代码是init.new。
function new( "chnl_agent0"); // 将上边的变量初始化
this.name = "chnl_agent0";
this.idle_cycles = 1;
endfunction
2.52 task run
task run 实现了数据的传输,当三个fifo传输完毕数据后,退出task。
task run();//运行三个agent
$display("%s started testing DUT", this.name);
fork
agent[0].run();
agent[1].run();
agent[2].run();
join
$display("%s waiting DUT transfering all of data", this.name);
fork//等三个fifo传输完毕,退出
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
task run调用了agent run,三个agent分别调用各自的run。 ntrans在上边new函数中将值赋给了各个agent。下面代码是agent.run。
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
task run中的第一个fork join是等三个agent都运行各自的run后才会退出这个fork join执行下一步。
第二个fork join想要表达的是等三个ch_margin都满足过 == 'h20,也就是数据传输完毕,才会退出这个fork join执行下一步。对于为何ch_margin == 'h20代表的是传输完毕呢?其实这个东西在fifo的设计中可能明确的说明了。
值得主要的点是fork join 、wait的用法,而不是张三为啥叫张三,1为啥是1,ch_margin == 'h20为啥数据传输完毕?
2.53 function set_interface
root_test调用了agent中的set_interface。
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
agent中的set_interface,实现了agent与interface的连接,并且调用了init.set_interface。
function void set_interface(virtual chnl_intf vif);
this.vif = vif;
init.set_interface(vif);
endfunction
init.set_interface实现了initiator与interface的连接。
function void set_interface(virtual chnl_intf intf); //将interface做了例化,当成功例化,initior的intf会和接口相接。
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
上述连接interface的过程可以类比为root_test给三个agent规定了暗号,agent把暗号传给了他的小弟initiator。
发送数据的过程,initiator中的组织A可以让小兵在合适的时间进攻,组织B可以让小兵休息一定时间;generator中的组织专门产生小兵的地方;agent把generator中的小兵送到了initator的组织中,同时规定了冲锋多少次,每次冲锋按照组织A规定的时间冲锋,并且冲锋后按照组织B计划的休息时间时间。
通透了吧???你应该通透了。
下文basic_test、burst_test、 fifo_full_test都是root_test的子类。
你可能疑问直接调用root_test不行吗?为啥还要弄这么多子类?
可以这样理解:root_test是一个非常全面的模板,写文章时可以直接套用模板,也可以选择复制模板到新world中修改。直接使用模板,这次文章写完,当下次需要修改一些地方的时候,分不清楚哪里是模板哪里是你写的。每次复制粘贴写文章,改动哪里一目了然。而子类就是"复制粘贴"。
2.6 子类chnl_basic_test
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));//冲锋后的休息次数
end
$display("%s configured objects", this.name);
endfunction
endclass: chnl_basic_test
需要注意的是,使用super.new()来调用父类的new(),在这个test中随机了set_idle_cycles,也就是idle_cycles的重复次数。
2.7 子类chnl_burst_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
2.8 chnl_fifo_full_test
class chnl_fifo_full_test extends chnl_root_test;
function new(int ntrans = 1_000_000, 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("%s started testing DUT", this.name);
fork: fork_all_run
agent[0].run();
agent[1].run();
agent[2].run();
join_none
$display("%s: 3 agents running now", this.name);
$display("%s: waiting 3 channel fifos to be full", this.name);
fork
wait(agent[0].vif.ch_margin == 0);
wait(agent[1].vif.ch_margin == 0);
wait(agent[2].vif.ch_margin == 0);
join
$display("%s: 3 channel fifos have reached full", this.name);
$display("%s: stop 3 agents running", this.name);
disable fork_all_run;
$display("%s: set and ensure all agents' initiator are idle state", this.name);
fork
agent[0].init.chnl_idle();
agent[1].init.chnl_idle();
agent[2].init.chnl_idle();
join
$display("%s waiting DUT transfering all of data", this.name);
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
endclass: chnl_fifo_full_test
fifo_full_test与root_test之间的区别主要在下面这段代码。
- 使用fork join_none,运行三个agent。
- 发送给fifo的数据写满后(fifo容量满),再disable停止三个agent的运行。
- 之后三个agent调用了init中的空拍,等待数据传输完毕,退出task run。
fork: fork_all_run
agent[0].run();
agent[1].run();
agent[2].run();
join_none
$display("%s: 3 agents running now", this.name);
$display("%s: waiting 3 channel fifos to be full", this.name);
fork
wait(agent[0].vif.ch_margin == 0);
wait(agent[1].vif.ch_margin == 0);
wait(agent[2].vif.ch_margin == 0);
join
$display("%s: 3 channel fifos have reached full", this.name);
$display("%s: stop 3 agents running", this.name);
disable fork_all_run;
fork
agent[0].init.chnl_idle();
agent[1].init.chnl_idle();
agent[2].init.chnl_idle();
join
2.9 tb文件
现在的tb文件对比之前实验的tb文件,结构一目了然,方法、数据全部存放在了对应的类中,tb文件中只有例化和运行。
module tb4_ref;
logic clk;
logic rstn;
logic [31:0] mcdt_data;
logic mcdt_val;
logic [ 1:0] mcdt_id;
//例化模块
mcdt dut(
.clk_i (clk )
,.rstn_i (rstn )
,.ch0_data_i (chnl0_if.ch_data )
,.ch0_valid_i (chnl0_if.ch_valid )
,.ch0_ready_o (chnl0_if.ch_ready )
,.ch0_margin_o(chnl0_if.ch_margin )
,.ch1_data_i (chnl1_if.ch_data )
,.ch1_valid_i (chnl1_if.ch_valid )
,.ch1_ready_o (chnl1_if.ch_ready )
,.ch1_margin_o(chnl1_if.ch_margin )
,.ch2_data_i (chnl2_if.ch_data )
,.ch2_valid_i (chnl2_if.ch_valid )
,.ch2_ready_o (chnl2_if.ch_ready )
,.ch2_margin_o(chnl2_if.ch_margin )
,.mcdt_data_o (mcdt_data )
,.mcdt_val_o (mcdt_val )
,.mcdt_id_o (mcdt_id )
);
// clock
initial begin
clk <= 0;
forever begin
#5 clk <= !clk;
end
end
// reset
initial begin
#10 rstn <= 0;
repeat(10) @(posedge clk);
rstn <= 1;
end
// import类
import chnl_pkg::*;
//例化接口
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;
initial begin
basic_test = new();//句柄的例化
burst_test = new();
fifo_full_test = new();
// 将暗号一级一级的传递
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);
// run
basic_test.run();
burst_test.run();
fifo_full_test.run();
$display("*****************all of tests have been finished********************");
$finish();
end
endmodule
tb文件结构是不是很清晰?
结语
看完代码,对整个实验过程应该通透了吧?
对于实验2,要学会接口、类、类中句柄、类中的指针this
fork join的使用。
关于实验代码、验证相关的资料,你找的到的找不到的都可以从公众号"袁小丑"免费获取。