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

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

日志

SV系统集成篇之三:测试场景的生成(上)

已有 1463 次阅读| 2017-6-8 23:57 |个人分类:验证系统思想|系统分类:芯片设计


文章结构:
  • 对比之前的硬伤,提出动态生成数据的优势
  • 介绍fork的用法和注意事项
  • 提供示例代码来表示多个stimulator的调度

在《SV组件实现篇》的激励器的封装和随机化两节中,可以看到通过将stimualtor与特定的test区分,就可以实现测试向量(test vector)的生成与stimulator剥离。为什么要这么做呢?因为stimualtor是作为验证环境的组件被设置到不同的测试平台上的,可以看作软件世界的”不动资产“;而测试向量呢?通过不同的test类生成符合不同场景的向量进而为不同功能点测试服务。我们再来梳理一下之前的对于激励器封装和随机化当中,如何随机化测试向量数组,以此使得stimualtor可以有数据可以发送呢?
  1. 第一种方式,可以在stimulator中内置一个数据类trans的动态数组ts[]。然后在其后的test中,随机化ts的数据内容,这包括了ts的长度和元素的内容。这种方式的特定是,test类只用来随机化stimulator内置的激励向量数组,本身不会产生数据。
  2. 第二种方式,在各个test中内置有随机向量数据源,在仿真刚开始的阶段通过随机化预先生成所有的测试向量,接着将所有的测试向量一并拷贝到stimulator中trans的动态数据ts[]内。

上面这两种方式是为了较清晰地说明test类同stimulator类之间的联系和区别:
  • 对于test类,它的任务是负责产生与测试对应的随机向量;对于stimulator而言,它是验证环境的“不动资产”,它本身不产生向量,而是只保留有测试向量。
  • 从验证的复用性来看,我们可以有多个test类,而只需要有一个stimulator类。这对应了不同的验证功能点(由test生成),和不变的接口时序(由stimulator生成)。

然而,对于验证的灵活性和复用性,上面的两种方式都有一些“硬伤”:
  1. 测试向量的生成都是在仿真刚开始生成好的,这使得无法根据测试向量生成的轨迹来调整后续向量的内容。
  2. 从模块级到子系统的集成中,无法直接复用来自于各个模块的test类。这是由于模块验证的test类直接包裹了模块验证环境类env,并且在其内部进行测试向量随机化的任务。

上面提到的一点,如果在test内部直接进行向量随机化的任务,那么可能会有从底层到高层的复用难题。从下面这张图可以观察到:

如果从模块级的验证环境env1和env2中,很容易可以通过类的嵌套完成验证结构的重新组织;而之前测试向量的生成是通过test1和test2内置的task来实现的。那么,这两个任务能否很好地嵌套进入test_top呢?首先,test_top既没有集成继承也没有继承test2,而是新的类,因此也谈不上这两个task的继承。那么是否可以像env_top集成env1和env2呢?也没有可行性,毕竟task并不像对象,是数据和方法的载体,无法进行例化。读者同时也需要注意,test_top一方面无法集成task1和task2,另外也无法集成task1和task2所使用到的随机测试向量数据源。

也就是说,test_top如果要进行集成,最好可以有载体容纳上面的task1和task2还有随机数据,那这是什么?这显然是一个类啊!如果对上面的随机数据和随机数据生成task进行封装的话,那么可以新添加一个中间的类即vector类。该类的作用就是生成随机数据,并且将随机数据传递给env中的stimulator。那么,新的模块级验证结构和子系统集成的流程图可以变为:
从这张“升级”了的测试场景示意图中可以发现,更新的部分是:
  • 原先定义在test内部的数据和数据随机任务被进一步封装到了类中,即vector类。该类的任务就是生成不同的数据,而与其对应的test,则只需要装载对应的vector对象和固定的env“不动资产”就够了。
  • 另外的变化是,由于数据被封装到了vector类中,无法直接暴露给test和env,因此,vector对象和env对象之间应该建立起来通信。

上面指出了升级的意图之一在于建立vector与env之间的通信,那么通信是像之前的方法预先生成好数据,然后只进行一次数据搬迁?还是保持动态的生成和不间断的数据通信呢?选择它们的根据又在哪里?让我们考虑一下下面的两种情况。

如何在数据包的传输过程中对激励进行控制?
如果只进行了一次的数据搬迁,那么不但会发生在短时间内有多个对象的产生即峰值内存的极度消耗以外,这些可能大量的内存消耗的下降却只会逐渐释放。从下图可以发现,前期建立验证环境会消耗一些内存空间(少量的),而紧接着如果要一次性生成大量的随机数据时,就会消耗很客观的内存,而只有当这些随机数据(对象)被stimulator消耗并且丢弃之后,它们这些对象才会被逐渐回收。因此,首先这种方式会使得仿真的性能降低很多。
另外,一旦数据被生成好送给stimulator以后,无论是vector类还是test类都无法很方便地控制stimulator是否继续发送数据(使能)、发送数据之间的间隔(数据间隔信息已经全部生成在随机数据中)。

