SV组件实现篇之二:激励器的驱动(下)
数据生命周期
在SV中,我们将数据的声明周期分为两类:
-
automatic(动态)
-
static(静态)
如果数据变量被声明为automatic,那么在进入该进程/方法之后,automatic变量会被创建,而在离开该进程/方法之后,automatic变量会被销毁。这同C语言的变量及其作用域的使用说明是一致的。而static变量在仿真开始时即会被创建,而在进程/方法执行过程中,自身不会被销毁,且可以被多个进程/方法所共享。
所以,对于automatic与static两种生命周期的数据类型,最直观的区别就是static在仿真过程中的任何时刻都可以被共享,且不会被销毁,直到仿真结束;而automatic变量则同软件的局部变量一样,在它的作用域生命结束时,它也会被销毁回收存储空间。
我们再来看下面这个例子:

输出结果:

上面的三个function被定义在了module内,分别被声明为了automatic、static和默认类型。
对于automatic方法,其内部的所有变量默认也是automatic,即伴随automatic方法的声明周期建立和销毁。对于static方法,其内部的所有变量默认也是static类型。对于automatic或者static方法,用户可以对其内部定义的变量做单个声明,使其类型被显式声明为automatic或者static。
对于static变量,用户在声明变量时应该同时对其做初始化,而初始化只会伴随它的生命周期发生一次,并不会随着方法调用被多次初始化。
而上面的的第三种方法def_cnt的默认类型是static,这是因为SV规定:
-
在module、program、interface、task和function之外声明的变量拥有静态的生命周期,即存在于整个仿真阶段,这同C定义的静态变量一样。
-
在module、interface和program内部声明,且在task、process或者function外部声明的变量也是static变量,且作用域在该块中。
-
在module、program和interface中定义的task、function默认都是static类型。
-
在过程块中(task、function、process)定义的变量均跟随它的作用域,即过程块的类型,如果过程块为static,则它们也默认为static,反之亦然。这些变量也可以由用户显式声明为automatic或者static。
-
为了使得在过程块中声明的变量有统一默认的生命周期,可以在定义module、interface、package或者program时,通过限定词automatic或者static来区分。对于上述程序块默认的生命周期类型为static。
例如下面的代码通过显式声明test,来改变其内部过程块task t的形式参数和内部变量类型为automatic。

通过接口驱动
在激励驱动的方法中我们可以看到,激励可以通过module内置的方法来生产,最终通过module端口输出给外部,而在更高层的TB中则依赖于interface来连接DUT与stimulator。这是一种常见的将stimualtor与DUT连接的方法,此外,我们也可以通过virtual interface(虚接口)在stimulator内部直接做采样或者驱动。
我们将之前stimulator stm_ini加以修改,使用virtual interface来实现激励的驱动:

在上面的代码示例中,与之前相比,可以看到下面几处不同点:
-
stm_ini module没有声明任何端口,即激励并不通过端口传递。
-
stm_ini 内部声明了virtual interface类型,即interface的“指针”。这一点很有趣,因为interface就内部构建和应用场景来看都贴近于硬件的“世界”,比如它内部可以声明过程语句块(always/initial),而也只有硬件部分才可以对interface做例化。同时,interface又可以被软件世界所引用,这里就依靠“指针”virtual interface。在声明时,stm_ini并不知道最终例化的regs_ini_if在何处,所以它先假定regs_ini_if在仿真开始时可以被传递到virtual interface vif。
-
信号驱动的方法例如op_wr直接引用vif,进而驱动regs_ini_if中的各个变量。
-
在TB中,需要在initial setif块中传递iniif给ini.vif,最终完成实体接口tb.iniif到虚接口ini.vif。只有完成了这一传递,才最终可以保证信号驱动任务在被调用之前,虚接口被赋值,可以寻址到实体接口。
-
在stm_ini的stmgen initial块中一开始先需要等待stm_ini.vif接口得到赋值传递,而不是null值。这可以保证在后期调用驱动方法时,不会因为tb.ini.vif悬空无法引用到tb.iniif中的变量而发生运行错误。当然,用户也可以使用“#1ps”给入固定的延时,保证tb.setif在tb.ini.stmgen执行之前完成。
测试向量产生
上面示例代码中,与之前通过stm_ini模块端口驱动的例子另外一个不同点是,这次在stm_ini中声明的trans数组不是固定数组,而是动态数组ts[],在其声明时并没有规定其容量大小。这么做的好处是使得数组的大小可以调整,并且将这一任务交给更高层的tb.arrini。在tb.arrini中,首先指定了tb.ini.ts的大小为new[3],其次对其中的单元做初始化赋值。
这里,动态数组的使用和外部初始化使得TB将stimulator的驱动功能和test vector(测试向量)生成的两个任务清晰地剥离开,这么做可以尽量保证stm_ini只完成驱动功能,有一定的封装性。而面对不同的测试场景,用户只需要关心如何生成测试向量。那么可以抽象地理解为,如果用户将每个test case(测试用例)包装为一个方法,用户就可以很方便地在TB中调用这些测试场景。
这种方法使得最终stimualtor、test vector和testbench可以很好的分开来。我们再来看看下面这个实例:


仿真结果:
-
sim +TEST=test_wr
# +TEST=test_wr is passed as a test vector
除了之前已经将stimulator stm_ini分开之外,这个例子也将tests和tb分开。在tests中将不同的test vector以task的形式来定义,在其内部初始化tb.ini.ts动态数组,这里通过绝对路径的引用方式可以在仿真时寻址到该数组。
而在tb.vecgen initial块中通过SV的系统函数$value$plusargs(user_string, variable)来得到仿真时外部传递的test vector指令。例如,如果仿真时传递参数+TEST=test_wr,则会通过tb.vecgen字符串比较,最终选择tb.tts.test_wr()来初始化生成测试向量;如果传递参数没有被识别,或者没有传递+TEST=参数,那么仿真都会利用系统函数$error()来报告错误。
通过上面这种方式,我们便可以将维护的力量主要投入到module tests中,而stm_ini和tb则有了较好的复用性。
仿真结束控制
在之前的例子中,可以发现,无论是stimulator、test vector还是tb都有结束仿真的权利,那么究竟让谁来结束仿真比较合适呢?我们倾向于让test vector来结束仿真,这是由于测试场景可以更具体地控制合适产生激励,也应该知道合适结束仿真。无路是仿真的开始、激励的生成,还是最终仿真的结束,都应该完整地属于测试向量的一部分。
这样就可以让整个测试场景完全交付于独立的测试向量,我们可以通过修改上面的代码,实现这一功能:

可以通过在tests中定义用来最终结束仿真的时间(或者其它事件),进而在不同的任务中赋予不同的数值。而在tb中,最终结束仿真的行为也会依赖于tb.tts.fin的数值。在这里,由于仿真默认的时间单位是1ns,所以我们没有再次声明`timescale,读者需要注意。如果时间单位需要是其它单位,则需要在module定义之外,声明`timescale。
通过这一节,我们已经能够掌握如何利用既有的激励器产生测试向量的方式,来将stimulator、tests和dut最终可以分开,保持各自的独立性。这么做的好处是使得复用和维护的效果更显著,而通过将不同功能隶属于不同模块的方式,也自然可以引申出我们下一节,如何将这种功能隔离、封装模块的方式带入到激励器的封装,为大家介绍类(class)和对于stimulator的应用场景。
浙公网安备 33010602011771号