第三章 并发断言:并发断言基础、并发断言与立即断言的区别、并发断言示例

各位同学,咱们今天聊聊并发断言。说实话,很多刚入行的朋友一听到“并发”两个字就头大,觉得这东西肯定特别复杂。其实不然,你把它想简单点——并发断言就是“盯着信号看一段时间”。

我在项目里见过不少同事,写断言时习惯用立即断言,结果仿真一跑,一堆误报。为什么?因为立即断言只看当前时刻,而很多时序问题,是需要跨时钟周期去检查的。嗯,这就是并发断言派上用场的地方。

并发断言基础

并发断言,说白了就是基于时钟沿的断言。它不像立即断言那样“看到就执行”,而是等到时钟沿来了才去评估。我个人习惯把并发断言理解为“带时钟的观察者”。

它的基本语法长这样:

property_name: assert property ( @(posedge clk) disable iff (rst) expression );

这里有几个关键点:

  • @(posedge clk):指定时钟沿,这是并发断言的核心
  • disable iff (rst):复位时禁用,避免复位期间误报
  • property:把断言包装成一个属性,方便复用

你想想看,如果没有时钟沿,你怎么知道信号变化发生在哪个时刻?并发断言就是帮你解决了这个问题。

重要概念:并发断言是“采样-评估”模式。它在时钟沿采样信号值,然后在同一时刻或后续时刻评估表达式。这和立即断言的“事件触发-立即评估”完全不同。

并发断言与立即断言的区别

我记得有一次,团队里一个新人写了个立即断言检查握手协议:

always @(posedge clk) begin
  if(valid & ready)
    assert(data !== 'x);
end

看起来没问题对吧?但仿真跑起来,发现这个断言在valid和ready同时为高的那个时钟沿,检查的是当前时刻的data值。如果data刚好在那个时刻从x变成有效值,就会误报。

换成并发断言就不一样了:

property check_data_valid;
  @(posedge clk) valid && ready |-> !$isunknown(data);
endproperty

assert property (check_data_valid);

并发断言会在时钟沿之前采样data值,然后在时钟沿之后评估。这就避免了竞争问题。

我把它们的区别整理成了一张表,方便你对照:

对比项 立即断言 并发断言
触发方式 事件触发(立即执行) 时钟沿触发(采样后评估)
执行时机 当前仿真时刻 时钟沿后的评估区域
采样方式 直接使用当前值 使用时钟沿前的采样值
时序检查 不支持跨周期 支持跨周期($past, |=> 等)
适用场景 组合逻辑、简单检查 时序协议、状态机、总线
误报风险 高(容易受竞争影响) 低(采样机制更稳定)

我的建议:在模块内部做简单检查时,用立即断言没问题。但一旦涉及跨时钟周期、握手协议、总线协议,请务必使用并发断言。这是血的教训换来的经验。

并发断言示例

光说不练假把式。咱们来看几个我在项目中实际用过的例子。

示例1:握手协议检查

这是一个典型的valid-ready握手协议。要求valid拉高后,ready必须在3个时钟内拉高:

property handshake_timing;
  @(posedge clk) disable iff (rst_n == 0)
  $rose(valid) |-> ##[1:3] ready;
endproperty

assert property (handshake_timing)
  else $error("握手超时:valid拉高后3个周期内ready未拉高");

这里用了 ##[1:3] 表示“1到3个时钟周期后”。我曾经在一个DMA控制器项目里用这个检查,抓到了两个模块之间时序不匹配的bug。嗯,那是个很难复现的问题,多亏了并发断言。

示例2:状态机跳转检查

状态机从IDLE状态只能跳转到BUSY状态,不能直接跳到DONE:

property fsm_transition;
  @(posedge clk) disable iff (rst_n == 0)
  (state == IDLE) |=> (state == BUSY);
endproperty

assert property (fsm_transition)
  else $error("非法状态跳转:IDLE不能直接跳到非BUSY状态");

注意这里用的是 |=>(非重叠蕴含),表示“下一个时钟周期”。如果是 |->(重叠蕴含),表示“同一个时钟周期”。这两个符号的区别,我建议你亲手写几个例子跑一跑,感受会更直观。

示例3:数据稳定性检查

当valid拉高时,data不能为X或Z:

property data_stable;
  @(posedge clk) disable iff (rst_n == 0)
  valid |-> !$isunknown(data);
endproperty

assert property (data_stable)
  else $error("数据异常:valid有效时data包含X或Z");

避坑指南:我曾经在检查数据稳定性时,忘了加 disable iff。结果复位期间data是X,断言疯狂报错。加了复位禁用后,世界清净了。记住:复位期间,所有断言都应该被禁用。

示例4:跨时钟域检查

两个时钟域之间的信号传递,要求信号稳定至少2个目标时钟周期:

property cdc_stable;
  @(posedge clk2) disable iff (rst_n == 0)
  $rose(sig_sync) |-> $stable(sig_sync, 2);
endproperty

assert property (cdc_stable)
  else $error("CDC不稳定:同步后信号在2个周期内发生变化");

这个检查帮我抓到了好几个跨时钟域同步器的问题。说实话,跨时钟域的问题在仿真里很难发现,但并发断言给了你一双“透视眼”。

总结一下

并发断言的核心就三点:

  1. 有时钟:基于时钟沿采样和评估
  2. 能跨周期:用 ##、|=>、$past 等操作符检查时序关系
  3. 更稳定:采样机制避免了竞争和误报

我个人觉得,掌握并发断言是验证工程师从“入门”到“进阶”的分水岭。你如果能把并发断言用好,很多隐蔽的时序问题都能提前发现,而不是等到后仿甚至流片回来才追悔莫及。

下一章咱们会深入讲讲并发断言的各种操作符和序列,到时候会有更多实战案例。今天就先到这里,回去把上面几个例子跑一跑,感受一下并发断言的威力。