从波形图上也可以看到,如果只是基于clk上升沿,那么采样的结果同基于clk上升沿叠加采样偏移量的结果是不相同的。如果,我们将采样偏移量抽象为物理中的建立时间(setup time),则其要求更明确为时钟上升沿之前的某一段时间内,数据不能有变化,否则数据采样会有不一样的结果;如果只是基于时钟上升沿采样,那么上图中的“采样vld”则表示了采样出的结果。而无论是“采样ck.vld”还是“采样vld”,值得变化均发生在了时钟上升沿,只是背后的采样点不同罢了。
上面的例子中,对于ck块中的grt赋值,都是通过clocking块ck.grt来引用进而赋值的。所以,虽然赋值的时间点都是在clk上升沿触发的,而真正grt值发生变化的时刻都是在clk上升沿叠加驱动偏移量的时间点。关于这一点,可以从下面的输出结果以及驱动时序波形图中印证。
monitor的采样功能
在介绍完了clocking块的三种属性之后,verifier梅就准备利用这一特性来实现slave monitor的数据采样。由于slave的输入输出端数据协议简单,因此,我们准备将采集到的数据都存入到一个统一的数据格式中:
class slv_trans; // 数据包定义
logic [31:0] data;
endclass
接下来,我们引入作为连接stimulator与monitor的interface,slv_ini_if和slv_rsp_if:
interface slv_ini_if(
input rstn,
input clk
);
logic valid;
logic [31:0] data;
logic ready;
slv_trans mon_fifo[$]; // 数据存储FIFO
clocking ck_mon @(posedge clk); // 采样clocking
default input #1step output #1ps;
input valid;
input data;
input ready;
endclocking
function void put_trans(slv_trans t); // 写入FIFO
mon_fifo.push_back(t);
$display("slv_ini_if::mon_fifo size is %0d", mon_fifo.size());
endfunction
endinterface
interface slv_rsp_if(
input rstn,
input clk
);
logic req;
logic [31:0] data;
logic ack;
slv_trans mon_fifo[$];
clocking ck_mon @(posedge clk);
default input #1step output #1ps;
input req;
input data;
input ack;
endclocking
function void put_trans(slv_trans t);
mon_fifo.push_back(t);
$display("slv_rsp_if::mon_fifo size is %0d", mon_fifo.size());
endfunction
endinterface
从上面定义的两个接口来看,verifier梅利用了clocking块的采样特性,在其中分别声明了用来采样的clocking和信号列表。同时,它们各自提供了一个开放的方法和FIFO(用队列来实现),用来存储采集到的数据。
再来看看两个monitor,即slv_ini_mon和slv_rsp_mon是如何定义的:
class slv_ini_mon;
slv_trans trans;
virtual interface slv_ini_if vif;
task run(); // 运转方法
forever begin
mon_trans();
put_trans();
end
endtask
task mon_trans(); // 采集有效数据
forever begin
@(posedge vif.clk iff vif.rstn);
if(vif.ck_mon.valid === 1 && vif.ck_mon.ready === 1) begin
trans = new();
trans.data = vif.ck_mon.data;
break;
end
end
endtask
function void put_trans(); // 将数据写入接口中的FIFO
vif.put_trans(trans);
endfunction
endclass
class slv_rsp_mon;
slv_trans trans;
virtual interface slv_rsp_if vif;
task run();
forever begin
mon_trans();
put_trans();
end
endtask
task mon_trans();
forever begin
@(posedge vif.clk iff vif.rstn);
if(vif.ck_mon.req === 1 && vif.ck_mon.ack === 1) begin
trans = new();
trans.data = vif.ck_mon.data;
break;
end
end
endtask
function void put_trans();
vif.put_trans(trans);
endfunction
endclass
上述两个monitor提供的方法相同,均为:
- run:让monitor运转起来,保持时刻监视和采样数据。
- mon_trans:每次收集一个有效数据。
- put_trans:将有效数据通过虚接口vif存入接口中的FIFO。
再来看看两个monitor提供的成员变量,分别是自己对应的虚接口和一个只有声明,并没有在构建函数中例化的句柄trans。
这里的mon_trans在采集完有效的一次数据写入后,通过clocking块准确地将采样到的数据先先入到“临时创建”的对象中,该对象的句柄为trans,进而通过break退出方法。接下来,将trans的值(新创建对象的句柄)写入到接口的FIFO中,如此往复。
这里,读者需要注意的是,通过每次触发有效的数据写入事件,来创建新的对象trans = new(),进而将有效数据写入到trans指向的新对象中,再到随后通过vif.put_trans(trans)将每次句柄的值写入接口的FIFO中,这整个过程当中,两个monitor都在不断地创建对象,那么在每次更改trans值(从旧对象指向了新的对象),之前对象是否还存在呢?毕竟,读者可能疑惑,trans毕竟不再指向旧对象了。
答案是,之前创建的对象仍然存在,因为FIFO中每个元素仍然在引用之前创建的每一个对象,根据SV空间自动回收机制的用法,只有我们再销毁了FIFO即这个队列之后,那么之前创建的对象由于环境中再没有其它句柄的引用,这样所有之前创建的对象才会被回收。例如,我们可以通过队列的delete()删除整个队列,进而销毁其元素指向的对象。
从上面这个上意图来看,随着有效数据采样发生e1 -> e2 -> e3 -> e4 -> ...,每次都会创建新的对象,而且trans也会指向最想的对象,同时之前创建的所有对象的句柄都存入vif.mon_fifo中。注意,这里存入的是对象的句柄,而不是对象本身。以后,其它的组件例如checker,也可以通过接口内FIFO的句柄元素来索引到它们分别指向的对象。
上面的这个数据打包写入的方式,是将interface内定义的队列作为数据缓存中间站,而后,一旦checker创建好以后,也可以将interface的指针传递给checker,进而为checker提供get_trans()的方法。这样,checker即可以从各个interface中得到采集到的数据。
实际上,采样好的数据应该缓存到什么地方,选择有很多,除了可以存放到interface中,也可以考虑存放到monitor或者checker内部,而后通过monitor与checker之间的直接对话来实现。这也就是下一节我们会引出的,组件之间的主要对话方式,或者进程之间同步的方法。
最后,我们再来看看,上面定义的interface和monitor在顶层testbench的例化:
module slave_tb;
logic rstn;
logic clk;
logic ch_valid;
logic [31:0] ch_data;
logic ch_ready;
logic req;
logic [31:0] data;
logic ack;
slave dut(...); // DUT slave例化
// 接口例化
slv_ini_if ini_if(.rstn(rstn), .clk(clk));
slv_rsp_if rsp_if(.rstn(rstn), .clk(clk));
// monitor
slv_ini_mon ini_mon;
slv_rsp_mon rsp_mon;
// 接口连接
assign ini_if.valid = ch_valid;
assign ini_if.data = ch_data;
assign ini_if.ready = ch_ready;
assign rsp_if.req = req;
assign rsp_if.ack = ack;
assign rsp_if.data = data;
initial begin
// 创建monitor对象
ini_mon = new();
rsp_mon = new();
// 连接虚接口
ini_mon.vif = ini_if;
rsp_mon.vif = rsp_if;
// 令monitor对象开始工作
fork
ini_mon.run();
rsp_mon.run();
join
end
... // test场景创建和激励生成
endmodule
至此,monitor通过clocking块做数据采样并且打包写入缓存的方式接介绍完了。目前,我们暂时可以将数据缓存至接口内的缓存,而在我们下一节学习了《组件间的通信》之后,我们就可以用进程间通信的三种类型来做更灵活的尝试了。
谢谢你对路科验证的关注,也欢迎你分享和转发真正的技术价值,你的支持是我们保持前行的动力。