4. SVA基础语法(二):序列与属性的实战应用

各位同学,欢迎来到第四讲。上一讲我们聊了SVA的基本结构,今天要深入两个核心概念:序列(sequence)属性(property)。说白了,这两个东西就是SVA的“左膀右臂”。我个人习惯把序列看作“事件的组合”,把属性看作“对事件的断言”。

4.1 序列(sequence)的定义与使用

先说说序列。什么是序列?序列就是一系列事件的组合。你想想看,验证一个协议,往往不是看单个信号跳变,而是看一组信号在时间上的配合。比如握手协议,先有请求,后有应答,这就是一个序列。

4.1.1 基本序列定义

定义一个序列,用 sequence ... endsequence 包裹起来。看个例子:

sequence req_ack;
    @(posedge clk) req ##1 ack;
endsequence

这个序列表示:在时钟上升沿,先看到 req 为高,一个时钟周期后ack 为高。这里的 ##1 就是延迟操作符,表示一个时钟周期。

小提示: 我在项目中遇到过,很多新手会把 ##1 理解成“延迟1个时间单位”,其实它延迟的是“1个时钟周期”。在同步设计中,这很关键。

4.1.2 带参数的序列

序列也可以带参数,这样复用性更好。比如:

sequence req_ack_delay(cycle);
    @(posedge clk) req ##cycle ack;
endsequence

调用的时候,你可以指定延迟周期数:

req_ack_delay(2);  // 延迟2个周期
req_ack_delay(5);  // 延迟5个周期

嗯,这里要注意:参数必须是常量,不能是变量。我曾经踩过这个坑,在循环里动态传参,结果编译报错。

4.1.3 序列的复合操作

序列可以组合使用。常用的操作符有:

  • and:两个序列都成功
  • or:两个序列至少一个成功
  • intersect:两个序列同时结束
  • within:一个序列在另一个序列内部发生

举个例子:

sequence seq_a;
    @(posedge clk) a ##1 b;
endsequence

sequence seq_c;
    @(posedge clk) c ##1 d;
endsequence

sequence seq_and;
    @(posedge clk) seq_a and seq_c;
endsequence

这个 seq_and 表示:a##1bc##1d 这两个序列要成功。注意,它们可以不同时开始,但都必须完成。

避坑指南: 我曾经在项目中用 intersect 时犯过错误。它要求两个序列同时开始、同时结束。如果你只是想检查两个序列都发生,用 and 更合适。

4.2 属性(property)的定义与使用

序列是“发生了什么”,属性是“应该发生什么”。属性是断言的灵魂。你定义一个属性,然后用 assert 去检查它。

4.2.1 基本属性定义

属性用 property ... endproperty 定义:

property req_ack_prop;
    @(posedge clk) req |-> ##1 ack;
endproperty

assert property (req_ack_prop);

这个属性表示:如果 req 为高,那么下一个时钟周期 ack 必须为高。这就是一个典型的蕴含操作。

4.2.2 属性中的蕴含操作符

蕴含操作符是SVA里最常用的。它有两种:

操作符 含义 示例
|-> 重叠蕴含(同一周期检查) a |-> b 表示a为高时,同一周期b必须为高
|=> 非重叠蕴含(下一周期检查) a |=> b 表示a为高时,下一周期b必须为高

我个人习惯这样记:|-> 是“立即检查”,|=> 是“等一个周期再检查”。

看个实际例子:

// 重叠蕴含:写使能时,数据必须有效
property write_data_overlap;
    @(posedge clk) write_en |-> data_valid;
endproperty

// 非重叠蕴含:请求后,下一个周期必须有应答
property req_ack_nonoverlap;
    @(posedge clk) req |=> ack;
endproperty
重点: 很多协议中,控制信号和数据信号是同一周期有效的,用 |->。而请求-应答这种有先后顺序的,用 |=>。选错了,验证结果就全错了。

4.3 序列与属性的配合使用

在实际项目中,序列和属性经常配合使用。我一般这样组织:

  1. 先定义底层的序列,描述信号的基本时序关系
  2. 再用属性把这些序列组合起来,形成完整的断言

举个例子:

// 定义序列
sequence s_req;
    @(posedge clk) req;
endsequence

sequence s_ack;
    @(posedge clk) ##[1:3] ack;  // 1到3个周期内应答
endsequence

// 定义属性
property p_req_ack;
    @(posedge clk) s_req |-> s_ack;
endproperty

// 断言
assert property (p_req_ack);

这个例子中,s_ack 用了 ##[1:3],表示应答可以在1到3个周期内出现。这种时间窗口在实际验证中非常常见。

经验之谈: 我建议把序列和属性分开定义。这样做的好处是:序列可以复用,属性可以单独调试。我曾经在一个项目中,把序列定义在单独的包(package)里,多个模块共用,省了不少事。

4.4 蕴含操作符的深入理解

蕴含操作符看起来简单,但用起来有不少门道。我重点说两个容易混淆的地方。

4.4.1 蕴含的“空成功”问题

你想想看,如果 a |-> b 中,a 从来没有为高过,这个属性算成功还是失败?

答案是:算成功。因为蕴含的语义是“如果a发生,那么b必须发生”。a没发生,条件不成立,自然不算失败。这叫“空成功”(vacuous success)。

我曾经在项目中遇到一个bug:某个信号一直没拉高,导致断言一直空成功,掩盖了真正的设计问题。后来我加了一个覆盖点(cover property)来检查前提条件是否被触发过。

4.4.2 蕴含与序列的嵌套

蕴含操作符后面可以跟复杂的序列:

property complex_prop;
    @(posedge clk) a ##1 b |-> c ##1 d;
endproperty

这个属性表示:如果看到 a 后一个周期出现 b,那么紧接着 c 后一个周期必须出现 d。说白了,就是“如果前半段序列成功,后半段序列必须成功”。

注意: 蕴含操作符的优先级比较低。如果你在属性里混合了 andor 等操作,最好加括号明确优先级。我见过有人因为优先级问题,写出来的属性和预期完全不一样。

4.5 实战建议

最后,给各位几个实战建议:

  • 从简单开始:先写简单的序列和属性,验证通过后再逐步复杂化
  • 多用参数化序列:提高复用性,减少重复代码
  • 注意时间窗口:用 ##[m:n] 而不是固定延迟,更贴近实际协议
  • 加覆盖点:检查前提条件是否被触发,避免空成功

好了,这一讲就到这里。序列和属性是SVA的基石,一定要多练。下一讲我们会聊SVA的高级序列操作,包括 throughoutwithin 等,敬请期待。