接下来我们对这些调度区域做简单的介绍。
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连接,而连接之后一旦有了激励,如何结束仿真,结束仿真的方式有哪些,我们将在下一节的《测试的始终》为大家介绍。
谢谢你对路科验证的关注,也欢迎你分享和转发真正的技术价值,你的支持是我们保持前行的动力。