2. 立即断言:基础中的基础

各位同学,咱们今天聊聊断言验证里最基础、也最直接的一种——立即断言。

我记得刚入行那会儿,带我的老工程师跟我说:“断言这东西,说白了就是给仿真器装上一双眼睛。” 我当时还不太理解,后来踩了不少坑才明白——没有断言,你就是在黑盒里瞎摸。

好,咱们直接进入正题。

2.1 什么是立即断言?

立即断言,英文叫 Immediate Assertion。它本质上就是一个运行时检查。

你想想看,写 RTL 的时候,我们经常用 if-else 判断条件。立即断言就是把这个判断“固化”下来,一旦条件不满足,仿真立刻报错。

它的语法很简单:

assert (expression) [else $fatal/$error/$warning/$info];

注意,这里没有 property,没有 sequence,就是一个纯粹的布尔表达式。这也是它和并发断言最大的区别。

核心要点:立即断言是过程性的,它必须写在 initial、always 或者 task/function 里。它不像并发断言那样可以独立于过程块存在。

2.2 使用场景:什么时候该用它?

我个人习惯把立即断言用在三个地方:

  1. 接口协议检查:比如握手信号的时序、数据有效标志的配合。
  2. 内部状态机检查:状态跳转是否合法、状态编码是否冲突。
  3. 数据通路完整性检查:数据在流水线中是否被意外修改。

举个例子,我在项目中遇到过这样一个场景:一个 AXI 从机接口,要求 awvalid 和 awready 不能同时为高超过一个周期。我当时就用立即断言来抓这个违规:

always @(posedge clk) begin
  if (awvalid & awready) begin
    assert (awvalid & awready) else $error("awvalid和awready同时为高超过1周期!");
  end
end

嗯,这里要注意,这个写法其实有个小问题——它只在 awvalid & awready 为真的那个周期检查一次。如果持续多个周期,它只会报一次错。但对我们调试来说,一次就够了。

2.3 代码示例:从简单到复杂

咱们来看几个实际例子。

示例1:最简单的检查

module test_assert;
  logic clk, rst_n;
  logic [7:0] data;

  always @(posedge clk) begin
    if (!rst_n) begin
      data <= 8'h00;
    end else begin
      data <= data + 1;
    end
    // 立即断言:data不能超过100
    assert (data <= 8'd100) else $fatal("data overflow!");
  end
endmodule

这个例子很简单。但我想提醒你:立即断言的位置很重要。如果你把断言写在赋值之前,检查的是旧值;写在赋值之后,检查的是新值。我见过不少新手在这里翻车。

示例2:带 else 分支的断言

always @(posedge clk) begin
  if (valid) begin
    assert (data != 0) 
      else $error("valid时data不能为0,当前data=%0d", data);
  end
end

这里我用了 $error,它会让仿真继续,但会打印错误信息。如果你用 $fatal,仿真会直接停止。我个人习惯:调试阶段用 $fatal,回归阶段用 $error。为什么?因为调试时我希望立刻停下来看波形;回归时我希望跑完所有 case,统计错误数量。

示例3:状态机检查

typedef enum {IDLE, READ, WRITE, DONE} state_t;
state_t state, next_state;

always @(posedge clk) begin
  if (!rst_n) begin
    state <= IDLE;
  end else begin
    state <= next_state;
  end
  // 检查:状态不能是未知态
  assert ($onehot({state, next_state})) 
    else $warning("状态编码异常!");
end

小技巧:用 $onehot 检查状态编码是个好习惯。我曾经在一个项目中,因为状态机编码冲突,导致芯片功能异常。后来加了这条断言,问题立刻暴露。

2.4 常见错误:我踩过的坑

下面这些错误,我几乎都犯过。你注意听。

错误类型 错误示例 正确做法
断言写在过程块外 assert (a & b); 写在 module 里 必须写在 always、initial 或 task 中
忘记 else 分支 assert (cond); 没有 else 至少加 else $error,否则断言失败不报错
断言位置错误 在赋值前检查新值 理解阻塞/非阻塞赋值的时序
使用 $fatal 过度 每个断言都用 $fatal 区分致命错误和普通错误

避坑指南:我曾经在一个项目中,把所有断言都写成了 assert (cond) else $fatal。结果仿真跑到一半,因为一个非关键路径的断言失败,整个仿真停了。后来我花了整整一天去排查,才发现那个失败其实不影响功能。从那以后,我学会了区分 $fatal 和 $error。

2.5 知识体系图

下面这张图,帮你理清立即断言的核心脉络:

立即断言知识体系 立即断言 语法结构 使用场景 常见错误 assert(expr) [else action] 必须写在过程块中 接口协议检查 状态机合法性检查 数据通路完整性检查 写在过程块外 忘记else分支 断言位置错误

2.6 小结

立即断言,说白了就是给仿真加个“哨兵”。它简单、直接、有效。但正因为简单,很多人容易忽略细节。

我个人建议:每个模块至少加 3-5 个立即断言。别嫌多,等你调试的时候就知道它们有多香了。

好,这一章就到这儿。记住:断言不是负担,是你的护身符。

可可精品资料库