目录
6.2.5协议异常、错误(error)和违规(violation)
6.9pre_randomise和post_randomise函数
6.1介绍
随着设计变得越来越大,要产生一个完整的激励集来测试设计的功能也变得越来越困难。解决的办法就是采用受约束的随机测试法(CRT)自动产生测试集。定向测试集能找到你认为可能存在的bug,CRT方法通过施加随机激励,可以找到你都无法确定的bug.可以通过约束来选择测试方案,只产生有效的激励,以及测试感兴趣的功能项。
简单的定向测试集只需要施加激励,然后人工检查输出结果,正确的输出结果随后可以保存为标准日志文件(golden file),用来和后面的仿真结果比较,以判断仿真结果的正确性。CRT环境不仅需要产生激励,还需要通过参考模型、传输函数或者其他方法预测输出结果。然而,只要准备好了这个环境,你就可以运行上百次的仿真而无需人工检查结果,从而提高工作效率。 这种用CPU时间(计算机的工作)来换取人工检查的时间(设计人员的工作)的方法是CRT的优势。
CRT由两部分组成:使用随机的数据流为DUT产生输入的测试代码和伪随机数发生器(PRNG)的种子(seed).只要改变种子的值,就可以改变CRT的行为。这样仅仅通过改变种子的值,就可以调整每次测试,使得测试可以达到很多次定向测试的效果。通常激励的空间是非常大的,以至于无法用for循环来产生各种可能的输入,所以必须要用产生子集的方式来解决这个问题。在后面的章节,我们将会学习如何用功能覆盖率来确定验证的进度。
6.2什么需要随机化
当想采用随机化的技术产生随机激励时,最先想到的是产生随机化的数据,这种方法实现起来非常简单,只需要调用$random函数。问题是这中方法找到bug的能力有限,只能找到数据路径方面的bug,这种方法的本质还是基于定向测试的方法。那些具有挑战性的bug大都在控制路径里。随机化使控制路径里的每个分支都可能被测试。
你需要考虑设计输入的各个方面,例如:
(1)器件配置;
(2)环境配置;
(3)原始输入数据;
(4)封装后的输入数据;
(5)协议异常;
(6)延时;
(7)事务状态;
(8)错误(error)和违规(violation);
6.2.1器件配置
在RTL级设计的测试过程中,找不到bug的最常见的原因是没有测试足够多的的配置!大多数测试仅仅使用刚退出复位设计的状态,或者仅仅用一个固定的初始化向量使设计进入确定的状态。这就好像PC刚装完系统还没有安装任何应用程序就对系统进行测试,这测试的结果肯定是非常好的,不会出现系统奔溃。
现实情况是,随着时间的变化,DUT的配置会变得越来越随机化。例如,测试工程师有一个600输入通道和12个输出通道的时分复用器。要测试这个器件,验证工程师必须写很多行的TCL代码来配置每个通道,他只可能验证少数几个通道的配置。如果采用CRT方法,他只需要写一个针对一个通道的随机化参数的测试平台,然后把它放到循环里去配置整个器件。这种方法可以发现以前的测试方法可能漏掉的bug。
6.2.2环境配置
通常你设计的器件在一个包含了若干器件的环境里工作。当验证DUT时,它连接到了一个模拟了这种环境的测试平台,你应该随机化整个环境,包括对象的数量以及它们的配置。
6.2.3原始输入数据
这是在使用随机激励时最先想到的问题,例如总线写操作的数据或ATM信填充的随机数据。实际上这非常简单,只需要准备好相关的事务类,但需要涉及协议的各个层次以及故障输入。
6.2.4封装后的输入数据
很多器件会处理激励的不同层次。例如一个器件可能会产生TCP流量,TCP数据随后被封装到IP协议里,最后被放到以太网包里被发送出去。协议的每个层次都有自己的控制域,可以采用随机化的方法测试不同的组合。你需要编写约束以产生有效的控制域,同时还允许注入故障。
6.2.5协议异常、错误(error)和违规(violation)
任何有可能出错的地方最终都会出错。你需要预见哪里有可能出错,注入会产生故障的测试矢量,然后确认设计可以正确处理这种故障,不会死锁或进入不确定的状态。好的验证工程师会测试设计在设计规范边界处的行为,甚至测试在设计规范之外的行为。例如,两个器件通信时,如果进行到一半通信中断了怎么办?如果在设计里存在错误检测和纠正部分,你必须确保测试各种正确和错误的组合情况。
测试平台应该能够产生功能正确的随机激励,然后通过翻转某一配置位,在随机的时间间隔里产生随机的错误类型。
6.2.6延时
许多通信协议定义了延时的范围,例如总线允许信号在总线请求信号1~3个时钟周期到来后,存储器的数据在4~10个总线周期后有效。然而许多针对速度优化的定向测试只使用一个测试集进行各种延时的测试,而其他测试都用最小的延时进行。测试平台应该在每个测试里都使用随机的、有效的延时,以便于发现设计中的bug。
在比周期级验证更低的级别,一些设计对时钟抖动非常敏感。通过把时钟沿来回移动一个很小的步长,可以检查设计是否对时钟周期的微小变化异常敏感。
时钟发生器应该是位于测试平台之外的一个模块,这样它就可以在有效区域(active region)产生事件,和其他设计事件一样。另外发生器应该具有一些可配置的参数,例如频率和相位,这些参数可以由测试平台在配置过程中配置。
注意,你现在是查找功能错误,而不是时序错误。测试平台不应该尝试违反建立时间和保持时间的约束。时序分析工具可以更好的发现时序错误。
6.3SV中的随机化
当和OOP同时使用时,SV中的随机激励产生是最有效的。你首先建立一个具有一组相关的随机变量的类,然后用随机函数为这些变量赋随机值,可以用约束来限制这些随机值的范围,使它们是有效值,也可以测试某些专用的功能。
6.3.1带有随机变量的简单类
这个类有4个随机变量,前3个使用rand修饰符,表示每次随机化这个类时这些变量都会赋一个随机值。kind变量是randc类型,表示周期性随机,即所有可能的值都出现过后随机值才可能重复。注意周期性是指单一变量的周期性,例如具有8个素的randc数组就有8种不同的周期。
约束是一组用来确定变量值范围的关系表达式,表达式的值永远为真。在这个例子里,src的变量值必须大于10小于15.注意,例子中的约束表达式是放在“{}”里的,并没有放在begin..end之间,这是由于这段代码是声明性质而不是程序性质。
randomise()函数在遇到约束方面的问题时返回0.本例使用断言来检查randomise函数的结果。针对不同的仿真工具,你必须使用相应的选项来使断言能够终止仿真过程,本例中使用$fatal来终止仿真。
注意,不能在类的构造函数里随机化对象,因为在随机化之前可能需要打开或关闭约束、改变权重甚至添加新的约束。构造函数用来初始化对象的变量,不能这里调用randomise()函数。此外,类里的所有变量都应该是随机的和共有的,这样测试平台才能最大程度测试DUT。
6.3.2检查随机化(randomise)的结果
randomise()函数为类里所有的rand和randc类型的随机变量赋一个随机值,并保证不违背所有有效的约束。当代码里出现矛盾的约束时,随机化过程会失败,所以一定要检查随机化的结果。如果不检查,变量可能会赋未知值,导致仿真的失败。
上例中用断言检查randomise()的结果,如果随机化成功则返回1,如果失败返回0.断言检查到错误后会显示错误信息。你必须对仿真器做一些设置,使仿真器在错误时能自动结束仿真。你也可以调用专用的函数来完成一些常规事务,例如显示一份总结报告后再结束仿真。
6.3.3约束求解
约束表达式的求解是由SV的约束求解器完成的。求解器能够选择满足约束的值,这个值由SV的PRNG从一个初始值(seed)产生。如果SV仿真器每次使用相同的初始值、相同的测试平台,那么仿真的结果也是相同的。
各种仿真器的求解器是不同的,因此使用不同的仿真器时受约束的随机测试得到的结果也有可能不同。SV标准定义了约束表达式的含义以及产生的合法值,但是并没有规定求解器计算约束的准确顺序。
6.3.4什么可以被随机化
SV可以随机化整型变量,即由位构成的变量;但是不能使用随机字符串或在约束中指向句柄。
6.4约束
有用的激励不仅仅是随机值-各个变量之间有着相互关系。你需要用包含一个或多个约束表达式的约束块定义这些相互关系,SV会选择满足所有这些表达式的值。
注意,每个约束表达式至少要有一个变量必须是rand或者randc类型的随机变量。下面的情况会报错:
randomise()函数会为随机变量选取一个随机值,并满足所有的约束条件。尽管可以使用约束来检查非随机变量的值是否有效,但是使用assert或if语句会更加方便。
6.4.1什么是约束
下例是一个具有随机变量和约束的类的例子:
6.4.2简单表达式
在一个表达式中最多能使用一个关系操作符(<、<=、==、>=、>).下例错误的想把3个变量按固定的顺序排序:
例6.5是实际的结果,可以看到它们并不是预期的。在例6.4中,约束bad按照从左到右的顺序分割成了两个关系表达式:((lo<med)<hi).首先计算表达式(lo<med),它的值为0或1,然后根据约束,hi的值要大于0或1.正确的约束如下所示:
6.4.3等效表达式
因为在约束块里只能包含关系表达式,所以在约束块里不能进行赋值。但是,可以用关系运算符为随机变量赋一个固定值,例如len==43.也可以在多个随机变量之间使用更加复杂的关系表达式,例如:len==payload.size() *4.
6.4.4权重分布
dist操作符允许产生权重分布,这样某些值的选取几率和其他值可以不同。dist操作符带有一个值的列表以及相应的权重,中间用:=或:/分开。值或权重可以是常数或变量。值可以是一个值或值的范围,例如[lo:hi]。权重不用百分比表示,权重的和也不必是100.:=操作符表示值范围内的每个值的权重是相同的,:/操作符表示权重要均分到范围内的每个值.
再次强调一遍,值和权重可以是常数或变量。你可以使用权重变量来随时改变值的概率分布,甚至可以把权重设为0,从而删除某个值。
在仿真过程中可以随时改变权重,以得到不同的概率分布。
6.4.5集合(set)成员和inside运算符
可以用inside运算符产生一个值的集合,除非对变量还存在其他约束,否则SV在集合里取随机值,各个值的选取概率是相同的,在集合里也可以使用变量。
在上例中,可选取的范围由lo和hi决定,可以采取这种方法使约束参数化。注意,如果lo>hi,就会产生一个空集,导致约束错误;
可以使用$来代表选取范围内的最小值和最大值,如下所示:
如果想选择一个集合之外的值,只需采用取反操作符!对约束取反即可:
6.4.6在集合里使用数组
把集合里的值保存到数组以后就可以使用这些值。
集合里的每一个值取出的概率都是等同的,即使该数值在数组中出现多次。
下面的例子是从枚举列表中取出一个值,可以随时修改这个列表。
如果想动态的向集合里添加或删除值,那么请慎重选择inside操作符,因为它会影响仿真器的性能。例如,如果希望集合里的每个值只取一次,那么可以使用inside从队列里取出一个值,然后通过从队列里删除这个值来慢慢减小队列,这种方法需要求解器计算N个约束。另一种方法是使用randc变量指向数组。和计算大量约束相比,这种方法速度更快。
6.4.7约束条件
通常约束块里所有的约束表达式都是有效的,如何让某些约束只在某些时候有效呢?SV支持两种关系操作:->和if-else.
->操作符可以产生和case操作符类似效果的语句块,可以用于枚举类型的表达式。
注意:在约束块中用{}把多个表达式组合成一个块,而在程序性代码中使用begin...end。
6.4.8双向约束
约束块不像是自上而下执行的程序性代码,它们是声明性代码,是并行的,所有的约束表达式同时有效。如果你用inside操作符约束变量的取值范围为[10:50],然后用另一个约束表达式约束变量必须大于20,SV对这两个约束同时求解,最终限定变量的范围是21~50.
SV的约束是双向的,这代表它会同时计算所有随机变量的约束,增加或删除任何一个约束都有可能对随机变量的取值产生影响。看下例:
SV同时计算四个约束表达式,根据约束之间的限定,下表列出了三个变量的各种取值:
6.4.9使用合适的数学运算来提高效率
约束求解器可以有效的处理简单的数学运算,例如加、减、位提取和移位。约束求解器对于32位数值的乘法、除法和取模运算的运算量是非常大的。
上述的取模运算可能需要耗费很长的时间,但是在硬件里很多常数都是2的幂,利用这一点可以用位提取来代替除法和取模运算。同样乘2的幂可以用移位运算来代替。
6.5解的概率
说到随机数,就必须提到概率。SV并不保证随机约束求解器能给出准确的解,但可以干预解的概率分布。
6.5.1没有约束的类
这两个变量的值有8种可能的解。只有经过上千次的随机化才能得到下表的概率分布。
6.5.2关系操作
下表列出了所有的解和相应的概率分布:
6.5.3关系操作和双向约束
6.5.4使用solve...before约束引导概率分布
solve...before约束不会改变解的个数,只会改变各个值的概率分布。
除非你对某些值出现的概率不满意,否则不要使用solve...before。过度使用会降低运算的速度,也会使约束难以理解。
6.6控制多个约束块
一个类可以包含多个约束块,例如把一个约束块用于确认事务的有效性。可以把不同的约束块用于不同的测试。在运行期间,可以使用内建的constraint_mode()函数打开或关闭约束,用handle.constraint_mode()控制对象的所有约束。看下例:
6.7有效性约束
设置多个约束以保证随机激励的正确性是一种很好的随机化技术,也称为有效性约束。
6.8内嵌约束
随着测试的进行,面对的约束越来越多,它们会相互作用,最终产生难以预测的结果;用来使能和禁止这些约束的代码也会增加测试的复杂度。
很多测试只会在代码的一个地方随机化对象,SV允许使用randomise()with来增加额外的约束,这和在类里面增加约束是等效的。看下例:
注意:在with{}语句里,SV使用了类的作用域,所以在上例中使用了addr变量,而不是t.addr。
在使用randomise()with语句时常犯的错误就是使用()而不是{}内嵌的约束。记住,约束块应该使用{},内嵌约束也应该使用{},{}用于声明性的代码。
6.9pre_randomise和post_randomise函数
有时需要在调用randomise()之前或之后立即执行一些操作,例如在随机化之前可能要设置一些非随机变量。SV可以使用两个特殊的void类型的函数 pre_randomise和post_randomise来完成这些功能。
6.9.1构造浴缸型分布
verilog已经提供了很多非线性分布函数,但没有浴缸型分布函数。下图展示了如何用两条指数曲线来构造浴缸型分布。
变量value的值在每次对象随机化的时候更新,经过多次随机化后,就可以得到预期的浴缸型分布。由于变量value是由程序计算得到的,不是由随机约束求解器得到的,所以不需要用rand修饰。
6.9.2关于void函数
由于函数不是任务,所以并不消耗时间。 pre_randomise和post_randomise函数只能调用其他函数,不能调用消耗时间的任务,所以在执行randomise()函数的期间无法产生一段延时。如果想调试随机化过程出现的问题,可以调用事先预备好的void类型的显示程序来显示中间结果。
6.10随机数函数
下面是一些常用的函数:
6.11约束的技巧和技术
怎么才能编写易于修改的CRT?下面是一些小窍门
6.11.1使用变量的约束
通过改变变量max_size的值可以改变随机变量size的上限。
6.11.2使用非随机值
如果你用一套约束在随机化的过程中已经产生了几乎所有的激励向量,但是还缺少几种,这时候可以采用先调用randomisr()函数,然后再把随机变量的值设定为固定的期望值来产生缺少的激励向量---并不一定要使用随机值。设置固定的激励值可以违反相关的约束。
如果只有少数几个变量需要修改,可以使用rand_mode函数把这些变量设置为非随机变量。
代码的后半部分先调用了rand_mode函数将length变量设置为非随机变量,并固定值为42,然后调用randomise()进行随机化。
6.11.3用约束来检查值的有效性
在随机化一个对象并改变它的变量的值后,可以通过检查值是否遵守约束来检查对象是否任然有效。
6.11.4随机化个别变量
可以在调用randomise()函数时只传递变量的一个子集,这样就只会随机化类里的几个变量。只有参数列表里的变量才会被随机化,其他变量会被当做状态变量而不被随机化。但是所有的约束依然有效。需要注意的是,可以在随机化时传递一个非随机变量。这种随机化某些变量的方法并不常用。
6.11.5打开或关闭约束
可以使用条件操作符->或if-else来构建非随机变量控制的约束,看下例:
更加常见的方法是对每种指令建立一套独立的约束,在使用时关闭其他的约束。
6.11.6在测试过程中使用内嵌约束
如6.8节所示,可以使用randomise()with内嵌约束语句使约束的作用范围局部化,这是一种很好的做法。但是内嵌约束也有一些缺点。首先,约束代码会位于代码的不同位置,当为一个类增加新的约束时很可能与之产生冲突;其次很难在不同的测试里复用内嵌约束。
6.11.7在测试过程中使用外部约束
函数的函数体可以在函数外部定义,约束的约束体也可以在类的外部定义。可以在一个文件里定义一个类,这个类只有一个空约束,然后在不同的测试里定义这个约束的不同版本以产生不同的激励。
外部约束与内嵌约束相比,外部约束可以放在另一个文件里,从而在不同的测试里复用外部约束。外部约束对类的所有对象都起作用,而内嵌约束仅仅影响一次randomise()调用。但是要注意,外部约束只能增加约束,而不能修改已有的约束,而且必须事先在原来的类里定义外部约束的原型。
6.11.8扩展类
采用这种技术,测试平台可以先使用已有的类,然后切换到增加了约束、子程序和变量的扩展类。注意,如果扩展类里定义的约束名字和基类里的约束名字相同,那么扩展的约束将会取代基类的约束。
6.12随机化常见的错误
6.12.1小心使用有符号变量
除非必要,不要在随机约束里使用有符号类型!
显然你可以得到(32,32)这样正确的数值对,也可以得到(-63,127)这样的数值对。它们都是等式的合法解,但是后者明显不是你所想要的。为了避免得到这样无意义的解,应该只使用无符号随机变量。
这个版本也会产生错误。两个随机变量的位宽所能显示的范围远大于64,这样两者相加时会丢弃高位,产生32'd64或32‘h40,最好的办法就是限制这两个变量的位宽。看下例:
6.12.2提高求解器性能的技巧
注意避免复杂的运算!
6.13迭代和数组约束
到目前为止,我们已经可以约束标量类型的变量,但如何在随机化数组时进行约束?
用foreach约束会产生很多约束,从而影响仿真器的运行速度。
6.13.1数组的大小
最容易理解的数组约束是size()函数,他可以约束动态数组或队列的素个数。
使用inside约束可以设置数组大小的上限和下限。注意要避免空数组,同时也一定要设置数组的上限。
6.13.2素的和
可以用sum() 函数约束数组素的和
单比特素的和在正常情况下还是单比特,但是在上例中把strobe.sum()和四位数值4'h4比较,所以数组素的和的计算是用4位精度进行计算的。
6.13.3数组约束的问题
sum()函数看起来很简单,但verilog的数学运算规则使它很可能会导致很多问题。看下例:
这段代码产生了一些长度较小的事务,但它们的和有时为负,并且时钟小于127,这绝对不是我们所期望的!下面采用无符号类型再试一次:
这里有个问题,虽然约束了数组的和小于1024,但是实际上的和始终小于256,问题的原因在于8位数值的和也是用8位表示的。下面数组素的宽度拓展到32位:
这又出现了问题,两个非常大的数的和为什么会变成一个小数值。下面再根据约束中的比较操作把素位宽减小:
结果还是不对,由于每个素的位宽超过8位,所以素值经常超过256,必须约束素的值在1~255之间,然后使用10位位宽来求和。这就涉及到对数组的每个素进行约束。
6.13.4约束数组和队列的每个素
SV可以用foreach对数组的每个素进行约束
注意,素的位宽可以是10位或更宽,但是必须是无符号数。
也可以对数组素之间的关系进行约束,但要特别注意数组两端的边界素。
6.13.5产生具有唯一素值的数组
如果使用randc数组,那么数组的每个素都会独立的随机化,所以一定会出现重复的值。你也可以用嵌套的foreach循环让求解器比较任意两个素,但是这样会降低求解器的速度影响仿真速度。
更好的办法是使用包含randc变量的辅助类,这样就可以不断随机化同一个变量。
下面是更常用的办法:
6.13.6随机化句柄数组
如果要产生多个随机对象,那么需要建立随机句柄数组。与数值数组不同的是,你需要在随机化前分配所有的素,因为随机求解器不会创建对象。如果使用动态数组,可以按照需求分配最大数量的素,然后再使用约束减小数组的长度。在随机化时,动态句柄数组的大小可以保持不变或减小,但是不能增加。
6.15随机控制
$urandom_range函数返回一个指定范围内的随机数,可以用(最大值,最小值)或(最小值,最大值)的形式作为函数参数。如果只使用一个参数,SV会把它当做(0,最大值)对待。
你也可以用类和randomise()函数编写上例。如果上例是一个大类的一部分,使用约束会比使用randcase语句简洁。
使用randcase的代码比用随机约束的代码更难修改和重载,所以要小心使用randcase。
6.15.1用randcase建立决策树
代码只有两级,但是可以很方便的扩展成多级。
6.16随机数发生器
SV的随机性到底如何?一方面,测试平台需要不相关的随机值来产生不同于定向测试的随机激励;另一方面,即使设计或测试平台只做了微小的修改,或在测试特殊的测试时,又都需要不断重复某个测试模式。
6.16.1伪随机数发生器
verilog使用了一种简单的PRNG(伪随机数发生器),通过$random函数访问。发生器有一个内部状态,可以通过$random的种子来设置。下例是一个简单的PRNG,这个PRNG有一个32位的内部状态,要计算下一个随机值,先算出状态的64位平方值,取中间的32位,然后再加上原来的32位数值。
可以看到这段代码可以产生看起来是随机的数据流,并且可以通过设置相同的种子来重复码流。SV称之为PRNG,通过randomise()和randcase来调用它产生随机值。
6.16.2随机稳定性--多个随机发生器
在SV中,每个对象和线程都有一个独立的PRNG,改变一个对象不会影响其他对象获得随机数。
6.16.3随机稳定性和层次化种子
SV的每个对象都有自己的PRNG和独立的种子。当启动一个新的对象或线程时,子PRNG的种子由父PRNG提供,所以在仿真开始时一个种子可以产生多个随机激励流,它们之间又是相互独立的。
在调试测试平台时,我们会增加或删除或移动代码,即使具备随机稳定性,代码的变化也会使随机平台产生不同的随机值。把新增的对象或线程放在现有的对象或线程之后,可以减少修改代码带来的结果。
下例增加了一个新的发生器,并在新的线程里运行。新的对象在已有对象之后创建,新的线程也是在原有线程之后产生。
虽然随着新代码的加入,随机码流无法和过去保持一致,但是可以减小这些改变带来的不良影响。
6.17随机器件配置
测试DUT的一个重要工作是测试DUT内部设置和环绕DUT的系统配置,测试应该随机化环境,这样才能保证尽可能测试足够多的的模式。
今天的文章 第六章-随机化分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/102249.html