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

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

日志

SV的环境构建篇之四:程序和模块

热度 1已有 3995 次阅读| 2016-12-4 22:07 |个人分类:验证系统思想|系统分类:芯片设计

module(模块)作为SV从Verilog继承过来的概念,也自然保持了它的特点,除了作为RTL模型的外壳包装和实现硬件行为,在更高层的集成上面,模块之间也需要通信和同步。从硬件实现的角度来看,Verilog通过always,initial过程语句块、信号数据连接来实现进程间通信。为此,我们可以将不同的module作为独立的程序块,他们之间的同步通过信号的变化(event触发)或者通过等待特定事件(时钟周期)或者时间(固定延时)来完成
如果按照软件的思维理解硬件仿真,那么上面的三个模块首先是独立运行的线程(thread)在仿真伊始并行执行,除了每个线程会依照自身内部产生的事件触发进程模块之外,也同时依靠相邻模块间的信号变化。

Verilog设计竞争问题

为了避免在RTL仿真行为中发生的信号竞争问题,我们建议通过非阻塞赋值或者特定的信号延迟来解决同步的问题。例如,从下面的例子可以看到,采用阻塞赋值可能引起的竞争问题:

module design_race;
logic clk;
logic rstn;
logic [3:0] a;
logic [3:0] b;

... //省略了时钟和复位产生代码

always @(posedge clk or negedge rstn) begin
if(rstn == 0) begin
a = 0;
b = 0;
end
else begin
a = a + 1;
b = a;
$display("@%0t a=%0d, b=%0d", $time, a, b);
end
end
endmodule

从仿真结果来看,寄存器a在每个时钟计数以后的值会被当前时钟内的b采集到,所以,每个时钟周期内a和b的数值是一致的。但是,这种仿真行为并不符合真实的硬件行为,因为b如果要对a的数值采样,那么必须有一个周期的延迟,而从软件角度理解,b不应该在当前时刻收集a变化后的数值,而应该是变化前的数值。
# @5 a=x, b=x
# @15 a=x, b=x
# @35 a=1, b=1
# @45 a=2, b=2
# @55 a=3, b=3
# @65 a=4, b=4
# @75 a=5, b=5
# @85 a=6, b=6
# @95 a=7, b=7

所以,我们可以通过非阻塞赋值的形式来避免这种情况。

always @(posedge clk or negedge rstn) begin
if(rstn == 0) begin
a <= 0;
b <= 0;
end
else begin
a <= a + 1;
b <= a;
$display("@%0t a=%0d, b=%0d", $time, a, b);
end
end

输出结果:
# @5 a=x, b=x
# @15 a=x, b=x
# @35 a=0, b=0
# @45 a=1, b=0
# @55 a=2, b=1
# @65 a=3, b=2
# @75 a=4, b=3
# @85 a=5, b=4
# @95 a=6, b=5

非阻塞赋值和信号输出的延迟赋值都可以有效避免设计层面的竞争问题。那么当testbench与DUT连接时,这种方式还是否行之有效呢?在讨论之前,我们首先从SV的仿真调度入手。

SV的仿真调度
SV的仿真调度完全支持Verilog的仿真调度,同时又扩展出来支持新的SV的结构体例如program(程序)和断言(assertion)。充分理解SV的不同结构体在仿真中执行的先后顺序,有利于理解testbench中对DUT的驱动和采样的顺序,避免不合理的驱动采样方式。

由于仿真器一般基于event驱动方式来执行,所以如果对各个类型的event发生做出合理安排就可以保证设计与验证环境之间有清晰的发生顺序,进而避免这两者之间的竞争问题
这里time-slot(时间片)是仿真时间中的一个抽象单位Ts,在该单位内所有的线程(always,initial,assertion等)和数据对象的赋值(阻塞赋值和非阻塞赋值)会被赋予相应的优先级,依次被执行。这种优先级即上图显示的scheduling regions(调度区域)。除了observed和reactive区域,其它区域均是继承于Verilog调度区域。preponed区域是从上一个Ts进入本时间单位的入口,而postponed区域则是所有event触发完毕所有数据也被赋值结束以后该Ts的出口。

从上述不同调度区域的功能来看,可以将其划分为:
接下来我们对这些调度区域做简单的介绍。

active 区域:在从preponed region进入active region之后,所有处于该调度阶段的线程(例如always、assign、initial等)将会执行。其中,跟非阻塞赋值有关的操作执行完毕后,对应的线程会进入NBA,而带有"#0"延时操作的线程会直接进入inactive区域。

inactive 区域:所有被进行零延时操作的线程会在inactive区域被激活,同时在被执行之前迁往active区域。所以,零延时的操作会延缓线程的执行时间。

