断言与随机激励协同调试实战 - 第3节
并发断言基础:让验证“活”起来
并发断言,说白了就是给设计装上一双“实时监控的眼睛”。它不像即时断言那样只检查某个瞬间,而是盯着信号的变化过程,持续地、周期性地做检查。我个人觉得,这是SystemVerilog验证里最强大的武器之一。
我记得刚入行时,前辈跟我说:“断言不是用来找bug的,是用来证明设计没bug的。”当时不太理解,后来做多了才明白——并发断言就像设计规范的可执行文档,你写清楚了,工具就帮你盯着。
并发断言 vs 即时断言
先看个简单对比:
| 特性 | 并发断言 | 即时断言 |
|---|---|---|
| 执行时机 | 每个时钟沿采样 | 执行到该语句时 |
| 书写位置 | module、interface、program | always块或initial块中 |
| 时序依赖 | 必须有时钟和复位 | 不需要时钟 |
| 典型用途 | 协议检查、时序约束 | 数据一致性检查 |
嗯,这里要注意:并发断言必须有时钟。没有时钟,它就像没电的闹钟——动不了。
序列(sequence):断言的“基本词汇”
序列是什么?说白了就是一组信号事件的组合。你可以把它想象成“信号跳舞的编排”——先这样,再那样,最后那样。
基本序列写法
// 最简单的序列:a信号在时钟上升沿为高
sequence s_a_high;
@(posedge clk) a;
endsequence
// 带延迟的序列:a为高后,过2个时钟b为高
sequence s_a_then_b;
@(posedge clk) a ##2 b;
endsequence
// 重叠序列:a为高时,b也为高
sequence s_a_and_b;
@(posedge clk) a && b;
endsequence
我在项目中遇到过一个问题:新手写序列时,总忘记加时钟。结果仿真跑了一天,断言一个都没触发。排查了半天,发现所有序列都缺了@(posedge clk)。从那以后,我习惯把时钟写在序列外面,用property统一控制。
序列操作符
这里有几个常用的操作符,我列个表方便你查阅:
| 操作符 | 含义 | 示例 |
|---|---|---|
| ##n | 延迟n个时钟周期 | a ##2 b(a后2个时钟b成立) |
| ##[m:n] | 延迟范围 | a ##[1:3] b(a后1~3个时钟b成立) |
| [*n] | 重复n次 | a[*3](a连续3个时钟为高) |
| [->n] | 非连续重复 | a[->2](a出现2次,不要求连续) |
| and | 两个序列同时结束 | s1 and s2 |
| or | 任一序列结束 | s1 or s2 |
| intersect | 两个序列同时开始且同时结束 | s1 intersect s2 |
属性(property):断言的“完整句子”
序列是词汇,属性就是句子。属性把序列放到一个完整的检查上下文中,加上时钟、复位、使能等控制条件。
属性基本结构
property p_check_req_ack;
@(posedge clk)
disable iff (!rst_n)
req |=> ##[1:3] ack;
endproperty
assert property (p_check_req_ack);
这个属性的意思是:在时钟上升沿,如果复位有效则禁用检查;否则,当req为高时,接下来1到3个时钟内ack必须为高。
为什么会用|=>而不是##1?|=> 表示“紧接着的下一个时钟”,而##1是“延迟1个时钟”。区别在于:|=> 隐含了“当前时钟采样,下一个时钟检查”的语义。我个人习惯在协议检查中用|=> ,因为它更符合“请求-应答”的时序关系。
属性中的常用表达式
- 蕴含操作符:
a |-> b:如果a成立,则在同一时钟b必须成立a |=> b:如果a成立,则在下一个时钟b必须成立a |=> ##[1:3] b:a成立后,1~3个时钟内b必须成立
- disable iff:条件禁用,通常用于复位
- ended:判断序列是否结束
@(posedge clk) (a & b) |-> c,结果因为a和b的毛刺导致误触发。后来我改成$rose(a) && b,只检测上升沿,问题就解决了。
断言时钟与采样:时机决定一切
并发断言的核心是“采样”。你想想看,如果采样点选错了,断言的结果就全错了。这就像拍照——快门按早了或晚了,拍出来的都不是你要的画面。
采样机制
SystemVerilog断言默认在时钟上升沿的“前一刻”采样信号值。为什么?因为要避免竞争。如果时钟沿同时改变信号,断言可能采到旧值或新值,结果不确定。
// 默认采样:时钟上升沿之前的值
property p_default_sample;
@(posedge clk) a |-> b;
endproperty
// 显式采样:使用$sampled函数
property p_explicit_sample;
@(posedge clk) $sampled(a) |-> $sampled(b);
endproperty
嗯,这里要注意:$sampled和直接写信号名在大多数情况下等价。但如果你在同一个时钟沿既有赋值又有断言,用$sampled更安全。
时钟选择的陷阱
我见过一个案例:团队用了一个分频时钟做断言时钟。结果分频时钟有毛刺,断言频繁误报。排查了三天,最后发现是时钟域的问题。
- 尽量用主时钟,不要用门控时钟
- 跨时钟域信号要用同步器,断言时钟选目的时钟域
- 复位信号不要用做断言时钟
多时钟域断言
有时候一个属性需要跨两个时钟域。比如:
property p_cross_clock;
@(posedge clk_a) a |=> @(posedge clk_b) b;
endproperty
这种写法虽然语法允许,但我建议慎用。跨时钟域的时序关系很难保证,容易产生误报。我在项目中遇到这种情况,通常的做法是:先把信号同步到同一个时钟域,再做断言。
知识体系总览
下面这张图概括了并发断言的核心逻辑:
这张图展示了并发断言的三层结构:底层是基础概念,中间是核心要素,顶层是实战应用。你写断言时,心里要有这个框架——先确定时钟,再写序列,最后封装成属性。
好了,并发断言的基础就聊到这里。记住三个关键词:时钟、序列、属性。把这三个东西搞清楚了,剩下的就是多练、多踩坑、多总结。