第4章 SVA序列基础:序列定义、序列操作符、序列匹配

各位同学,今天我们来聊聊SVA里最基础、也最核心的概念——序列。说实话,很多初学者一上来就盯着assertion怎么写,结果序列没搞明白,调试起来一头雾水。我个人习惯是,先把序列玩透了,再谈断言。

4.1 什么是序列?

序列,说白了就是一组事件的组合。你想想看,我们验证一个协议,比如AHB的写操作,它不是一个信号跳变就完事了,而是一连串的信号变化:地址有效、数据有效、响应返回……这一连串的事件,就是序列。

在SystemVerilog里,我们用sequence关键字来定义它。举个例子:

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

这个序列表示:在时钟上升沿,先看到req为高,过一个时钟看到gnt为高,再过一周期看到ack为高。嗯,这里要注意,##1表示延迟一个时钟周期,不是延迟一个时间单位。

小提示: 我在项目中遇到过有人把##1#1搞混。#1是过程赋值里的延迟,##1才是序列里的时钟周期延迟。千万别弄反了。

4.2 序列操作符

序列操作符是SVA的精华。我刚开始学的时候,觉得这些符号跟天书似的,后来发现其实就那么几种,用熟了就好。

4.2.1 延迟操作符 ##

这个刚才提到了,##n表示延迟n个时钟周期。但你知道吗?它还有变种:

  • ##[m:n]:延迟m到n个周期,不确定范围
  • ##[m:$]:延迟m到无穷大,$表示仿真结束

举个例子:

sequence s_data_valid;
    @(posedge clk) req ##[1:3] data_valid;
endsequence

这个序列表示:req为高之后,在1到3个周期内,data_valid必须为高。我在项目中经常用这种不确定延迟的写法,特别是处理那些延迟不固定的握手协议。

4.2.2 连续重复操作符 [*n]

有时候我们需要某个事件连续发生多次。比如,一个burst传输,连续4个数据:

sequence s_burst4;
    @(posedge clk) data_valid[*4];
endsequence

这表示data_valid连续4个周期都为高。注意,是连续!中间不能断。

避坑指南: 我曾经在项目中用过data_valid[*4],结果仿真一直不匹配。后来发现,data_valid中间有一个周期被拉低了。连续重复要求每个周期都满足,少一个都不行。

4.2.3 非连续重复操作符 [=n]

这个跟上面那个不一样。非连续重复不要求连续,只要在指定范围内出现n次就行。比如:

sequence s_nonconsec;
    @(posedge clk) a ##[1:5] b[=3];
endsequence

这个序列表示:a为高之后,在1到5个周期内,b出现3次(不要求连续)。说实话,这个操作符我用得不多,但在某些协议里确实有用。

4.2.4 跟随操作符 -> 和 =>

这两个操作符表示因果关系。a -> b表示a发生,然后b发生。但注意,它不要求b紧跟着a,中间可以有延迟。

sequence s_cause_effect;
    @(posedge clk) req -> ##[1:3] ack;
endsequence

这个序列表示:req为高之后,在1到3个周期内,ack必须为高。说白了,就是req导致ack发生。

4.3 序列匹配

序列定义好了,怎么知道它匹配了呢?这就涉及到序列匹配的概念。

序列匹配,就是检查序列是否按照我们定义的方式发生了。SVA里,序列匹配的结果是一个布尔值:匹配成功返回1,失败返回0。

但这里有个关键点:序列匹配是基于时钟边沿的。每个时钟上升沿,SVA都会检查序列是否开始、是否结束。

4.3.1 序列的起始点和结束点

每个序列都有一个起始点和一个结束点。起始点就是序列第一个事件发生的时刻,结束点就是最后一个事件发生的时刻。

比如:

sequence s_example;
    @(posedge clk) a ##2 b ##1 c;
endsequence

这个序列的起始点是a为高的那个时钟沿,结束点是c为高的那个时钟沿。中间经过了3个时钟周期(##2 + ##1)。

4.3.2 序列匹配的时机

序列匹配是在结束点进行判断的。也就是说,只有当最后一个事件发生时,SVA才会告诉你这个序列是否匹配成功。

重要: 序列匹配是非阻塞的。什么意思?就是序列匹配不会影响仿真时间。它只是在每个时钟沿检查一下,匹配了就报告,不匹配就继续等。

4.3.3 序列匹配的示例

来看一个完整的例子:

module seq_match_example;
    logic clk, a, b, c;
    
    // 定义序列
    sequence s_abc;
        @(posedge clk) a ##2 b ##1 c;
    endsequence
    
    // 使用断言检查
    assert property (s_abc);
    
    // 生成时钟
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end
    
    // 驱动信号
    initial begin
        a = 0; b = 0; c = 0;
        #10 a = 1;
        #10 a = 0;
        #10 b = 1;
        #10 b = 0;
        #10 c = 1;
        #10 c = 0;
        #20 $finish;
    end
endmodule

这个例子中,序列s_abc在a为高后的第2个周期b为高,再过一个周期c为高。如果信号按照这个顺序变化,序列就匹配成功。

4.4 序列的复用

序列定义好了,可以像函数一样被多次调用。我个人习惯把常用的序列放在一个包里,方便复用。

package seq_pkg;
    sequence s_req_ack;
        @(posedge clk) req ##[1:3] ack;
    endsequence
    
    sequence s_data_valid;
        @(posedge clk) data_en ##1 data_valid;
    endsequence
endpackage

然后在不同的模块里,直接import这个包就能用了。这样写,代码整洁,维护也方便。

4.5 常见问题与避坑

最后,我总结几个我在项目中踩过的坑:

  1. 时钟域问题:序列里的时钟必须一致。别在一个序列里混用posedge clk和negedge clk,除非你很清楚自己在做什么。
  2. 延迟范围##[m:n]里的m必须小于等于n,否则仿真器会报错。
  3. 重复次数[*0]表示重复0次,也就是不匹配。这个在有些场景下有用,但容易搞混。
  4. 序列嵌套:序列可以嵌套,但别嵌套太深,否则调试起来很痛苦。
我的建议: 刚开始写序列时,先用简单的波形图把时序画出来。我在项目里就是这么干的,画完图再写代码,基本一次过。

好了,这一章的内容就到这里。序列是SVA的基石,搞懂了序列,后面的断言、属性就水到渠成了。下一章我们聊聊属性,看看怎么把序列组合成更复杂的检查逻辑。