NBA 区域:该区域是在所有的active区域和inactive区域均没有其它线程之后所到达的调度区,到达了该区域之后,之前在active区域的非阻塞赋值会生效,而这些非阻塞如果触发了别的线程,那么这些被触发的线程又要被迁移到active区域。

observed 区域:当之前active/inactive/NBA区域均全部执行完毕之后,也即表示了设计部分的线程执行完毕,接下来的区域是SV为验证一侧准备的。在进入到observed区域之后,这一区域是为了属性断言(property assertion)准备的,由于属性中需要监测设计中的变量,且必须等到所有每一个数据对象被赋予最终的数值,所以该区域处在了设计区域之后。这样做的好处可以避免采集到不稳定的变量从而导致错误的属性检查。同时,该区域也同样适用于interface和program中的采样操作,使得采集到的数据是处于该Ts的最终值。

reactive 区域:在经历了数据采样之后,断言语句需要进行属性判断,而同时,该区域如果再次对设计区域中的线网和变量赋值则又会使得被激活的线程被再次迁移到active区域。经历了信号采样之后,处于testbench区域中的线程也会被在该区域执行。

postpone 区域:在分别经历了跟设计与testbench有关的区域之后,当前Ts进入了postponed区域。该区域内的值保持稳定,且应同下一个Ts中preponed的值一致。同时,该区域也作为SV PLI/DPI的回调函数(callback)点,使得在SV外部的调用语言例如C在使用SV变量时仍然可以使用到最新的数值,无论是设计部分还是验证部分。

在对上面各区域做了介绍以后,我们结合着之前阻塞赋值和非阻塞赋值的例子进行分析。在上面阻塞赋值时:
...
a = a + 1; // a在Ts active区域被赋值,且赋值立即生效
b = a; // b在同一个Ts区域被赋值,同时使用被立即赋值后的a(a = a+1)
...

再来看看非阻塞赋值的仿真调度安排
...
a = a + 1; // a在Ts active区域被赋值,而赋值在NBA区域生效
b = a; // b在同一个Ts区域被赋值,且使用被赋值前的a值
...

所以非阻塞赋值可以用来避免一些设计中的竞争情况,而这种方式也针对于组合和同步时序逻辑的设计场景。但这种设计技巧在验证领域中仍然受到了不少的挑战:
  • 验证人员在testbench实现中更多地采用软件编程方式,即连续性赋值(continous assignment)而不是阻塞/非阻塞赋值的形式
  • 验证人员首先并不关心设计行为中可能出现的竞争场景,对他们而言首要的是采集到正确的稳定数值
  • SV中的属性/断言需要在一个特定的仿真调度区域采集数据和执行属性检查

对此,我们可以从下面这个的两个例子观察提到的testbench部分数据采样和执行的部分:

module数据采样示例1
module counter(input clk);
bit [3:0] cnt;

always @(posedge clk) begin
cnt <= cnt + 1;
$display("@%0t DUT cnt = %0d", $time, cnt);
end
endmodule

module tb1;
bit clk1;
bit [3:0] cnt;

initial begin
forever #5ns clk1 <= !clk1;
end

counter dut(clk1);

always @(posedge clk1) begin
$display("@%0t TB cnt = %0d", $time, dut.cnt);
end
endmodule
仿真结果:
# @5 DUT cnt = 0
# @5 TB cnt = 0
# @15 DUT cnt = 1
# @15 TB cnt = 1
# @25 DUT cnt = 2
# @25 TB cnt = 2
# @35 DUT cnt = 3
# @35 TB cnt = 3
# @45 DUT cnt = 4
# @45 TB cnt = 4

可以看到DUT与TB的采样均发生在clk1的上升沿,并且均采样到了dut.cnt变化前的数值。用仿真调度时序图来表达,则如下图所示。

DUT和TB对dut.cnt的采样均在active区域发生,所以都采样到了dut.cnt变化前的数值。
module数据采样示例2
module tb2;
bit clk1;
bit clk2;
bit [3:0] cnt;

initial begin
forever #5ns clk1 <= !clk1;
end

always @(clk1) begin
clk2 <= clk1;
end

counter dut(clk1);

always @(posedge clk2) begin
$display("@%0t TB cnt = %0d", $time, dut.cnt);
end
endmodule

在DUT和testbench的数据采样结果不一致:
# @5 DUT cnt = 0
# @5 TB cnt = 1
# @15 DUT cnt = 1
# @15 TB cnt = 2
# @25 DUT cnt = 2
# @25 TB cnt = 3
# @35 DUT cnt = 3
# @35 TB cnt = 4
# @45 DUT cnt = 4
# @45 TB cnt = 5

