数字前端的功能验证利器——SVA断言学习笔记

在我们辛苦搭建环境之余,不如先放松下学习一下非常流行并且历史悠久的断言技术(assertion),熟练掌握断言技术通过断言进行行为检查可以及时发现代码中的低级的bug。以下内容大部分参考了《SystemVerilog Assertions应用指南》,还有一些参考了各类帖子,尽量采用简单易懂的形式来说明。

这次我们只关注最常见的并发断言而不对即时断言进行探究了(下文断言皆指并发断言),我个人理解并发断言一般有验证来完成,即时断言一般由设计完成,二者之间具体的区别详见《指南》第9页,不再赘述。

下文中的所有波形均使用timing designer绘制,如需要软件可以发网盘链接,侵删。

断言储备知识

断言用来干嘛

那么我们先来明确一下断言到底是来干嘛的呢?简单而言就是检查某一行为与我们的预期是否相符,如果相符则断言成功否则断言失败。举个栗子,这个行为可以是:1)A信号有效后(由当拍算起)三拍内B信号值跳变为5,2)vld信号有效时data信号不能为X态,那么针对这两个行为我们可以都可以写出断言,一旦仿真中行为不符合了那么会报错。

断言可以放在过程块(procedural block)、模块(module)、接口(interface)和程序(program)中。

断言的时间点

断言要熟记两个时间点:采样时刻和匹配时刻,断言在preponed域采样,在observed域执行检查(关于时钟域也有很多介绍,之后我再写一个简单明了的吧)。简单来说断言的采样点处于时钟上升沿之前、module中@clk采样时刻之后,也就是说采样到的是上升沿之前的”旧值”。验证环境中,@clk进行采样时的采样顺序可以简记为module @clk采样(旧值)->断言采样(旧值)->program @clk采样(新值,跳变后的)。

数字前端的功能验证利器——SVA断言学习笔记_第1张图片

 

信号与序列

信号(signal)和序列(sequence)是断言中最常用的两个概念,信号就不用说了,单说一说序列。序列可以是“简单的同一个时钟边沿被求值的布尔表达式,或者是连续多个时钟周期求值的事件”。简单来讲,a==1是一个序列,a有效的三拍内b也有效且b有效的当拍c==5也是一个序列,后者更为典型。那么我们就从序列的表示开始入门。

事件的表述

我们首先要学会描述事件,之后才是进行判定,因为断言常用的形式是事件A发生了,之后需要发生事件B,如果没发生那么就报错。

下文中的a、b、c既可以表示信号表达式也可以表示序列(除非有特殊说明),可以统称为事件,如果不太理解那就认为是1比特信号好了。

还要注意一点,验证平台会在每一拍检查你写下的序列,如果不匹配就会报失败,当然这个“失败”没关系,更多的情景我们是在找我们写下的序列什么时候发生了,是否触发额正确的后续操作,因此这类失败可以称之为假“失败”,如何避免这类失败打扰我们检查后面会提到。

运算符

a ##3 b:延时运算符

含义是a有效后的第3拍(a有效当拍为第0拍),b也有效。如果b在第3拍未能有效,则序列匹配失败。例如如下波形图,信号a在第60ns采样判定为1,开始进行序列的匹配;在第120ns即开始匹配后的第3拍,采样信号b的值为1,则判定开始与60ns的序列匹配成功。

数字前端的功能验证利器——SVA断言学习笔记_第2张图片

再看下一个波形,注意我们使用的值都是时钟沿之前的信号值,匹配点可以认为在时钟沿之后一点点(实际上就在这个时钟沿,仿真中time slot的概念,以后再讲)。那么对于下面的波形,(起始于)160ns的序列匹配成功,180ns的序列匹配成功,200ns的序列匹配失败因为在260ns处信号b不为1。

数字前端的功能验证利器——SVA断言学习笔记_第3张图片

应该说是非常简单了,那么继续深入下a和b也可以是布尔表达是例如(a==’h5) ##2 (b==’ha)这样的序列,甚至a和b本身也可以是序列例如(a==1 ##1 b==2) ##2 c表示只有在满足a==1下一拍b==2再两拍后c有效时匹配成功,否则匹配失败(注:如果断言中只写了这个序列,那么时钟会在每个上升沿时寻找该序列的有效开端a==1,那么也就是说如果当拍a!=1断言会报一个假“错误”,对于这一点如何解决请见下文)。

拓展一下,这句语法的全集是a ##[m:n] b,例如写成a ##[2:4] b意味着a有效后的第2拍到第4拍之间b有效至少一次则序列匹配成功。

##0需要单独说一下,这个信号用于两个序列紧密连接,a ##0 b即a事件的结束与b事件的开始发生在同一拍,这里我们先举一个单拍信号的例子,之后肯定还会再看到。##0需要单独说一下,这个信号用于两个序列紧密连接,a ##0 b即a事件的结束与b事件的开始发生在同一拍,这里我们先举一个单拍信号的例子,之后肯定还会再看到。

波形为a ##3 b ##0 c,a ##3 b在120ns匹配成功,当拍a ##3 b ##0 c也匹配成功,因为120ns也是c起起点。

数字前端的功能验证利器——SVA断言学习笔记_第4张图片

