路科验证的个人空间 https://blog.eetop.cn/1561828 [收藏] [复制] [分享] [RSS]

空间首页 动态 记录 日志 相册 主题 分享 留言板 个人资料

日志

SV组件实现篇之四:监测器的采样

已有 4705 次阅读| 2016-12-25 23:47 |个人分类:验证系统思想|系统分类:硬件设计

当verifier梅在实现slave channel验证环境的时候,她也绘制了一幅slave channel验证结构图:
verifier梅借鉴了verifier董在验证模块registers时的方法,为DUT slave channel创建了2个stimulator,即上行发送数据的initiator和下行接收数据的responder。在实现了这两个组件之后,由initiator发送的激励,经过slave的数据通路可以最终送出至responder。在stimulator实现之后,就需要将必要的信号采集下来,以用作日后的数据比较。

就之前的介绍,slave的initiator和responder应该采取各自的interface,即ini_if和rsp_if,而从不同接口上可以分别采集到送入slave的数据和从slave送出的数据。因此,我们倾向于也分别实现两个内部结构类似的monitor,即ini_monitor和rsp_monitor(别怕麻烦,将monitor按照接口和功能分离为两个的优势在后面的验证集成中会显现)。待monitor采集完数据之后,我们也准备将其进一步发送至checker。

对于monitor的功能而言,它的核心部分就是从interface做数据采样(sampling)和打包(packaging)送给checker。我们在之前的《SV的环境构建篇之四:程序和模块》中已经提到了,设计部分(硬件)和验证部分(软件)之间可能存在竞争的现象,除了program可以消除竞争之外,我们也可以通过interface的clocking块来消除竞争。所以,我们先在这里引入interface clocking块的介绍,再利用clocking来实现monitor的数据采样功能。

interface clocking介绍
在之前《SV的环境构建篇之三:接口》中,可以看到硬件和软件世界的连接可以通过灵活的interface来连接,也可以通过modport来进一步限定信号传输的方向,避免端口连接的错误。同时,我们也可以在接口中声明clocking(时序块)来通过显式地指出时钟信号,用其来对信号做信号的同步和采样。clocking块基于时钟周期来对信号在进行驱动或者采样的方式,使得testbench不需要再关注于如何准确及时地对信号传输或者采样,也消除了信号竞争的问题。通过clocking,testbench可以利用其进行:
  • 事件的同步
  • 输入的采样
  • 输出的驱动

我们来看看,一个典型的clocking定义:

clocking bus @(posedge clock1);
default input #10ns output #2ns;
input data, ready, enable = top.mem1.enable;
output negedge ack;
input #1step addr;
endclocking

在上面这个例子中,第一行定义了一个clocking块bus,由clock1的上升沿来驱动。第二行指出了在clocking块中所有的信号,默认情况下会在clocking事件(clock1上升沿)的前10ns来对其进行输入采样,在其事件的后2ns对其进行输出驱动。下一行是声明了要对其采样的三个输入信号,data,ready和指向top.mem1.enable的enable信号,这三个信号作为输入,它们的采样时间即采用了默认输入事件(clock1上升沿前的10ns)。第四行则声明了要驱动的ack信号,而驱动该信号的时间则是时钟clock1的下降沿,即覆盖了原有的默认输出事件(clock1上升沿后的2ns)。接下来的addr,也采用了自身定义的采样事件,即clock1上升沿前的1step。这里的1step会使得采样发生在clock1上升沿的上一个time slot的postponed区域,即可以保证采样到的数据是上一个时钟周期的数据。

从上面这个例子可以看到,关于定义clocking块需要注意的几个地方
  • clocking块不但可以定义在interface中,也可以定义在module和program中。
  • clocking中列举的信号不是自己定义的,而是应该由interface或者其它声明clocking的模块定义的。
  • clocking在声明完名字之后,应该伴随着定义默认的采样事件,即“default input/output event”。如果没有定义,则会采用默认的在clocking采样事件前的1step对输入进行采样,在采样事件后的#0对输入进行驱动。
  • 除了定义默认的采样和驱动事件,也可以在其后定义信号方向时,用新的采样事件对默认采样和驱动事件做覆盖。

这里,我们再深入一些,讨论关于所谓定义采样和驱动事件的规定,例如,上面例子中第二行:
default input #10ns output #2ns;
相对的采样事件是如何定义的。
从这张关于clocking事件的说明图能够得知,输入信号的采样,会在关于时钟事件(clock1上升沿)前的10ns做采样,所以,规定的“input clocking_skew”中clocking skew(时钟偏移量)采取的是负值,即相对clocking事件的前10ns;而输出驱动则采用的是正值,会在clocking事件后的2ns时刻做输出驱动。

所以,从上面的图中可以发现,这种在时钟事件前后的采样或者驱动方式可以有效地避免竞争的情况,因为可以通过在不同的time slot中做信号的采样或驱动。例如input #1step,可以使得在上一个时钟postponed区域做时钟采样,而#1ps,利用一个很小的输出延迟,使得输出可以在clocking事件(时钟变化沿)后的time slot内发生变化。这样就使得在clocking块中罗列的信号事件不会造成竞争的情况。

