3、SystemVerilog断言基础:立即断言与并发断言,assert/assume/cover的使用

各位做验证的朋友,咱们今天聊聊断言。说实话,我刚入行那会儿,觉得断言就是个可有可无的装饰品。直到有一次,一个时序问题在仿真里跑了三天才暴露出来,而如果当时写了断言,第一拍就能抓到。从那以后,我再也不敢小看它了。

SystemVerilog里的断言,说白了就两种:立即断言并发断言。它们俩就像验证工具箱里的螺丝刀和电钻——各有各的用处,但你不能拿螺丝刀去钻墙,对吧?

3.1 立即断言:简单粗暴,立竿见影

立即断言,我习惯叫它"过程断言"。它写在过程块里(比如always块、initial块),执行到这一行就立刻检查。语法很简单:

assert (expression) else $error("断言失败");

嗯,就这么一行。但你别小看它。我在项目中遇到过一种情况:一个状态机的跳转条件写错了,仿真跑了上万拍才触发。后来我在状态跳转的地方加了个立即断言,每次跳转都检查当前状态是否合法。结果呢?第一轮回归就抓到了三个非法跳转。

核心要点:立即断言是阻塞性的。它执行完这一行,结果就出来了。适合用在组合逻辑、状态机跳转、数据通路的关键节点上。

举个例子:

always_ff @(posedge clk) begin
  if (valid) begin
    data_q <= data_in;
    // 检查数据是否在合法范围内
    assert (data_in inside {[0:255]})
      else $error("数据越界: %0d", data_in);
  end
end

你看,这个断言写在时序逻辑里,每次valid有效时都会检查。如果数据是256,仿真立刻报错,不用等到数据传到下游才发现问题。

3.2 并发断言:时序的守护者

并发断言就不一样了。它不写在过程块里,而是写在模块的声明区。它跟时钟走,可以描述跨多个时钟周期的时序关系。

我个人习惯把并发断言叫做"时序断言"。为什么?因为它天生就是为时序检查设计的。你想想看,一个握手协议,req拉高后几个周期ack必须拉高——这种场景,用立即断言你得写一堆状态机,而并发断言一行搞定:

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

a_handshake: assert property (p_handshake);

这个断言的逻辑是:如果req拉高,那么1到3个时钟周期后,ack必须拉高。简洁、清晰、可读性强。

我的经验:并发断言一定要写property,然后assert property。不要图省事把表达式直接写在assert里。为什么?因为property可以复用,可以单独调试,还可以被cover收集。

3.3 assert、assume、cover:三兄弟各司其职

这三个关键字,我经常看到有人混用。其实它们分工很明确:

关键字 用途 仿真行为 形式验证行为
assert 检查设计行为是否正确 失败时报错 作为证明目标
assume 约束输入环境 失败时报错 作为约束条件
cover 收集覆盖率 记录触发次数 不适用

assert 是老大,负责抓bug。设计该有的行为没出现,它立刻报警。

assume 是老二,负责管输入。我曾经在一个项目中,测试环境给DUT的输入信号总是违反协议,导致仿真跑飞。后来我在接口处加了assume,约束输入必须满足协议。这样一来,测试环境不规范,assume先报错,问题定位快多了。

cover 是老幺,负责记录。它不报错,只告诉你某个事件发生了多少次。我经常用cover来检查:这个状态机有没有进入过所有状态?这个握手协议有没有出现过背压?

注意:assume和assert在仿真里行为一样,都是报错。但在形式验证里,assume是约束,assert是目标。如果你把assume当assert用,形式验证会认为你在约束环境,而不是在检查设计。我曾经吃过这个亏,形式验证跑了三天,结果发现assume写错了,所有结果都是废的。

3.4 实战:一个完整的断言示例

咱们来看一个实际项目中的例子。这是一个AXI-Stream接口的验证:

module axis_checker (
  input logic clk,
  input logic rst_n,
  input logic tvalid,
  input logic tready,
  input logic tlast,
  input logic [7:0] tdata
);

  // 1. 并发断言:valid不能在没有ready时拉低
  property p_valid_stable;
    @(posedge clk) disable iff (!rst_n)
    ($rose(tvalid) && !tready) |=> tvalid;
  endproperty
  a_valid_stable: assert property (p_valid_stable)
    else $error("valid在未握手时被拉低");

  // 2. 并发断言:tlast之后必须跟新的包起始
  property p_tlast_to_tvalid;
    @(posedge clk) disable iff (!rst_n)
    $rose(tlast) |=> ##1 !tvalid;
  endproperty
  a_tlast_to_tvalid: assert property (p_tlast_to_tvalid)
    else $error("tlast后下一拍valid必须为低");

  // 3. cover:收集握手成功的次数
  cover property (
    @(posedge clk) tvalid && tready
  );

  // 4. 立即断言:检查数据在握手时不能为X
  always_ff @(posedge clk) begin
    if (tvalid && tready) begin
      assert (!$isunknown(tdata))
        else $error("握手时数据包含X态");
    end
  end

endmodule

这个例子涵盖了并发断言、立即断言、cover。你看,每个断言都有明确的职责:有的检查时序,有的检查数据完整性,有的收集覆盖率。

3.5 避坑指南

做了这么多年验证,我总结了几条断言的使用心得:

  • 不要过度断言。我曾经在一个模块里写了200多个断言,结果仿真速度慢了30%,而且大部分断言从来没触发过。断言要打在关键路径上,比如协议边界、状态机跳转、数据通路。
  • 并发断言一定要写disable iff。复位期间断言应该被禁用,否则复位释放那一拍会误报。我见过太多人忘了写这个,然后花一整天排查假失败。
  • cover不要只写一个。我习惯在每个assert旁边配一个cover,记录这个断言被检查了多少次。如果cover计数为0,说明这个断言从来没被触发过——要么是环境没覆盖到,要么是断言写错了。
  • 立即断言和并发断言可以混用。时序检查用并发断言,数据完整性检查用立即断言。各取所长。

一句话总结:assert抓bug,assume管输入,cover看覆盖。三者配合,你的验证环境才算完整。

SystemVerilog断言体系 立即断言 并发断言 cover 写在过程块中,立即执行 适合:组合逻辑、数据检查 写在声明区,基于时钟 适合:时序检查、协议验证 记录事件发生次数 不报错,只统计覆盖率 assert抓bug | assume管输入 | cover看覆盖 三者配合,构建完整的验证闭环

这张图把断言体系梳理得很清楚。你写断言的时候,先想清楚:我要检查什么?是时序关系还是数据值?然后选对工具。别拿立即断言去检查跨周期时序,也别拿并发断言去检查组合逻辑——虽然也能用,但代码会变得很难看。

好了,关于断言的基础就聊这么多。记住:断言不是装饰品,它是验证的守护者。写好了,它能帮你省下几周的调试时间。写不好,它就是个摆设。多练、多用、多总结,你也能成为断言高手。


公众号:蓝海资料掘金营,微信deep3321