发生数据采集不一致的原因在于DUT和TB用于采样的时钟不同,即DUT使用了clk1,而TB使用了clk2。粗看起来,clk1与clk2没有延迟,但因为非阻塞赋值使得clk2较clk1有从active区到NBA区的延迟,简单而言,clk2的沿变化要比clk1晚,由此带来的变化造成了数据采样的竞争问题,用时序图描述可以表达为下图的形式:
由于dut与tb使用的时钟存在一个active到NBA的延迟周期,这使得testbench在使用clk2对dut进行采样时,已经采集到了该Ts中DUT在第一个NBA已经生效的非阻塞赋值。

通过上面的两个采样示例可以看到,TB中的数据采样如果在module内部执行会有可能造成不同的采样结果。在这里我们需要强调的不是是否应该采样的当前Ts中dut.cnt变化前或者变化后的数值,而是应该保证的是,采样的结果是按照预期执行的。也就是说,如果通过一些方法可以使得采样的数据按照预期发生在dut.cnt变化前或者变化后,都是可以接受的。

我们之前已经介绍了可以通过SV的property中的sequence采样特性、interface采样以及program采样三种方法。在这一节,我们先介绍program的采样方式。通过对上面的例子进行简单的改造,我们可以使得program内部发生的采样是预期的结果。

program数据采样示例:

program dsample(input clk);

initial begin
forever begin
@(posedge clk);
$display("@%0t TB cnt = %0d", $time, dut.cnt);
end
end
endprogram

module tb;
bit clk1;
bit [3:0] cnt;

initial begin
forever #5ns clk1 <= !clk1;
end

counter dut(clk1);
dsample spl(clk1);
endmodule


仿真结果:
# @5 DUT cnt = 0
# @5 TB cnt = 1
# @15 DUT cnt = 1
# @15 TB cnt = 2
# @25 DUT cnt = 2
# @25 TB cnt = 3
# @35 DUT cnt = 3
# @35 TB cnt = 4
# @45 DUT cnt = 4
# @45 TB cnt = 5

可以看到仿真结果同“module数据采样示例2”保持一致,而且通过program内部进行数据采样的结果是可以预期的。我们再通过仿真进度时序图来理解这种采样方式:
从通用角度来解释program内部执行的情况,上面的示例可以遵循下面的进度安排原则:
  • 在program执行之前,会先进行设计代码相关的仿真调度区域即active、inactive和NBA
  • 待设计调度区域执行完后,会通过observed区域,最后至reactive区域。而program会在reactive区域执行。所以program会采用之前已经被阻塞/非阻塞赋值后的稳定值进行计算。
  • 在program执行过程中,如果有内部变量发生变化且又影响到该Ts中设计调度区相关的变量,则对应设计的调度区会被再次迁移到active区域,而该program会被挂起,直到整个调度阶段再次进入reactive区域。

由此看来,SV介绍program的一个重要部分就是为了将设计和验证的调度区域通过显式的方式来安排,例如设计部分被建议放置在module中,而测试采样部分被建议放置在program中。下面是一些关于program实现的要求和建议:
  • 读者可以将program看做是软件的“领地”,所以program中不可以出现和硬件行为相关的过程语句和实例,例如always、module、interface,也不应该出现其它program例化语句。
  • 为了使得program进行类软件方式的顺序执行方式,可以在program内部定义变量,以及发起多个initial块
  • program内部定义的变量赋值的方式应该采用阻塞赋值(软件方式)。
  • program内部在驱动外部的硬件信号时应该使用非阻塞赋值(硬件方式)。
  • program中的initial块(类软件的执行方式)会在reactive区域被执行,而program之外的initial块(module内部)则会在active区域被执行,这一点值得注意。

所以SV通过program可以将DUT与TB的领地做清晰的划分,根本上从调度区域的不同执行顺序来解决的。在下一篇的《SV的组件实现篇》中我们会介绍如何通过interface clocking来给出第二种解决时序采样和驱动信号的方式

在清楚了硬件信号采样可能出现的竞争问题以及如何通过program来解决之后,我们便可以通过合适的连接和采样方式将验证组件和DUT连接,而连接之后一旦有了激励,如何结束仿真,结束仿真的方式有哪些,我们将在下一节的《测试的始终》为大家介绍。

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


1

点赞

刚表态过的朋友 (1 人)

评论 (0 个评论)

facelist

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

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

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 253

    粉丝
  • 25

    好友
  • 33

    获赞
  • 45

    评论
  • 访问数
关闭

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

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

GMT+8, 2024-4-19 08:46 , Processed in 0.018942 second(s), 12 queries , Gzip On, Redis On.

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