a[*3]:连续重复运算符

含义是a连续为1重复3拍,即a ##1 a ##1 a,参见下面的波形,160ns处起始的序列在200ns处匹配成功一次。

数字前端的功能验证利器——SVA断言学习笔记_第5张图片

理解了上面这个序列,再来理解下(a ##1 b)[*3],这个序列等同于(a ##1 b) ##1 (a ##1 b) ##1 (a ##1 b),对于下面这个波形会在80ns处开始尝试匹配在160ns处匹配成功。那么如果是(a ##1 b)[*2]呢?会在120ns处和180ns处分别匹配成功,成功两次。

数字前端的功能验证利器——SVA断言学习笔记_第6张图片

那么我要如何表示下面的序列呢?可以写成(a ##1 b)[*3] ##2 c,在200ns处匹配成功,思考一下有没有问题。

数字前端的功能验证利器——SVA断言学习笔记_第7张图片

 

拓展一下,这句语法的全集是a [*m:n] b,如a[*2:4]含义为a信号重复2~4拍后b信号有效均可匹配成功,等同于a [*2] b or a [*3] b or a[*4] b。

注意:延时运算符和连续重复运算符中(a、b、c)既允许使用表达式,又允许使用序列;而接下来要看的跟随重复和非连续重复则只能使用表达式,不能使用序列。

a [->3]:非连续跟随重复符

或者叫go-to重复符。

含义是a非连续(就是无所谓连不连续)重复3次,只在第3次重复时刻点匹配(跟随的含义),不妨直接看这个序列a ##1 b[->3] ##1 c,先来理解一下这个,这个序列表示在a有效之后,c有效之前,b应该有效3次且最后一次有效的时间应当恰好比c有效早1拍(跟随性);

数字前端的功能验证利器——SVA断言学习笔记_第8张图片

 

在60ns处a信号有效,在160ns处b[->3]有效,下一拍c信号有效,整个序列在180ns处匹配成功,注意b信号有效是不需要跟随a信号的,也就是说下面这个波形也是可以匹配成功的。

数字前端的功能验证利器——SVA断言学习笔记_第9张图片

这个一样可以拓展一下,写成a [->m:n],那么就是a重复2次或者3次时均匹配成功,对于

a ##1 b[->2:3] ##1 c这个序列,以下两个波形都是在180ns处匹配成功。

数字前端的功能验证利器——SVA断言学习笔记_第10张图片

数字前端的功能验证利器——SVA断言学习笔记_第11张图片

之后我们会看到,这个运算符最长见的是[->1]的用法,a [->1]等价于(!a) [*0:$] ##1 a。

a [=3]:非连续(无所谓跟随)重复操作符

含义是a无所谓连续不连续,反正重复了3次那么就匹配成功了。

a[=1]等价于(!a) [*0:$] ##1 a ##1 (!a)[*0:$],这句话很难懂吧,没事我们一点一点分析:(!a)意思是a事件没发生,(!a) [*0:$]意思是a任意多拍都没有发生($代表大于等于:前面的那个值的任意数),(!a) [*0:$] ##1 a意思是a任意多拍都没有发生后,终于在某一拍有效;(!a) [*0:$] ##1 a ##1 (!a)[*0:$]意思是a某一拍有效了之后,后面又任意多拍无效、没发生。

同理,a[=2]等价于(!a) [*0:$] ##1 a ##1 (!a)[*0:$] ##1 a ##1 (!a)[*0:$],以此类推。

不如简单的看下a[=3]波形理解。

我在图中标蓝的点都是匹配成功的点,那么匹配起始在哪里呢?40ns前的上升沿都是起始点。那么继续考虑,这个跟a[->3]的区别在哪里呢?下图中标红的点是a[->3]匹配成功的点,只有一个。

用一个更加明显的例子来说明一下吧,a ##1 b[=3] ##1 c跟刚刚的差不多,那么这个序列以下两个波形是都可以匹配成功的,c无需跟随b[=3]。

数字前端的功能验证利器——SVA断言学习笔记_第12张图片

数字前端的功能验证利器——SVA断言学习笔记_第13张图片

 

and运算符

and运算符用来连接两个事件(多为序列),当两个事件均成功时匹配成功,需要注意的是两个事件必须有相同的起始点,但是可以有不同的结束点。举个简单的例子,(a ##[2:4] b) and (a ##5 c)在波形中可见,100ns处(a ##[2:4] b)匹配成功,140ns处整个序列匹配成功,其他时间都是匹配失败的(假“失败”)。

数字前端的功能验证利器——SVA断言学习笔记_第14张图片

说一下and和&在断言中的区别与联系。a & b也是要a和b同时成立在是成功,不过此式中a和b必须为表达式,匹配也只能在每一拍内进行;而a and b中的a、b既可以是表达式又可以是序列,是可以进行跨拍匹配的。例如(sig_a == 5) & (sig_b == 3)也可以写成(sig_a == 5) and (sig_b == 3),但是(a ##[2:4] b) and (a ##5 c)不可以写作(a ##[2:4] b) & (a ##5 c)。

or运算符

and运算符用来连接两个事件(多为序列),当两个事件任一匹配成功则整体成功,与|的区别和之前类似,or可以连接两个表达式或是两个序列,|只能连接两个表达式。

intersect运算符

我认为intersect运算符是最重要的一个运算符,后面会大量的用到。intersect和and有一些类似,都是连接两个事件,两个事件均成功整个匹配才成功。不过intersect多了一个条件,那就是两个事件必须在同一时刻结束(and已经需要保证两个事件同一时刻开始了),换句话说a intersect b能匹配成功,a、b两个序列的长度必须相等。

通过几个例子来深入理解下,(a ##[1:2] b) intersect (c ##[2:3] d),见波形如下图。图中黄色点为a ##[1:2] b匹配成功时刻点,蓝色为c ##[2:3] d匹配成功点,根据我们之前的分析,intersect连接的两个序列必须有同样的结束时刻,因此整个序列匹配的时间点一定在黄色和蓝色重合的点中,那么我们需要分析下起始点。对于180ns,a ##[1:2] b起始点为160ns或140ns,c ##[2:3] d的起始成功点为140ns或120ns(如果觉得这样比较乱,那就顺着从开头找),因此起点在140ns终点在180ns的波形符合序列(a ##[1:2] b) intersect (c ##[2:3] d),匹配成功;同理起点在160ns终点在200ns的波形符合序列,起点在180ns终点在220ns的波形也符合序列。

数字前端的功能验证利器——SVA断言学习笔记_第15张图片

记住intersect最重要的特点:连接打两个序列有相同的起点相同的起点相同的终点和必然相同的序列长度,这一点之后会非常好用。

within运算符

a within b含义是在事件b(序列b)匹配期间,事件a(序列a)至少出现1次则匹配成功,否则失败。再正规一些的表述就是序列a在序列b开始到结束的范围内发生,序列b开始匹配点必去在序列a开始 匹配点之前发生,序列a的结束匹配点必须在序列b结束匹配点之前结束。不过在此我自己有一个疑点,序列b应当包含序列a,那么序列b和序列a的起始时间或结束时间相同是否可以,对于这点暂做遗留问题,我亲自仿真下寻找答案(暂时认为是不可以的)。

还是举一个简单的栗子看看波形吧,(a ##1 b[=3]) within (c ##1 d[->1])。

数字前端的功能验证利器——SVA断言学习笔记_第16张图片

40ns~200ns是一次成功的匹配,(c ##1 d[->1])在40ns开始在200ns成功,(a ##1 b[=3])在80ns开始在160ns(和180ns、200ns...)匹配成功,(a ##1 b[=3])完美包裹在(c ##1 d[->1])之中;240ns~320ns是一次典型的匹配失败,请自行分析下。

因为后面用到within很少,我就不再深入探究了。

throughout运算符

throughout运算符和intersect有些接近(我个人认为throughout是可以完全被intersect取代的,所有throughout都可以写成intersect),不过区别在于throughout必须连接一个表达式和一个序列即req throughout seq,含义是在seq匹配起始到结束期间,req都必须成立。例如a throughout (b ##1 c[->1])就要求从b有效开始,直到c第一次有效结束,这段期间a必须始终保持有效,如下波形图60ns~140ns就是一次典型的匹配成功。

数字前端的功能验证利器——SVA断言学习笔记_第17张图片

这个我用的的确不多,就写这么多吧。

first_match

含义是只使用第一次匹配,丢弃其他时刻点的匹配。

在之前所写的事件描述中我们提到过,仿真器会在每个时钟窗口去匹配,大概率会匹配成功多次。在一些情景下,我们只关心第一次出现的状况,例如我们认定某一些信号组合是不能出现的,一旦出现了就应该报error提醒我们进行检查,这种情况下我们只需要关注第一次出现的时间点就可以了,那么我们就可以使用first_match了。不过说实话这个运算符在断言中用的不多。

内建函数

以下内建函数,都是对表达式进行操作,不能对序列进行操作。

$rose

$rose()函数就是我们常说的上升沿检测,匹配规则是信号的当拍值为1上拍数据为0。以$rose(a, clk1)为例,注意看下面的波形:

注意看$rose(a, clk1)匹配的时间点并不是上升沿的时间点,而仿佛是上升沿的下一个时钟沿;这是因为我们之前说过,断言是在preponed域采样采到的是跳变前值,如在40ns时a信号的确有一个上升沿不过断言采到的是0与20ns时采到的是一致的,而在60ns时采到的是1,与40ns相比是一个由0->1的跳变过程,因此在60ns时钟沿匹配成功,后面两个匹配点含义一样。

$fell

$fell()函数和$rose函数刚好相反,是检测下降沿的,还是以$fell(a, clk1)为例,拿刚刚的波形我们看一下:

还是要注意匹配点,60ns采样值为1,80ns采样值为0,因此在80ns处匹配成功,后面两个匹配点同理。

$change

$fell()函数和$rose函数的结合体,当拍采样值与上一拍不一致则匹配成功,$change(a, clk1)还是看刚刚的波形:

6个时间点都是匹配点,不再赘述;

$stable

$stable和$change刚好相反,信号当拍采样值与上一拍一样则匹配成功,$stable(a)还是看刚刚的波形:

这个完全可以自己分析了,也不赘述;

$past

$past(exp1, num_ticks, exp2, clk),这个函数比较复杂一般要跟之后的蕴含算子一起使用(事实上之前讲的东西之后基本上都要和蕴含算子配合使用),这个函数的含义是:以clk为采样时钟,从当前时刻往前看,第num_ticks次exp2成立时exp的值,$past返回的是一个信号值。这一点我先不举栗子,后面的实际训练时候再看。

其他会用到的内建函数

$onehot(a):

任意时钟沿,表达式a都只有1bit为高;

$onehot0(a):

任意时钟沿,表达式a都只有1bit为高或这均为低电平;

$isunkown(a):

任意时钟沿,表达式a任意位是否有X态或者Z态,如果有则匹配,没有X态或Z态则报错;注意这个函数我们一般时~$isunkown(a)这样使用,在有X态或Z态时候我们希望报错出来;

$countones(a):

返回表达式中高电平的bit数量;

蕴含算子

前面的序列啊表达式啊在断言中大多都要配合蕴含算子使用,蕴含算子就类似我们代码中的if语句。蕴含算子只有两个:|->和|=>,二者区别也很简单。

a |-> b的含义是如果a事件发生,那么就从当拍(##0)开始,检查b事件是否会有效(匹配),如果b不能匹配则断言失败如果能匹配则成功。有点类似于if(a) then chec(b),这样写出来的断言就不是拍拍在检查了(当然了还是在拍拍匹配事件a,如果a能匹配上就会触发后续的断言检查),而是有条件的检查;比如说要检查ready信号有效的当拍vld信号必须要有效,那么就可以直接写ready |-> vld了。

a |=> b和前者唯一的区别就是,|=>是从下一拍(##1)开始检查,起始就是等同于a |-> ##1 b,我一般习惯使用|->。

其实只要简单的记住蕴含算子就是相当于if就好了,下面实际训练中我们会反复加深。

实操训练

其实还有一些语法我们没有讲,不过没关系我们一边练习一边继续拓展。需要注意的是断言有很多组织形式,我就选取最常用最简单的形式了。

练习1

题目:系统复位后,状态机信号status在任意一拍均有且只有1bit信号有效

分析:每一拍都要检查,因此不需要前置蕴含算子了;均有且只有1比特信号有效意味着我们需要使用系统函数$onehot();复位之后检查我们需要用到disable iff语句;

解答:

我们循序渐进,第一步先把需要的事件/序列写出来,这个我们只需要建立一个序列块(由于我本身极少这样使用,所sequence里面是不是必须加采样时钟我不是太清楚,加上应该是肯定没问题的);

sequence seq_onehot;
    @(posedge clk) $onehot(status);
endsequence

第二步建立事件块,一般由若干序列块+蕴含算子组织而成,不过我们这个简单只有一个序列(disable iff...表示当rst_n为时即~rst_n==1不进行检查);

property onehot_chk;
    @(posedge clk) disable iff(~rst_n)
        seq_onehot;
endproperty

第三步,把组织好事件块加入断言检查中;

assert_1: assert property(onehot_chk);

到这里这个断言就写完了,不过我们可以拓展一下下,比如我有很多个信号都要进行onehot检查,难道我还有一个一个这样写么?当然不必了,因为序列块和事件块都是可以传参的,例如可以写成下面这样;

sequence seq_onehot(chk_sig);
    @(posedge clk) $onehot(chk_sig);
endsequence

property onehot_chk(in_sig);
    @(posedge clk) disable iff(~rst_n)
        seq_onehot(in_sig);
endproperty

assert_1: assert property(onehot_chk(status1));
assert_2: assert property(onehot_chk(status2));
assert_3: assert property(onehot_chk(status3));
assert_4: assert property(onehot_chk(status4));

这样就便捷了很多,不过其实我最常写的是下面这样,而且不知道是vcs天然支持还是有工具支撑,assert property我在环境中也没怎么写过,对于这个存疑。

常用写法,后面都按这样写——

第一行声明property块的名称是onehot_chk;

第二行声明使用xxx_clk时钟的上升沿采样,当rst_n为时(即~st_n==1)不进行检查(disable);

第三行是断言的主体,也是我要检查的内容;

第四行是声明property结束;

property onehot_chk;
    @(posedge clk) disable iff(~rst_n)
        $onehot(status);
endproperty

练习2

题目:系统复位后,data_ff1信号在任何时刻均不能为X态或Z态;

分析:和上一个题目类似,可以直接调用系统函数,注意是不能为X态或Z态;

解答:

property x_chk;
    @(posedge clk) disable iff(~rst_n)
        ~$isunkonwn(data_ff1);
endproperty

练习3

题目:某一握手协议中规定,vld信号只能在ready信号有效期间有效;

分析:简单来说,就是任何一拍vld是有效的,那么ready信号也必须是有效的,因此我们可以使用蕴含算子|->在vld有效时候检查ready是否为有效态;

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        vld |-> ready;
endproperty

练习4

题目:接口vld有效时,data_id信号值必须处于合理的范围内(假设合理范围时‘h0000~'h7FFF);

分析:data_id信号值处于合理范围需要用到之前没提过的inside方法,他本身就是检查一个值是否处于之后的范围内;

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        vld |-> (data inside{['h0:'h7FFF]});
endproperty

练习5

题目:data_vld是一个检测上升沿的单拍脉冲信号,最多有效1拍就会拉低等待下次触发;

分析:其实就是说data_vld如果这一拍为1,那么下一拍一定为0,那么我们只需要在data_vld==1时检查下一拍是不是0就可以了,可以使用|=>也可以使用|-> ##1;

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        data_vld |=> ~data_vld;
endproperty
property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        data_vld |-> ##1 ~data_vld;
endproperty

 练习6

11

题目:单比特信号data_vld连续有效时间跨度最多为5拍;

分析:也就是说我们要检查vld信号每次为1应该持续[1:5]拍,那么不如我们先写一个简单的,如果规定vld有效必须为5拍,那么我们怎么写呢?这里肯定要用到$rose和$fell函数了,因为持续几拍肯定是从头数到尾我们需要上升沿和下降沿;持续几拍那就用到了我们连续重复了[*m:n],那我们先写一个看看;

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        $rose(data_vld) |-> data_vld[*5] ##0 $fell(data_vld);
endproperty

看上去挺合理的,data_vld有效开始检查,到data_vld无效时结束,在这期间data_vld连续有效了5拍,如果不是5拍就报错;

不过这样写是有问题的,问题请见下面的波形,这很明显是一个维持了5拍的信号对吧:

数字前端的功能验证利器——SVA断言学习笔记_第18张图片

注意开始检查的时刻是60ns,此时data_vld采样值为1与上一拍0满足了$rose,同时由于我们使用了|->符号单拍开始检查,所以data_vld在60ns出重复为1次;接下来80ns处连续重复了第2次,100ns处重复第3次,120ns处重复第4次,140ns处重复第5次,好的在140ns处data_vld[*5]匹配完成;接下来匹配##0 $fell(data_vld),##0含义为当拍就要看,那么当拍data_vld值为1上一拍也是1,$fell匹配失败,断言也就失败了;

因此这样的写法是不正确的,会把一个符合预期的行为误判为断言失败,所以我们修改一下成下面这样:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        $rose(data_vld) |-> data_vld[*5] ##1 $fell(data_vld);
endproperty

变成##1就好啦,在data_vld[*5]的下一拍来检查$fell,完美的解决了这个问题,这个用法一定要牢记。

再来思考一个问题,如果不用$rose(data_vld) |-> data_vld[*5] ##1 $fell(data_vld)而改成data_vld |-> data_vld[*5] ##1 $fell(data_vld)会有什么后果,反正都是再data_vld有效时候开始检查嘛;后果就是本来只在60ns处开始一条检查线程,现在会在60ns、80ns、100ns、120ns和140ns处开了多条检查线程,而后面这些线程显然是一定失败的也不是我们需要检查的。

对于这个题目其实还可以用intersect或者throughout来写,一会我们看过其他题目后再回头过来试一试;

那么回到题目本身,其实也就很好解决了,只需要把[*5]修改为[*1:5]就契合了题目要求;

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        $rose(data_vld) |-> data_vld[*1:5] ##1 $fell(data_vld);
endproperty

练习7

题目:req信号(单拍脉冲)有效之后(含当拍),ack信号有效之前(含当拍),必须要收到(出现)4次data_vld有效;

分析:这个题目和上一个题有点类似,我们来分析一下。这个断言应该在req有效当拍开始而在ack有效(第一次有效)时结束,我们关注的就是req->ack之间的事情,所以很显然前置算子就是req ->,后面怎么写呢?我们可以使用intersect+[->1]的句式,注意我们强调过intersect连接的两个序列必须等长,ok我先把答案写下来,再分析下。

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        req |-> data_vld[=4] intersect (ack[->1]);
endproperty

intesect连接的两个序列必须等长,那么后面的ack[->1]持续的时间段时哪里呢?从下图的波形中可以看出时从前置算子匹配成功的60ns到ack信号第一次为高有效的220ns处;因此通过这样的句式就成功的限制住了data_vld高有效4次这一事件必须也发生在60ns~220ns之间,即从req有效当拍到ack有效当拍;这个句式事实上时非常常见的写法。

数字前端的功能验证利器——SVA断言学习笔记_第19张图片

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        req |-> data_vld[=4] intersect (ack[->1]);
endproperty

练习8

题目:req信号(单拍脉冲)有效之后(不含当拍从下一拍开始),ack信号有效之前(不含当拍,ack有效之前),必须要收到(出现)4次data_vld有效;

分析:和上一个问题非常相似,我们继续来解决一下,其实核心好事data_vld[=4]这个序列发生在哪个时间段的问题。req的下一拍开始,那么我们可以使用|=>或者|->##1,这两个是都可以的;ack有效的前一拍data_vld[=4]要匹配完成,要怎么处理呢?可以采用这种形式(data_vld[=4] ##1 1’b1),由于1'b1是一个恒成立的序列,这样写的效果就是data_vld[=4]匹配成功点往后推迟了一拍;

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        req |-> ##1 (data_vld[=4] ##1 1'b1) intersect (ack[->1]);
endproperty

我们还是以波形来理解,先看一个成功的波形;

前置算子req在60ns匹配成功,|-> ##1意味着从下一拍开始进行后续的判定。黄色箭头220ns处ack[->1]匹配成功,这意味着(data_vld[=4] ##1 1’b1)序列必须在80ns~220ns间匹配完成,而在data_vld[=4]在200ns蓝色箭头处匹配成功,因此(data_vld[=4] ##1 1’b1)在220ns匹配成功,断言成功。

数字前端的功能验证利器——SVA断言学习笔记_第20张图片

下面看一个失败的例子,ack[->1]在220ns匹配成功,而(data_vld[=4] ##1 1’b1)在哪里匹配成功呢?在240ns处才匹配成功(换句话说,在220ns处刚刚匹配到data_vld[=3] ##1 1’b1成功),因此最后断言失败。

数字前端的功能验证利器——SVA断言学习笔记_第21张图片

再看一个略带迷惑性的例子,如下图这个行为最后断言是会报成功还是失败呢?我和同学有一些争论我会用波形再试验一下。不过我认为是会成功的,因为在220ns时(data_vld[=4] ##1 1’b1)报匹配成功,ack[->1]在220ns处报匹配成功,intersect前后序列等长断言匹配成功,检查也就此结束。不过你说在220ns处data_vld[=5]了,那没关系因为(data_vld[=5] ##1 1’b1)作为一个整体在240ns才匹配此时我的断言都已经检查完成了,等待下一次req有效才会开始下一次检查,因此对断言成功没有影响。

数字前端的功能验证利器——SVA断言学习笔记_第22张图片

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        req |-> ##1 (data_vld[=4] ##1 1'b1) intersect (ack[->1]);
endproperty

练习9

题目:继续是上一个题的变种,稍有变化。n_ready是维持信号,在n_ready有效期间,data_vld信号有效的次数应该小于等于4次,data_vld信号为离散信号;

分析:这也是实际中很常见的场景,当后级模块的n_ready信号拉起时,一般表示后级模块已经不能接收更过的数据了,这个时候作为前级模块应该尽快停止数据的发送,不过鉴于前级模块一般需要一些反应的时间因此后级模块允许再发送几个数据过去,在本题目中这个数量最大为4个,也就是说0~4个都是可以接收的;

注意n_ready是维持信号,因此我们选取前置算子时候要选取的是n_ready的上升沿$rose(n_ready);我们仍旧选择intersect+[->1]的组织形式,那么后面跟的应该是!n_ready[->1]即检查结束点为n_ready无效的时刻,因此也可以写成$fell(n_ready)[->1];因此初步组织断言为:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        $rose(n_ready) |-> (data_vld[=0:4]) intersect ($fell(n_ready)[->1]);
endproperty

不过还是老问题,这样写会发生误判下面这种情况,可以看到下图中n_ready有效期间data_vld有效了4拍这是完全符合要求的,不过会被上面断言误判为失败,原因还是在于intersect后面的$fell(n_ready)是在220ns处匹配完成匹配的而不是200ns,因此220ns处的data_vld有效也会被计入因此断言会判定data_vld[=5],导致断言失败。

数字前端的功能验证利器——SVA断言学习笔记_第23张图片

要避免这一个问题还是要用到上个练习的方法,引入##1 1'b1(还是直接理解成把序列匹配的时间点往后推一拍),这样在220ns处(data_vld[0:4] ##1 1'b1)处于匹配成功的时间点,整个断言判定成功。220ns处的data_vld就被这个##1 1'b1给推出去了。

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        $rose(n_ready) |-> (data_vld[=0:4] ##1 1'b1) intersect ($fell(n_ready)[->1]);
endproperty

练习10

题目:start信号(单拍脉冲信号)有效之后,over信号(单拍脉冲信号)有效之前(含当拍),start信号不能再次有效,如下图为合理情况的波形;

数字前端的功能验证利器——SVA断言学习笔记_第24张图片

分析:这其实也是系统中比较常见的一个场景,那么我们来组织一下断言;从前置算子入手,因为说了start是单拍脉冲信号所以前置算子可以直接写start了;后面要断的是什么呢?是在over[->1]之前start不能为1,所以可以使用intersect (over[->1])句式,写成~start intersect (over[->1]);我们观察下intersect前面是一个信号表达式,那么就跟前面我写的东西连上了,这里也可以用throughout来连接等长的表达式与序列;而要使用|->还是|=>来连接呢?必须使用|=>来连接的,因为你从不能在start有效的当拍断言检查start无效吧?!

解答:

property vld_chk;
    @(posedge clk) disable iff(~rst_n)
        start |-> ~start thoughout (over[->1]);
endproperty

练习10

题目:某个模块接收前级模块发送的请求req,请求以req_id作为标记,本模块以rep和rep_id作为响应信号表示已经接收到请求;请检查req和req_id有效后,最近的一拍rsp有效时rsp_id等于req_id;

数字前端的功能验证利器——SVA断言学习笔记_第25张图片

分析:这个问题中遇到一个难点,就是我要判断rsp_id是不是跟上一个req_id一样,那么我就必须想办法把reg_id保存下来;SVA提供了这样的方法,我们直接用就可以了,我们可以在断言中定义局部变量用来保存数据以备之后检查使用,注意这个局部变量只在断言期间有效,无法在断言外使用;确定一下前置算子,在什么情况下我们需要检查呢,就是在req有效后最近的那一拍rsp有效时,我们需要触发断言检查,“最近的那一拍”自然就时req后rsp第一次为1的时间点,因此使用req ##1 rsp[->1]来寻找这个时间点(这里我默认rsp至少比req晚一拍),前置算子有了自然就可以完整的组织断言,注意解答中的tmp_id就是断言中可以使用的临时变量;

解答:

property vld_chk;
    reg [7:0] tmp_id
    @(posedge clk) disable iff(~rst_n)
        (req,tmp_id = req_id) ##1 (rsp[->1]) |-> (rsp_id == tmp_id);
endproperty

练习11

题目:某个模块接收前级模块发送的请求req,请求以req_id作为标记,本模块以rep和rep_id作为响应信号表示已经接收到请求;请检查req有效后,下一次req有效时req_id为上一次req_id加1(0~FF~0循环发送id);

数字前端的功能验证利器——SVA断言学习笔记_第26张图片

分析:跟上一个题目几乎是一毛一样的,直接解答就好了;

解答:

property vld_chk;
    reg [7:0] tmp_id
    @(posedge clk) disable iff(~rst_n)
        (req,tmp_id = req_id) ##1 (req[->1]) |-> (req_id == tmp_id+1);
endproperty

练习12

题目:某个模块接收前级模块发送的请求req,请求以req_id作为标记,本模块以rep和rep_id作为响应信号表示已经接收到请求;某个id被本模块相应前,不应该再收到相同id的req请求;

数字前端的功能验证利器——SVA断言学习笔记_第27张图片

分析:不用分析了,使用intersect或者thoughout都可以;

解答:

property vld_chk;
    reg [7:0] tmp_id
    @(posedge clk) disable iff(~rst_n)
        (req,tmp_id = req_id) |=> ~(req && req_id==tmp_id) thoughout (rsp && rsp_id==tmp_id)[->1];
endproperty

练习13

题目:某模块与两个前级模块对接,前级模块1发送请求req1和req1_id,前级模块2发送请求req2和req2_id,当该模块在同一拍收到两个前级模块的请求且req_id一致时,需要保证首先对前级模块1进行响应;

数字前端的功能验证利器——SVA断言学习笔记_第28张图片

分析:经过了这么多题,还是直接分析解答吧,看看写的有没有错。。

解答:

property vld_chk;
    reg [7:0] tmp_id
    @(posedge clk) disable iff(~rst_n)
        (req1 & req2 & req1_id==req2_id, tmp_id = req1_id) |=> ~(req2 && req2_id==tmp_id) thoughout (rsp1 && rsp1_id==tmp_id)[->1];
endproperty

练习14

题目:在对SDRAM进行数据读取,如果在第1拍发起读操作,需要在第4拍得到读取数据;如果在这4拍内有对相同地址的写入操作,那么我们需要使用最新的写入值作为读操作的返回值(也就是常说的mem读取hazard处理);举例如下图,60ns发起读操作120ns返回数据(均以上升沿实际采样为准),在这期间如果没有对同一地址的写入操作,那么将返回从sdram中读取的数据,如果有的话如下图60ns和100ns时均有对同一地址的写入操作,那么我们必然应该回读最新的写入值即100ns时刻写入的20;注意由于要求hazard 4拍,那么在60ns~120ns期间发生的同一地址写操作均需要检查进去;用断言检查RTL hazard行为的正确性;

数字前端的功能验证利器——SVA断言学习笔记_第29张图片

分析:这个问题是我学到的最难的断言了,看起来简单做起来完全没有思绪的那一种。。。所以我觉得有必要掰开揉碎的说一下,不能直接上答案的。直接来思考有点转不过弯来,不如我们分四种场景来检查先:对第0拍继续hazard、对第1拍继续hazard、对第2拍继续hazard和对第3拍继续hazard。

1.什么时候我们需要对第0拍继续hazard呢?条件就是第0拍有对同一地址的写入,且之后的3拍没有同一地址的写入,那么读回数据必须是第0拍写入的数据,波形示意如下:

数字前端的功能验证利器——SVA断言学习笔记_第30张图片

根据描述我们可以写出断言如下,结合波形具体做一下解释;rd_en有效即对应波形中的60ns时刻,由于只对同一地址进行hazard因此我们需要记录一下地址(当拍的这种其实不记录也行,主要为了和后面一致);##0表示当拍,当拍有wr_en&wr_addr=tmp_addr的话说明这个数据我们有可能进行hazard所以也得保留下来;要继续满足什么条件呢,要满足从下一拍开始连续3拍不能有wr_en&wr_addr=tmp_addr这个事出现,否则我们就要hazard后面最新的一拍嘛,因此写上##1 ~(wr_en & wr_addr==tmp_addr)[*3],注意因为##1和[*3]的作用,时间点已经来到了120ns;如果前置算子的条件满足了,那么我们只需要在120ns当拍检查回读数据rd_data==tmp_data是否成功就可以了,tmp_data就是我们之前记录的第0拍的同一地址写入数据嘛;好的因此第一种情况我们写出来了,这个断言是必须要满足的。

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##0 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*3] |-> rd_data==tmp_data;
endproperty

2.什么时候我们需要对第1拍继续hazard呢?条件就是第1拍有对同一地址的写入(显然第0拍有没有已经不重要了或者说不必关心了),且之后的2拍没有同一地址的写入,那么读回数据必须是第1拍写入的数据,波形示意如下:

数字前端的功能验证利器——SVA断言学习笔记_第31张图片

rd_en有效即对应波形中的60ns时刻,由于只对同一地址进行hazard因此我们需要记录一下地址,下一拍有wr_en&wr_addr=tmp_addr的话说明这个数据我们有可能进行hazard所以保留下来;要继续满足从下一拍开始连续2拍不能有wr_en&wr_addr=tmp_addr这个事出现,因此写上##1 ~(wr_en & wr_addr==tmp_addr)[*2],时间点已经来到了120ns;如果前置算子的条件满足了,那么我们只需要在120ns当拍检查回读数据rd_data==tmp_data是否成功就可以了,这个断言是必须要满足的。

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##1 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*2] |-> rd_data==tmp_data;
endproperty

3.什么时候我们需要对第2拍继续hazard呢?条件就是第2拍有对同一地址的写入(显然第0、1拍有没有已经不重要了),且之后的1拍没有同一地址的写入,那么读回数据必须是第1拍写入的数据,波形示意如下:

数字前端的功能验证利器——SVA断言学习笔记_第32张图片

不多分析了,直接写

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##2 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*1] |-> rd_data==tmp_data;
endproperty

4.什么时候我们需要对第3拍继续hazard呢?条件就是第3拍有对同一地址的写入(不需要任何其他条件了)读回数据必须是第3拍写入的数据,波形示意如下:

数字前端的功能验证利器——SVA断言学习笔记_第33张图片

那么可以直接写出断言了:

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##3 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) |-> rd_data==tmp_data;
endproperty

不过我们观察了一下,这个形式跟之前的好像有点不太和谐,没关系可以修理一下,这里我们使用一个特殊重复算子[*0],他的作用是什么呢?是向前吃掉一拍的操作,举几个例子感受下:

req_a ##N req_b[*0] 等价于req_a ##N-1 req_b;

req_a ##1 req_b[*0] 等价于req_a;

req_a[*0] ##N req_b 等价于 ##N-1 req_b;

req_a[*0] ##1 req_b 等价于 req_b;

具体可以再查查书,先说这么多吧;通过这个特殊算子,我们可以改写如下([*0]吃掉了##1 ~(wr_en & wr_addr==tmp_addr)):

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##3 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*0] |-> rd_data==tmp_data;
endproperty

好的,四种情景我们已经写好了,下面把他们放在一起来观察下,仿佛规律套路很深啊!观察了一个小时后我们发现,里面除了数字不一样其他的根本就是完全一样的嘛!

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##0 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*3] |-> rd_data==tmp_data;
endproperty
property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##1 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*2] |-> rd_data==tmp_data;
endproperty
property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##2 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*1] |-> rd_data==tmp_data;
endproperty
property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##3 (wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*0] |-> rd_data==tmp_data;
endproperty

既然我们观察好了,那么就试着把他们合到一起吧,合到一起是什么样子呢?

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##0
        ((##[0:3] wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*0:3])) |-> rd_data==tmp_data;
endproperty

就是这样对不对?!可是我们观察下上面这个断言是有明显问题的,我们希望的是当前面取##0后面取[*3],前面取##1后面取[*2],前面取##2后面取[*1],前面取##3后面取[*0]对吧!那么我们要怎么办呢?是时候再把最好用的intersect拿出来了,记得intersect的作用吧,必须前后的序列等长才会匹配成功,我们想下前面取##0后面取[*3]这个序列长度是多少呢?是4拍长度对吧(别忘了里面还插着要给##1呢哈);以此类推,是不是每种匹配的情况都是4拍长度!所以我们只要约束((##[0:3] wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*0:3]))这个事在4拍内匹配就行了呀,怎么约束呢?使用intersect ##3 1‘b1,注意真的不是##4,看一下下面的波形就更清楚了,60ns再##3 1‘b1是在什么时候匹配的,是不是在120ns处!所以这样一约束,就使得intersect之前的序列在4拍之内匹配成功才会触发断言的|->检查了;

这个技巧真的很好用,望掌握(话说回来,这个看不懂也没关系,太难了我觉得)。

数字前端的功能验证利器——SVA断言学习笔记_第34张图片

解答:

property vld_chk;
    reg [31:0] tmp_data;
    reg [31:0] tmp_addr;
    @(posedge clk) disable iff(~rst_n)
        (rd_en, tmp_addr=rd_addr) ##0
        (((##[0:3] wr_en & wr_addr==tmp_addr, tmp_data=wr_data) ##1 ~(wr_en & wr_addr==tmp_addr)[*0:3])) intersect ##3 1'b1) |-> rd_data==tmp_data;
endproperty

 

你可能感兴趣的:(断言,IC验证)