相比较之前预先全部生成数据的方式,如果通过持续动态的数据生成方式,那么上面的问题就能得到解决了:
  1. 不会短时间生成大量对象消耗太多空间。
  2. 通过动态的生成方式,后续生成数据可以用来使能stimulator以及数据之间的时序。


在集成的上层环境中,如何对多个stimulator的时序做出调度?
从第一个问题的考虑来看,在预先生成测试向量的情况下,如果无法对单一的stimulator做出很好的调度,那么自然无法对于后期MCDF集成环境中的多个stimulator做出灵活的调度了。

那么动态测试向量的生成方式有帮助吗?怎么来控制stimulator之间的时序呢?在给出实例代码之前,我们有必要介绍SV的线程并行调度方式特性fork-join。


fork-join
在之前《SV组件实现篇》中提到了组件之间(线程之间)的通信手段,而这里我们再需要考虑如何进行进程之间的并行运行。上面的vector类通过并行执行的方式,可以实现真实的硬件场景。我们在这里需要思考一下,为什么并行的线程才是真实的硬件场景呢? 因为硬件的行为和处理方式都是“并行”的,所以软件的激励为了模拟硬件的行为也应该是并发的。下面再来梳理梳理fork-join常见的几种使用方式吧。


面的图中,T1、T2和T3是耗时不一的三个线程,耗时按照从长到短的顺序排列依次为T2、T1、T3。在这三种不同的模式下,执行的细节应该是:
  • fork-join:fork线程块(包含三个子线程T1、T2、T3)需要等待三个子线程都执行完毕之后才会结束,即需要等待最长的T2执行完毕,进而执行接下来的部分。
  • fork-join_any:fork线程块只需要等待T1、T2、T3中的任何一个子线程执行完毕,就可以继续执行后面的任务。这里,fork块只需要等待T3执行完毕,就可以完成等待动作,执行后面的任务了。
  • fork-join_none:fork线程块无需等待任何的子程序完成,没有任何延迟,即可以执行后面的任务。在这里fork-join_none的线程块起到了触发子线程运行的“点火”作用,而不会等待这些线程“烟花”结束。

从上面回顾的三种fork线程块使用方式来看,读者需要注意的是:
  • 三种方式都可以用来对线程进行“点火”。
  • 三种方式的不同在于结束fork块等待的时序不同。最慢的数fork-join,fork-join_any次之,fork-join_none则是最快的。
  • 对于fork-join_any和fork-join_none而言,尽管fork块结束等待后继续进行后面的任务,但是已经被“点火”的线程还会持续“升空”直到绽放出烟花,运行完毕各个子线程。这一点读者要务必注意。

fork-join的特点在于fork线程块结束等待时,也就是各个子线程全部执行完毕的时候。那么对于fork-join_any和fork-join_none,我们如何得知各个子线程的执行状况呢?毕竟如果不做特殊的处理,子线程的结束不会再额外“敲门”,告诉verifier,嗨,快来瞧瞧我,我得走(执行完毕)了!

我们可以通过设置一些在子线程和线程外部的共享变量来满足上面的需求。譬如下面这个例子:

module fork_case1;
event e1, e2, e3;
task t1;
#15ns;
$display("t1 is leaving");
-> e1;
endtask
task t2;
#20ns;
$display("t2 is leaving");
-> e2;
endtask
task t3;
#10ns;
$display("t3 is leaving");
-> e3;
endtask

initial begin
$display("fork:thread_trigger start");
fork: thread_trigger
t1();
t2();
t3();
join_none
$display("fork:thread_trigger finish");
$display("fork:thread_monitor start");
fork: thread_monitor
@e1 $display("bye to t1");
@e2 $display("bye to t2");
@e3 $display("bye to t3");
join
$display("fork:thread_monitor finish");
end
endmodule

输出结果:
# fork:thread_trigger start
# fork:thread_trigger finish
# fork:thread_monitor start
# t3 is leaving
# bye to t3
# t1 is leaving
# bye to t1
# t2 is leaving
# bye to t2
# fork:thread_monitor finish

首先来看看fork块thread_trigger,由于是join_none的时序要求,所以只管“点火”,点完就跑到下面的fork块thread_monitor,该块的作用是监测thread_trigger中的三个子程序t1、t2、t3,等待它们各自执行完毕时触发的event e1、e2和e3。由于在进程thread_trigger和thread_monitor之间,e1、e2、e3是共享的,因此thread_monitor可以通过这三个event来监测各个子进程是何时结束的。待所有子进程都执行完毕之后,thread_monitor的三个子线程的监测任务才会结束。

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


点赞

评论 (0 个评论)

facelist

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

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

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 253

    粉丝
  • 25

    好友
  • 33

    获赞
  • 45

    评论
  • 访问数
关闭

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

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

GMT+8, 2024-4-26 22:20 , Processed in 0.015293 second(s), 12 queries , Gzip On, Redis On.

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