2. SVA语法入门:序列、属性、断言指令

好,咱们直接进入正题。SVA(SystemVerilog Assertions)说白了就是给硬件写“契约”——你规定信号该怎样跳变,仿真器帮你盯着。我刚开始接触SVA时,觉得它跟Verilog的always块差不多,后来才发现完全不是一回事。

这一节,咱们把三个核心概念讲透:序列(sequence)、属性(property)、断言指令(assert/assume/cover)。这三者层层递进,缺一不可。

一句话总结:序列描述“发生了什么”,属性描述“应该怎样”,断言指令告诉工具“去检查、去假设、去覆盖”。
SVA 断言三层结构 序列 (sequence) 描述信号时序关系 a ##1 b, $rose(clk) 属性 (property) 封装序列 + 逻辑条件 property p1; @(posedge clk) a |-> b; endproperty 断言指令 assert / assume / cover 检查 / 约束 / 覆盖 序列是砖块,属性是墙,断言指令是质检员 sequence s_req_ack; req ##[1:3] ack; // req拉高后1~3拍ack必须拉高 endsequence property p_req_ack; @(posedge clk) $rose(req) |-> s_req_ack; endproperty a_req_ack: assert property(p_req_ack);

2.1 序列(sequence)—— 时序关系的“最小单元”

序列,就是描述信号在一段时间内的跳变关系。你可以把它理解成“时序表达式”。

举个例子:

sequence s1;
  a ##1 b ##2 c;
endsequence

这个序列的意思是:在当前时钟沿,a为高;1拍后b为高;再2拍后c为高。注意,这里的数字是时钟周期数,不是时间。

我的习惯:写序列时,我总会在纸上先画波形。画清楚了再写代码,能省一半调试时间。

常用的序列操作符:

操作符含义示例
##n延迟n个时钟周期a ##2 b
##[m:n]延迟范围a ##[1:3] b
$rose(sig)信号上升沿检测$rose(clk)
$fell(sig)信号下降沿检测$fell(rst)
$stable(sig)信号保持稳定$stable(data)
and两个序列同时满足s1 and s2
or两个序列任一满足s1 or s2
intersect两个序列同时结束s1 intersect s2
throughout条件在整个序列期间成立en throughout s1
within一个序列包含在另一个内s1 within s2
我曾经踩过的坑:写序列时忘记加时钟事件。比如直接写 a ##1 b 而不放在 @(posedge clk) 里,仿真器会报错。记住:序列本身不指定时钟,时钟是在属性或断言里指定的。

2.2 属性(property)—— 给序列穿上“逻辑外衣”

属性是在序列的基础上,加上逻辑判断条件。说白了,序列只描述“发生了什么”,属性则描述“应该怎样”。

最常用的属性结构:

property p_example;
  @(posedge clk) disable iff (rst)
    antecedent |-> consequent;
endproperty

拆开来看:

  • @(posedge clk):时钟事件,所有采样都在时钟上升沿进行
  • disable iff (rst):当rst有效时,禁用这个属性检查。我习惯把复位条件都写在这里,干净利落
  • antecedent |-> consequent:如果antecedent成立,那么consequent必须成立。这是最核心的蕴含操作符

蕴含操作符有两种:

  • |->:重叠蕴含。antecedent和consequent在同一个时钟沿判断
  • |=> :非重叠蕴含。antecedent在当前时钟沿判断,consequent在下一个时钟沿判断
记住这个区别: |-> 是“同时”,|=> 是“下一拍”。我见过很多新手把这两个搞混,仿真结果对不上才回头查。

举个例子,验证一个简单的握手协议:

property p_handshake;
  @(posedge clk) disable iff (rst)
    $rose(valid) |-> ##[1:3] ready;
endproperty

这个属性说:当valid上升沿到来时,未来1到3拍内ready必须拉高。如果ready在3拍内没拉高,断言就失败。

2.3 断言指令(assert/assume/cover)—— 三种不同的“检查模式”

有了属性和序列,怎么用?三种指令,各有各的用途。

指令用途仿真行为形式验证行为
assert检查设计行为是否正确失败时打印错误作为断言检查
assume约束输入环境失败时打印警告作为约束条件
cover统计覆盖情况触发时记录覆盖点不适用

assert——最常用。用来检查设计是否满足预期。比如:

a_valid_ready: assert property(p_handshake)
  else $error("握手协议失败:valid后3拍内ready未拉高");

assume——约束输入。我在项目中常用assume来限制测试平台的激励。比如:

a_input_stable: assume property(@(posedge clk)
  $stable(data) throughout (valid ##[1:3] ready));

这个assume告诉工具:在valid有效到ready有效的这段时间里,data信号必须保持稳定。如果测试平台违反了,assume会报错。

我个人习惯:assume和assert成对写。一个约束输入,一个检查输出。这样环境干净,设计也干净。

cover——统计覆盖。用来确认某些场景是否被测试到。比如:

c_handshake_done: cover property(@(posedge clk)
  $rose(valid) ##1 ready);

这个cover统计“valid拉高后正好1拍ready就响应”的情况是否被覆盖到。覆盖率报告里能看到这个cover点触发了多少次。

注意:cover不会报错,它只是默默地计数。我见过有人把cover当成assert用,结果仿真跑完才发现该检查的都没检查。

2.4 实战:一个完整的断言例子

咱们把上面讲的串起来,写一个完整的断言模块:

module assertions_example(input clk, rst, valid, ready, [7:0] data);

  // 序列:valid有效后1~3拍ready响应
  sequence s_handshake;
    valid ##[1:3] ready;
  endsequence

  // 属性:valid上升沿触发握手检查
  property p_handshake;
    @(posedge clk) disable iff (rst)
      $rose(valid) |-> s_handshake;
  endproperty

  // 断言:检查握手协议
  assert_handshake: assert property(p_handshake)
    else $error("Handshake failed at time %0t", $time);

  // 假设:data在握手期间必须稳定
  assume_data_stable: assume property(@(posedge clk) disable iff (rst)
    valid |-> $stable(data) until ready);

  // 覆盖:统计1拍内响应的场景
  cover_fast_handshake: cover property(@(posedge clk) disable iff (rst)
    $rose(valid) ##1 ready);

endmodule

这个例子包含了序列、属性、三种断言指令。你想想看,如果不用SVA,用纯Verilog写同样的检查逻辑,得写多少行?而且可读性差很多。

核心要点:
  • 序列描述时序关系,属性加上逻辑条件
  • assert检查设计,assume约束环境,cover统计覆盖
  • 三者配合使用,才能构建完整的验证闭环

嗯,这一节的内容就到这里。SVA的语法其实不复杂,难的是怎么把验证思路转化成断言表达式。多写、多仿真、多踩坑,慢慢就有感觉了。

可可精品资料库