利用clocking事件同步
关于clocking功能的第一特性,使用起来同一般@或者wait事件方式没有明显差别,只是需要注意添加信号所定义的clocking块名称。为了使得读者对于包括clocking事件同步在内的clocking三个功能特性有更好的体会,我们拿出下面这个例子来看看:

module clocking1;
bit vld;
bit grt;
bit clk;

clocking ck @(posedge clk);
default input #3ns output #3ns;
input vld;
output grt;
endclocking

initial forever #5ns clk <= !clk;

initial begin: drv_vld
$display("$%0t vld initial value is %d", $time, vld);
#3ns vld = 1; $display("$%0t vld is assigned %d", $time, vld);
#10ns vld = 0; $display("$%0t vld is assigned %d", $time, vld);
#8ns vld = 1; $display("$%0t vld is assigned %d", $time, vld);
end

initial forever
@ck $display("$%0t vld is sampled as %d at sampling time $%0t", $time, vld, $time);
initial forever
@ck $display("$%0t ck.vld is sampled as %d at sampling time $%0t", $time, ck.vld, $time-3);
endmodule

无论是上面利用@ck(即@(posedge clk))还是@ck.vld,可以肯定的一点是,这些事件发生的时刻都是在clk的上升沿,因为无论是clocking块本身,还是通过ck采样的vld或者驱动的grant,都是基于时钟上升沿的。所以,我们仍然可以通过使用@或者wait等耗时的操作符来基于信号变化的事件进行同步。

利用clocking采样数据
依然还是上面的clocking1的例子,其中虽然在clocking1::drv_vld中并没有在clk时钟沿驱动vld,但采样时,仍然还是基于时钟沿和采样偏移量进行基于固定采样时刻的数据采样;如果没有利用clocking块进行采样,那么采样只会基于时钟沿(没有叠加采样偏移量)进行。

从上面的例子输出结果可以看到:
# $0 vld initial value is 0
# $3 vld is assigned 1
# $5 ck.vld is sampled as 0 at sampling time $2
# $5 vld is sampled as 1 at sampling time $5
# $13 vld is assigned 0
# $15 vld is sampled as 0 at sampling time $15
# $15 ck.vld is sampled as 1 at sampling time $12
# $21 vld is assigned 1
# $25 ck.vld is sampled as 1 at sampling time $22
# $25 vld is sampled as 1 at sampling time $25

vld的驱动和采样,也可以从波形图实例中有更直观的体现:
从波形图上也可以看到,如果只是基于clk上升沿,那么采样的结果同基于clk上升沿叠加采样偏移量的结果是不相同的。如果,我们将采样偏移量抽象为物理中的建立时间(setup time),则其要求更明确为时钟上升沿之前的某一段时间内,数据不能有变化,否则数据采样会有不一样的结果;如果只是基于时钟上升沿采样,那么上图中的“采样vld”则表示了采样出的结果。而无论是“采样ck.vld”还是“采样vld”,值得变化均发生在了时钟上升沿,只是背后的采样点不同罢了。

利用clocking产生激励
同样地,如果采样clocking块的驱动,也可以实现一种类似于“物理保持时间”的驱动方式,实现了驱动时钟沿叠加驱动偏移量的延迟效果。例如下面的例子:

module clocking2;
bit vld;
bit grt;
bit clk;

clocking ck @(posedge clk);
default input #3ns output #3ns;
input vld;
output grt;
endclocking

initial forever #5ns clk <= !clk;

initial begin: drv_grt
$display("$%0t grt initial value is %d", $time, grt);
@ck ck.grt <= 1; $display("$%0t grt is assigned 1", $time);
@ck ck.grt <= 0; $display("$%0t grt is assigned 0", $time);
@ck ck.grt <= 1; $display("$%0t grt is assigned 1", $time);
end

initial forever
@grt $display("$%0t grt is driven as %d", $time, grt);
endmodule

上面的例子中,对于ck块中的grt赋值,都是通过clocking块ck.grt来引用进而赋值的。所以,虽然赋值的时间点都是在clk上升沿触发的,而真正grt值发生变化的时刻都是在clk上升沿叠加驱动偏移量的时间点。关于这一点,可以从下面的输出结果以及驱动时序波形图中印证。

输出结果:
# $0 grt initial value is 0
# $5 grt is assigned 1
# $8 grt is driven as 1
# $15 grt is assigned 0
# $18 grt is driven as 0
# $25 grt is assigned 1
# $28 grt is driven as 1
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块做数据采样并且打包写入缓存的方式接介绍完了。目前,我们暂时可以将数据缓存至接口内的缓存,而在我们下一节学习了《组件间的通信》之后,我们就可以用进程间通信的三种类型来做更灵活的尝试了。


谢谢你对路科验证的关注,也欢迎你分享和转发真正的技术价值,你的支持是我们保持前行的动力。


点赞

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册

  • 关注TA
  • 加好友
  • 联系TA
  • 0

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 253

    粉丝
  • 25

    好友
  • 33

    获赞
  • 45

    评论
  • 访问数
关闭

站长推荐 上一条 /1 下一条

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-5-9 13:21 , Processed in 0.016206 second(s), 12 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
返回顶部