第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 常见问题与避坑
最后,我总结几个我在项目中踩过的坑:
- 时钟域问题:序列里的时钟必须一致。别在一个序列里混用posedge clk和negedge clk,除非你很清楚自己在做什么。
- 延迟范围:
##[m:n]里的m必须小于等于n,否则仿真器会报错。 - 重复次数:
[*0]表示重复0次,也就是不匹配。这个在有些场景下有用,但容易搞混。 - 序列嵌套:序列可以嵌套,但别嵌套太深,否则调试起来很痛苦。
好了,这一章的内容就到这里。序列是SVA的基石,搞懂了序列,后面的断言、属性就水到渠成了。下一章我们聊聊属性,看看怎么把序列组合成更复杂的检查逻辑。