3. 并发断言:并发断言基础、并发断言语法、并发断言与时钟的关系

好,咱们进入断言的重头戏——并发断言。

说实话,我刚接触UVM那会儿,觉得并发断言挺吓人的。又是时钟又是序列的,看着就头大。但后来我发现,只要你理解了它的核心思想,这东西其实比想象中简单。

并发断言,说白了就是在时钟沿上检查信号的行为。它不像立即断言那样,信号一变就触发。它得等时钟沿来了,才去采样、判断。嗯,这一点非常关键。

3.1 并发断言基础

先说说并发断言长什么样。我习惯把它理解成一种“带时钟的表达式”。

举个例子:

property p_req_ack;
    @(posedge clk) req |=> ##[1:3] ack;
endproperty

a_req_ack: assert property(p_req_ack);

你看,这里有个 @(posedge clk)。这就是并发断言和立即断言最大的区别——它依赖时钟。

我在项目中遇到过一种情况:有人把并发断言写成了立即断言的样子,结果仿真跑起来,断言要么不触发,要么乱报。后来一查,就是少了时钟沿的触发条件。

核心要点:并发断言必须在时钟沿上求值。没有时钟,就没有并发断言。

3.2 并发断言语法

并发断言的语法,其实就三块:

  • 序列(sequence):描述信号在时间上的变化
  • 属性(property):把序列包装成可验证的规则
  • 断言语句:assert / cover / assume

咱们一个一个说。

3.2.1 序列(sequence)

序列是并发断言的基础。它描述的是“信号A在某个时刻为高,紧接着信号B在下一个时钟为高”这种时序关系。

sequence s_req_ack;
    req ##1 ack;
endsequence

这里的 ##1 表示延迟一个时钟周期。你想想看,这比用Verilog写状态机去检查时序,是不是简洁多了?

常用的操作符有这些:

操作符 含义 示例
##n 延迟n个时钟周期 a ##2 b
##[m:n] 延迟范围 a ##[1:3] b
|=> 非重叠蕴含(下一拍) a |=> b
|=> 重叠蕴含(当前拍) a |-> b
and 两个序列同时结束 s1 and s2
or 任一序列结束 s1 or s2
intersect 两个序列长度相同且同时结束 s1 intersect s2

我的小技巧:写序列时,先从简单的单拍检查开始。比如先检查req拉高,再检查ack拉高。等这两个都对了,再加延迟和范围。别一上来就写复杂的嵌套序列,容易把自己绕进去。

3.2.2 属性(property)

属性是序列的“升级版”。它可以在序列的基础上加条件、加时钟、加使能。

property p_req_ack_with_enable;
    @(posedge clk) disable iff (!rst_n)
    enable |-> req ##[1:3] ack;
endproperty

这里我加了两个东西:

  • disable iff (!rst_n):复位时禁用断言。这个我强烈建议加上,不然复位期间断言乱报,你查都查不过来。
  • enable |->:使能条件。只有enable为高时,才检查这条规则。

我曾经在一个项目里忘了加disable iff,结果复位期间报了上百个fail。后来花了整整一天去排查,才发现是复位导致的误报。嗯,从那以后,我写属性第一件事就是加disable iff。

3.2.3 断言语句

属性写好了,怎么用?有三种方式:

// assert:验证正确性
a_req_ack: assert property(p_req_ack) else $error("req-ack协议违例");

// cover:覆盖率收集
c_req_ack: cover property(p_req_ack);

// assume:假设约束
asm_req_ack: assume property(p_req_ack);

我个人习惯:assert用来抓bug,cover用来确认激励是否到位,assume用来约束环境。这三者配合使用,效果最好。

3.3 并发断言与时钟的关系

这一点我得重点说说。并发断言和时钟的关系,说白了就是时钟决定了断言什么时候“看一眼”信号。

你想想看,如果时钟是100MHz,那断言就在每个上升沿采样一次。如果时钟是1GHz,那就采得更频繁。但不管频率多高,断言只在时钟沿上干活。

这里有个容易踩的坑:时钟域匹配问题。

我在项目中遇到过:一个模块有两个时钟域,一个100MHz,一个200MHz。有人写断言时,把两个时钟域的信号放在同一个属性里,结果仿真结果完全不对。

注意:跨时钟域的断言,必须用同步器处理。不能直接把两个时钟域的信号放在同一个@(posedge clk)里。否则你会看到一堆莫名其妙的fail。

正确的做法是:

// 错误示例:跨时钟域直接比较
property p_wrong;
    @(posedge clk1) sig_a |=> @(posedge clk2) sig_b;  // 这样写不行!
endproperty

// 正确做法:分别断言,或者用同步后的信号
property p_correct;
    @(posedge clk1) sig_a |=> sig_a_sync_to_clk2;  // sig_a已经同步到clk2域
endproperty

另外,时钟的边沿也很重要。大部分情况下我们用posedge,但有些协议(比如DDR)需要用到negedge。这时候只要把@(posedge clk)改成@(negedge clk)就行。

总结一下:

  • 并发断言依赖时钟沿触发
  • 序列描述时序关系,属性加条件,断言语句决定用途
  • 跨时钟域要格外小心,别把不同时钟域的信号直接比较
  • 别忘了加disable iff,不然复位期间有你受的

好了,并发断言的基础就这些。下一节咱们聊聊更高级的序列操作符和匹配机制。到时候我会分享一些我在调试复杂断言时的实战经验。


可可精品资料库