ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

掌握SVA六大序列操作符:从[=]到first_match的细节与陷阱

掌握SVA六大序列操作符:从[=]到first_match的细节与陷阱 写SVA断言这几年我最深的体会是序列操作符看着就几个关键词真正用起来全是细节。尤其是[]、throughout、within、intersect、first_match、ended这六个几乎每个项目都会碰到但大多数人只停留在见过语法的层面。去年评审一个同事的断言他用ack[-2]表达第二次ack后下一拍要产生done结果仿真里偶尔提前一拍通过排查半天才发现应该用[2]。这种问题不报错、不难查但足以让你对SVA的信任打折扣。所以这篇文章不打算铺开讲SVA全部语法就聚焦这六个序列操作符把这个序列端点的底层模型、每个操作符的语义边界、以及它们组合使用的完整案例一次说清楚。适合刚接手验证、正在写断言的新人也适合被多匹配、窗口边界这类问题折磨过的老手。1. 先把序列端点模型讲透所有操作符都是围绕匹配与时刻运转的1.1 SVA序列匹配不是在看信号跳变而是在标记时间点很多人写SVA序列时脑子里想的是信号是不是高了信号是不是变了这没错但一旦进入序列操作符这个思维就不够用了。SVA序列的匹配本质上是在一段时间轴上标记出一个区间一个起点采样点和一个终点采样点。举个例子a ##1 b这条序列。它在a为真的那个时钟沿开始然后在下一个时钟沿检查b。如果b也为真那么这条序列的匹配就算成立。这里的起点是a为真的那个采样点终点是b为真的那个采样点。中间隔了几个周期、a期间有没有变化都不重要重要的是起点和终点各自落在哪个时钟沿。当你开始写[]、within、intersect这些操作符时真正在调整的就是起点和终点。$rose(req) |- (req throughout (ack[-1]))这句断言如果弄不清ack[-1]的终点到底在哪一拍throughout的区间范围就是一笔糊涂账。所以先把这个模型立起来后面的操作符才能真正看懂。1.2 六个操作符在时间轴上各管一段我用一张表把这六个操作符对起点和终点的影响先列出来后面再逐个展开。操作符对起点的约束对终点的约束核心语义throughout起点由右侧序列决定终点由右侧序列决定左侧表达式在区间内全程为真全程条件约束within左侧序列起点不能早于右侧序列起点左侧序列终点不能晚于右侧序列终点窗口包含intersect左右序列起点必须相同左右序列终点必须相同强制时间对齐first_match保留同一起点下最早结束的匹配剪掉后续匹配多匹配剪枝ended把序列匹配结束点转成布尔值结束点那一刻变为1跨序列同步[]起点由前置上下文决定第n次事件后的下一个采样周期结束非连续恰好n次这张表是我自己总结的不是LRM原文但用来指导写断言和调试非常有效。你只要记住一句话序列操作符改的是时间轴上的起点和终点调试断言时第一件事就是定位这两个点在哪一拍。2. []和[-]的纠缠非连续重复的两种收尾方式2.1 先把连续重复[*]作为基线要理解[]和[-]绕不开[*]这个基础操作符。a[*3]表示a连续三个时钟周期都为真第三个周期结束时序列匹配。它是最直观的重复缺点是太死板不允许中间有间隔。实际协议里很多事件不是连续发生的。比如中断确认信号可能隔几个周期才来一次总线上的一次burst传输数据有效信号也可能中间拉低几拍。这个时候[*]就没法用了得靠[-]和[]这种非连续重复。2.2 [-]goto重复最后一次事件当拍收尾a[-2]的语义是a非连续出现2次第2次出现的那一拍序列立即匹配结束。这个立即是关键。它把终点钉在第2次a为高的那个采样点上。这种特性在检测事件第n次发生时同拍必须有什么动作的场景下很合适。我经常这样写property p_flag_on_second_event; (posedge clk) $rose(start) |- (event[-2]) ##0 flag; endproperty##0 flag表示和event[-2]的匹配终点同一拍检查flag。如果协议要求的是第2次event出现时flag必须同时为高这个写法就是对的。这里##0的语义是组合连接不是延迟一拍很多人第一次看到会懵。2.3 []非连续重复最后一拍后还要再等一个周期收尾a[2]和a[-2]的差别在工程上可以理解为a[2]的匹配终点比a[-2]晚一个周期。也就是说a[2]匹配结束于第2次a为高之后的下一个采样周期。所以第2次事件发生后下一拍必须产生done用[]写起来更直接property p_done_after_two_events; (posedge clk) $rose(start) |- (event[2]) ##1 done; endproperty注意看##1 done是从event[2]的匹配终点继续往后数一拍。由于[2]的终点已经比[-2]晚了一拍这个##1 done实际上检查的是第2次事件后的第2拍第2次事件后的下一拍已经包含在[2]的收尾里了。这正是最容易出错的地方想表达第2次后的下一拍用[-2] ##1 done和用[2] ##0 done都能达到目的但如果你用[2] ##1 done检查点就整体后移了一拍。2.4 实战中一个很反直觉的行为上面说[]比[-]晚一拍收尾但实际项目里还有个更隐蔽的问题如果第2次事件之后紧接着又来了第3次事件[-2]和[2]的行为完全不同。[-2]的匹配点钉在第2次事件上之后再来多少次都不影响它的匹配结果。[2]就不一样了它会继续观望如果第2次事件后的下一拍事件仍然为高那这个多出来的一次会让[2]的匹配点继续向后延。我把这个案例写出来你就明白了property p_irq_ack; (posedge clk) $rose(irq) |- (irq_ack[2]) ##1 irq_done; endproperty如果irq_ack在第2次拉高后的下一拍又拉高了第3次[2]的匹配点不会停在第2次那一拍而是会延到第3次之后才收尾。于是##1 irq_done的检查点跟着往后移原来设计的第2次后下一拍必须来done就形同虚设了。遇到这种情况要么改用[-1]当拍收尾要么在[]的基础上显式约束额外周期的irq_ack为假。这也是为什么我建议**如果你的协议描述是恰好n次事件且事件不能在最后一次后继续出现[]语义更接近如果只是要第n次事件发生这个事实[-]更安全。**不要想当然认为[]和[-]只差一拍它们的匹配行为在连续事件出现时会有实质性差异。3. throughout与within一个管全程一个管窗口3.1 throughout在整个序列区间内某个条件必须一直成立throughout的用法是条件 throughout 序列含义是右侧序列匹配的整个区间内左侧条件必须在每一个采样周期都为真。这个操作符在请求保持类断言里几乎是标配。举个例子FIFO写使能拉高后必须连续保持3拍且这3拍内fifo_full必须为0property p_no_full_during_write; (posedge clk) disable iff (!rst_n) $rose(wr_en) |- (fifo_full 0) throughout (wr_en[*3]); endproperty$rose(wr_en)作为前件匹配结束点就是wr_en上升沿那个采样点。蕴含右侧的throughout序列从同一个采样点开始这里我强调的是SVA蕴含语义中前件匹配结束点就是后继序列的起点。wr_en[*3]连续3拍为高这3拍里fifo_full都必须是0缺一拍整个断言就fail。这里我踩过的一个坑是用throughout时左侧写成了$rose(fifo_full 0)之类的边沿表达式。throughout要求的是全程为真边沿表达式只在某一拍为真逻辑上天然不可能满足。所以throughout左侧一般放电平类条件不要放边沿检测。3.2 within左侧序列的匹配区间必须落在右侧序列的窗口内within的用法是序列1 within 序列2表示序列1的匹配区间必须完整落在序列2的匹配区间之内。它不要求两侧长度相等只要求包含关系序列1的起点不早于序列2的起点序列1的终点不晚于序列2的终点。典型场景是事件必须发生在某个窗口内。比如rd_done脉冲必须出现在rd_window连续为高2到4拍的窗口里property p_rd_done_in_window; (posedge clk) disable iff (!rst_n) $rose(rd_start) |- (rd_done[-1]) within (rd_window[*2:4]); endproperty这个断言展开看rd_done非连续出现一次即拉高那一拍匹配结束这个匹配点必须落在rd_window连续为高2到4拍构成的窗口区间内。rd_done可以比rd_window早结束但不能晚于窗口结束。这里有个边界要特别注意within是允许左侧序列和右侧序列终点重合的。rd_window连续4拍rd_done在窗口最后一拍出现完全合法。很多工程师想当然地认为within必须是严格内部、不能触碰边界结果把约束写宽了或者写窄了调试时一脸懵。实际写断言时如果对边界有特殊要求一定要在注释里标明否则后来接手的人很容易理解错。3.3 合起来用窗口内全程稳定的三层组合throughout和within经常要配合使用一个管窗口范围一个管窗口内的全程条件。比如总线burst场景片选cs_n拉低期间数据有效信号data_valid必须出现连续两拍的有效窗口且这个窗口必须落在burst_window连续4拍窗口内整个过程中cs_n必须一直为低。property p_burst_data; (posedge clk) $rose(burst_start) |- (cs_n 0) throughout ((data_valid[*2]) within (burst_window[*4])); endproperty这里throughout的右侧是(data_valid[*2]) within (burst_window[*4])也就是说throughout覆盖的是data_valid连续两拍匹配且落在burst_window内的整个时间段。这个组合看起来复杂但拆开后每一层都清晰最外层是全程条件中间层是within窗口最内层是事件本身的长度。我建议你在写这种组合断言时先在纸上画出时间轴标出起点、终点、条件翻转点再落代码。不要直接在编辑器里堆很容易把自己绕晕。4. intersect起点和终点必须同时刻对齐的强约束4.1 intersect的核心语义不是都发生而是同时合流intersect是SVA序列操作符里约束最严格的一个。它的语义是两个序列必须从同一个时钟沿开始匹配并且必须在同一个时钟沿匹配结束。换句话说两个序列的起点相同、终点也相同。这和大多数人第一印象里的两个条件都满足完全不是一回事。seq1 and seq2才是两个序列都发生允许终点不同以较晚结束的那个终点作为整个组合的终点。而intersect要求两个序列像两条河一样汇合必须同一时刻合流。正因为这个特性intersect经常用来检查两个并行事件的完成信号是否对齐。模块A和模块B各自完成标志连续两拍拉高且必须同拍开始、同拍结束property p_chan_done_align; (posedge clk) disable iff (!rst_n) $rose(start) |- ##[1:3] (chan1_done[*2]) intersect (chan2_done[*2]); endproperty##[1:3]先给两个通道1到3拍的启动时间窗口窗口内的某一拍作为两个序列的共同起点。如果chan1_done和chan2_done都在这一拍开始连续两拍为高属性通过如果chan2_done比chan1_done晚了一拍才拉高intersect直接判定起点不一致属性失败。4.2 长度不同intersect必然失败intersect对两个序列的长度有硬性要求。chan1_done[*2] intersect chan2_done[*3]这种写法在两个序列从同一拍开始的前提下第一个序列第2拍就结束了第二个序列还要等第3拍才结束终点对不齐永远匹配不上。这个特性有时候能当长度匹配的检查用。我想检查一个请求信号req到ack之间的延时必须是固定的两拍可以直接写property p_req_ack_fixed_delay; (posedge clk) $rose(req) |- (req[*2]) intersect (##[1:3] ack[-1]); endproperty等一下这个写法其实有问题req[*2]是连续两拍req为高而##[1:3] ack[-1]是延迟1到3拍后ack出现一次。如果ack在第1拍出现##[1:3] ack[-1]匹配长度是2拍延迟1拍匹配1拍刚好和req[*2]对上如果ack在第2拍出现匹配长度变成3拍对不上intersect就失败。所以这个断言确实能约束ack必须出现在延迟1拍后的那一拍也就是固定延时关系。这种写法比较取巧不是所有人都能一眼看懂但实际验证中很实用。4.3 常见误用拿intersect当and用我在代码评审里见过好几次这种情况工程师想检查两个条件都满足写成了a[*2] intersect b[*3]然后发现断言总是失败怎么也想不通。问题就在intersect要求长度一致而他真正想要的是两个信号最终都出现就行。这种场景应该用and或者直接写a[*2] ##0 b[*3]这类组合。所以我的建议是先问自己两个序列的终点需不需要严格对齐。需要用intersect不需要用and或其它逻辑组合。intersect是种强约束用对地方很漂亮用错地方就是给自己挖坑。5. first_match与ended处理多匹配和跨序列同步的关键5.1 first_match给可变延迟产生的多匹配剪枝SVA里面有个让新手很头疼的问题一个序列可能从同一个起点产生多个匹配。最典型的就是可变延迟序列比如##[1:$] b。b可以在第1拍、第2拍、第3拍、甚至更远的将来为高每一种情况都构成一个匹配这样从同一个起点出发就有无限多个匹配端点。如果在属性里直接使用这种序列而不做处理很多工具会报multiple matches警告更糟的是属性会在多个匹配点重复生效断言行为和你预期完全对不上。first_match就是干这个的它只保留每个起点的最早匹配端点后面的全部剪掉。看这个例子property p_after_first_match; (posedge clk) $rose(a) |- first_match(##[1:$] b) ##1 c; endproperty如果b在第2拍、第4拍、第7拍都为高##[1:$] b会产生多个匹配端点。加上first_match后只保留第2拍这个最早匹配点然后##1 c检查第3拍的c。不加first_match的话属性会在第2、4、7拍各自派生出一次检查c的计算点完全不同断言结果自然不可控。在写协议类断言时还有一种常见写法是用or构造多个候选序列first_match((req[-2]) or (req[-3]))这个的意思是req要么非连续两次出现要么非连续三次出现以先发生的那个匹配为准。用first_match包一下避免or两个分支都产生匹配时属性被重复触发。5.2 ended把序列的结束点变成布尔值ended解决的是另一个问题你定义了一个序列但你想在另一个属性里引用这个序列结束的那一刻。SVA允许用sequence_name.ended把这个结束点转成一个布尔值在那个时钟沿为1。举个例子请求后延迟1到3拍ack拉高把这个交互定义成一个序列s_reqsequence s_req; req ##[1:3] ack; endsequence property p_req_ack_then_data; (posedge clk) s_req.ended |- ##[1:2] data_valid; endpropertyreq ##[1:3] ack可能有多个匹配点ack可能在第1、2、3拍出现但这里直接写s_req.ended而不加first_match大多数工具默认会按多匹配处理或告警。稳妥的做法是配合first_match使用first_match(s_req).ended这个组合我很常用。它的语义是保留s_req最早的匹配结束点然后把结束时刻变成布尔值供后面的蕴含或序列使用。用这个写法重写上面的属性property p_req_ack_then_data; (posedge clk) first_match(s_req).ended |- ##[1:2] data_valid; endproperty这样语义就完全确定了ack最早出现的那次算数属性从那一拍往后检查data_valid。5.3 关于ended的三个常见误解第一seq.ended是布尔值不是序列。它不能放在需要序列操作数的位置比如不能写成seq.ended ##1 xxx这种形式虽然布尔值在某些上下文里可以隐式转为单位序列但这里明确讲seq.ended作为一级表达式用于属性前件、蕴含右侧的布尔组合是最稳的。第二seq.ended在属性中做前件时它的真值只在seq匹配结束的那一拍为1其它情况为0。如果你把它写在蕴含前件里属性只在seq匹配结束的那个时钟沿被触发。想表达seq结束后后续必须发生什么这个思路是对的。第三seq.ended和自己写一遍同样的断言并不完全等价。$rose(a) |- b检查的是a上升沿之后的一个周期窗口s_seq.ended |- b检查的则是s_seq匹配结束后的窗口。s_seq可能跨多个周期也可能涉及复杂交互可读性和复用性都更好。项目里定义好公共序列再用.ended组合出新的断言维护成本低很多。6. 一个接近真实项目的断言集把六种操作符串起来用6.1 场景定义简化FIFO读写接口前面把每个操作符分开讲了下面用一个综合场景把它们串起来。假设一个简化FIFO接口信号定义如下信号方向含义clk, rst_ninput时钟、异步复位wr_eninput写使能fifo_fullinputFIFO满标志rd_startinput读事务启动脉冲rd_eninput读使能允许间断rd_doneoutput读完成脉冲rd_windowoutput读窗口指示连续多拍为高chan1_done, chan2_doneoutput两个并行通道完成标志协议要求这么几条写使能拉高后必须保持至少3拍期间fifo_full不能为高。读事务启动后rd_en非连续出现2次第2次之后的下一拍必须产生rd_done。rd_done必须落在rd_window连续为高2到4拍的窗口内。两个并行通道的完成标志必须连续两拍同拍出现、同拍结束。如果rd_en的匹配存在多个可能完成点只取第一个完成点并在其下一拍检查complete_indicator。6.2 逐条断言实现第一条用throughoutproperty p_no_full_during_write; (posedge clk) disable iff (!rst_n) $rose(wr_en) |- (fifo_full 0) throughout (wr_en[*3]); endproperty第二条用[]property p_rd_done_after_two; (posedge clk) disable iff (!rst_n) $rose(rd_start) |- (rd_en[2]) ##1 rd_done; endproperty第三条用withinproperty p_rd_done_in_window; (posedge clk) disable iff (!rst_n) $rose(rd_start) |- (rd_done[-1]) within (rd_window[*2:4]); endproperty第四条用intersectproperty p_chan_done_align; (posedge clk) disable iff (!rst_n) $rose(rd_start) |- ##[1:3] (chan1_done[*2]) intersect (chan2_done[*2]); endproperty第五条用first_match和ended组合property p_evt_first_complete; (posedge clk) disable iff (!rst_n) $rose(rd_start) |- first_match(rd_en[-2] ##[1:$] rd_flag).ended ##1 complete_indicator; endproperty6.3 调试这些断言时我常用的排查思路属性写出来只是第一步仿真失败后怎么定位才是真本事。根据这几个操作符的特点我的排查顺序基本是固定的。先看断言窗口的起点在哪一拍。SVA工具在波形里通常会把断言匹配的起点、终点用窗口标出来。起点不对优先怀疑是蕴含前件写错了或者##[1:3]这类前置延迟没生效。再看终点。终点对应失败逐个操作符对号入座throughout失败去看它左侧的条件在哪一拍变假了。变假的点通常就是bug根因所在。within失败去看左侧序列的结束点是不是落在了右侧窗口外面。我记得有次调了很久最后发现rd_done的结束点刚好比rd_window的结束点晚了一拍确认是窗口开小了。intersect失败看两个序列的起点差了几拍、终点差了几拍。如果起点相同但终点不同大概率是两侧序列长度本身就不一致。[]相关的失败重点看最后一次事件之后有没有多余的事件出现导致匹配点被整体拖后。这一点用[-]替代往往立刻见效。first_match相关的问题一般不是fail而是断言不触发或者触发次数比预期多。这时候要回过去看是不是没有包first_match导致工具在多匹配点重复评估。调试工具方面VCS里开-assert report能输出每个断言的命中次数和失败次数QuestaSim里可以用assertion窗口看对象的pass/fail历史。但这些都比不上先把操作符的时间语义吃透。波形里手动数周期永远是验证工程师最可靠的兜底手段。写完这五条断言再回头看整篇的内容其实核心就一句话SVA序列操作符不是语法糖它们每个都在精确地修改时间轴上的起点和终点。写断言之前先问自己三个问题序列的起点在哪终点在哪中间的条件覆盖了哪些周期这三个问题想清楚[]还是[-]、within还是intersect、要不要first_match答案自然就出来了。
返回列表