目录
systemverilog相对于verilog引进了一些新的数据类型,它们有如下优点:
(1)双状态数据类型:更好的性能,更低的内存消耗;
(2)队列、动态和关联数组:减少内存消耗,自带搜索和分类功能;
(3)类和结构:支持抽象数据结构;
(4)联合和合并结构:允许对同一数据有多种视图(view);
(5)字符串:支持内建的字符序列;
(6)枚举类型:方便代码编写,增加可读性;
2.1内建数据类型
2.1.1逻辑(logic)类型
SV对reg类型数据做了改进,使得它除了作为一个变量外,还可以被连续赋值、门单元和模块所驱动。为了与reg类型相区别,改进的数据类型称为logic。在任何使用线网(wire)的地方均可以使用logic,但要求logic不能有多个结构性的驱动,如在对双向总线建模的时候,此时需要使用线网类型;看下面例子:
module logic_data_type(input logic rst_h);
parameter CYCLE=20;
logic q,q_l,d,clk,rst_l;
initial begin
clk = 0;//过程赋值
forever #(CYCLE/2) clk = ~clk;
end
assign rst_l=~rst_h;//连续驱动
not n1(q_l,q);//q_l被门驱动
my_dff d1(q,d,clk,rst_l);//q被模块驱动
endmodule
//logic类型只能有一个驱动,如果存在多个驱动便会报错;有些信号如果希望
//它有多个驱动,例如双向总线,这些信号则需要定义为线网类型
2.1.2双状态数据类型
SV引入双状态数据类型相比于verilog的四状态数据类型来说,有利于提高仿真器的性能并减少内存的使用; 最简单的双状态数据类型是bit,它是无符号的;另外四种带符号的双状态数据类型分别是:byte,shortint,int,longint;看下面例子:
bit b;//双状态,单比特
bit [31:0] b32;//双状态,32比特无符号整数
int unsigned ui;//双状态,32比特无符号整数
int i;//双状态,32比特有符号整数
byte b8;//双状态,8比特有符号整数
shortint s;//双状态,16比特有符号整数
longint l;//双状态,64比特有符号整数
integer i4;//四状态,32比特有符号整数
time t;//四状态,64比特无符号整数
real r;//双状态,双精度浮点数
byte变量的范围是-128~127;把双状态变量连接到被测设计时,被测设计的输出务必要小心,如果被测设计试图产生X或Z,这些值会自动转换为双状态值,而测试代码可能永远无法察知;这些值被转换为0或者1并不重要,关键是要随时检查未知值的传播,使用$isunknown操作符,可以在表达式的任意位置出现X或Z时返回1;看下面例子:
if($isunkonwn (iport) == 1)
$display ("@%0t: 4-state value detected on iport %b", $time,iport);
//使用格式符%0t和参数$time 可以打印出当前的仿真时间,打印格式在$timeformat()子程序中指定
2.2 定宽数组
2.2.1定宽数组的声明和初始化
verilog要求在声明中必须给出数组的上下界,因为几乎所有的数组都使用0作为索引的下界,所以SV允许只给出数组宽度的便捷声明方式,跟C类似。看下面例子:
例2.4 定宽数组的声明
int lo_hi[0:15];//16个整数[0]..[15]
int c_style [16];//16个整数[0]..[15]
可以通过在变量名后指定维度的方式来创建多维定宽数组。例2.5创建了几个二维的整数数组,大小都是8行4列,最后一个元素被设置为1,如下所示:
例2.5 声明并使用多维数组
int array2 [0:7][0:3];//完整的声明
int array3 [8][4];//紧凑的声明
array2 [7][3]=1;//设置最后一个元素
如果试图从一个越界的数组中读取数据,那么SV将返回数组元素类型的缺省值。也就是说,对于一个元素为四状态类型的数组,例如logic,返回的是X;而对于双状态类型例如int或bit,则返回0。这种情况适用于所有的数组类型,包括定宽数组、动态数组、关联数组和队列,也同时适用于地址中含有X或Z的情况。线网在没有驱动时的输出是Z。
很多SV仿真器在存放数组元素时使用32比特的字边界,所以byte、shortint、int都是存放在一个字中,而longint则存放在两个字中。如例2.6所示,在非合并数组中,字的低位用来存放数据,高位则不使用。图2.1所示的字节数组b_unpack被存放在三个字的空间里。
仿真器通常使用两个或两个以上连续的字来存放logic和integer等四状态类型,这会比存放双状态变量多占用一倍的空间;
2.2.2 常量数组
例2.7示范了如何使用常量数组,即一个单引号加大括号来初始化数组;可以一次性为数组的部分或所有元素赋值,在大括号前标上重复数字可以对多个数组重复赋值,还可以为那些没有显式赋值的元素指定一个缺省值(注意加上单引号)。
2.2.3 基本的数组操作-for和foreach
操作数组最常见的方式是使用for和foreach循环。在例2.8中,i被声明为循环内的局部变量,SV的$size函数返回数组的宽度;在foreach循环中,只需指定数组名并在其后的方括号中给出索引变量,SV便会遍历数组中的元素。索引变量将自动声明,并只在循环内有效。
例2.8 在数组操作中使用for和foreach循环
initial begin
bit [31:0] src[5],dst[5];
for(int i=0;i<$size(src);i++)
src[i]=i;
foreach(dst[j])
dst[j]=src[j]*2;//dst的值是src的两倍
end
注意在例2.9中,对多维数组使用foreach的语法是用逗号隔开后放在同一个方括号里,像[i,j]。
如果你不需要遍历数组中的所有维度,可以在foreach循环里忽略他们;例2.11把一个二维数组打印成一个方形的矩阵,它在外层循环中遍历第一个维度,然后再内层循环中遍历第二个维度。
2.2.4 基本的数组操作-复制和比较
可以在不使用循环的情况下对数组进行聚合比较和复制(聚合操作适用于整个数组而不是单个元素),其中比较仅限于等于和不等于比较。
例2.13 数组的复制和比较操作
initial begin
bit [31:0] src[5]='{0,1,2,3,4};
bit [31:0] dst[5]='{5,1,2,3,4};
//两个数组的聚合比较
if(src == dst)
$display("src == dst");
else
$display("src != dst");
//把所有src元素赋值给dst
dst = src;
//只改变一个元素的值
src[0] = 5;
//对数组的算术运算不能使用聚合操作,而应该使用循环
end
2.2.5 同时使用位下标和数组下标
例2.14打印出数组的第一个元素、它的最低位以及紧接的高两位。
2.2.6 合并数组
声明合并数组时,合并的位和数组大小作为数据类型的一部分必须在变量名前面指定。数组大小定义的格式必须是[msb,lsb],而不是[size]。例2.15中的变量bytes是一个有4个字节的合并数组,使用单独的32比特来存放,如图所示:
合并和非合并数组可以混合使用。在例2.16中,barray是一个具有3个合并元素的非合并数组。
barray的存放形式如下图所示:
使用一个下标可以得到一个字的数据barray[2];使用两个下标可以得到一个字节的数据barray[0][3];使用三个下标可以访问到单个比特位barray[0][1][6];注意数组声明中在变量名后指定了数组的大小,barray[3]这个维度是非合并的,所以在使用该数组时至少要有一个下标。
2.2.8合并数组和非合并数组的选择
当需要和标量进行相互转换时,使用合并数组会非常方便;任何数组类型都可以合并;如果需要等待数组中的变化,则必须使用合并数组。当测试平台需要通过存储器数据的变化来唤醒时,你会想到使用@操作符,但是该操作符只能用于标量或合并数组;
2.3动态数组
前面介绍的verilog数组类型都是定宽数组,其宽度在编译时就确定了。SV提供了动态数组类型,可以在仿真时分配空间或调整宽度,这样在仿真中就可以使用最小的存储量。
动态数组在声明时使用空的下标[ ],数组最开始是空的,必须用new[ ]操作符来分配空间,同时在方括号中传递数组宽度。可以把数组名传给new[ ]构造符(不同于构造函数),并把已有数组的值复制到新数组中,如例2.17所示:
例2.17 使用动态数组
int dyn[],d2[];//声明动态数组
initial begin
dyn = new[5];//分配5个元素
foreach(dyn[j]) dyn[j]=j;//对元素进行初始化
d2=dyn;//复制动态数组
d2[0]=5;//修改元素值
dyn=new[20](dyn);//分配20个整数值并进行复制,把原来的dyn数组复制给开始的5个元素,然后释放dyn数组原有5个元素的空间,最终dyn指向了一个具有20个元素的数组
dyn=new[100];//分配100个整数值,旧值不复存在
dyn.delete();//删除所有元素
end
动态数组有一些内建的子程序,例如delete和size。
如果想声明一个常数数组又不想统计元素的个数,可以使用动态数组并使用常量数组进行赋值。在例2.18中声明的mask数组具有9个8比特元素,SV会自动统计元素的个数。
只要基本数据类型相同,例如都是int,定宽数组和动态数组之间就可以相互赋值。在元素数目相同的情况下,可以把动态数组的值复制到定宽数组;当把定宽数组复制到一个动态数组时,SV会调用构造符new[ ]来分配空间并复制数值。
2.4队列
SV引入了一种新的数据类型-队列,它结合了链表和数组的优点。队列与链表相似,可以在队列的任何地方增加或删除元素,这类操作在性能上的损失比动态数组小得多,因为动态数组要分配新的数组并复制所有元素的值。队列与数组相似,可以通过索引实现对任意元素的访问,而不需像链表那样去遍历目标元素之前的所有元素。
队列的声明是使用带美元符号的下标:[$]。队列元素的编号从0到$;SV的队列类似于标准模板库中的双端队列,你增加元素来创建队列,SV会分配额外的空间以便能够快速插入新的元素。当元素增加到超过原有空间的容量时,SV会自动分配更多的空间。注意不要对队列使用构造符new[ ]。
如果把$放在一个范围表达式的左边,那么$将代表最小值,例如[$:2]代表[0:2];如果$放在表达式的右边,则代表最大值。
例2.20 队列操作
int j=1,
q2[$]={3,4},
q[$]={0,2,5};
initial begin
q={q[0],j,q[1:$]};//{0,1,2,5} 在2之前插入1
q={q[0:2],q2,q[3:$]};//{0,1,2,3,4,5} 在q中插入一个队列
q={q[0],q[2:$]};//{0,2,3,4,5} 删除第1个元素
//下面的操作执行速度很快
q={6,q};//{6,0,2,3,4,5} 在队列前插入
j=q[$];//等同于 j=5
q=q[0:$-1];//从队列末尾取出数据
q={q,8};//在队列末尾插入
j=q[0];//等同于j=6
q=q[1:$];//{0,2,3,4,8} 从队列前面取出数据
q={};//{} 删除整个队列
end
队列中的元素是连续存放的,所以在队列的前面或后面存取数据非常方便。无论队列有多大,这种操作所耗费的时间都是一样的。在队列中间增加或删除元素需要对已存在的数据进行搬移以便腾出空间,相应操作所耗费的时间会随着队列的大小线性增加;也可以把定宽或动态数组的值复制给队列。
2.5 关联数组
SV提供了关联数组类型,用来保存稀疏矩阵的元素。这意味着当你对一个非常大的地址空间进行寻址时,SV只为实际写入的元素分配空间。在图2.4中,关联数组只保留0...3、42、1000、4512和200000等位置上的值。这样,用来存放这些值的空间比有200000个条目的定宽或动态数组所占用的空间小得多。
关联数组采用在方括号中放置数据类型的形式来进行声明,例如[int];也可以采用通配符作为关联数组的下标进行声明,例如wild[*],但是不推荐该种方法。看下面例子:
例2.21 关联数组的声明、初始化和使用
initial begin
bit [63:0] assoc[bit[63:0]],idx=1;
//对稀疏分布的元素进行初始化
repeat(64) begin
assoc[idx]=idx;//1、2、4、8、16等
idx=idx<<1;
end
//使用foreach遍历数组
foreach(assoc[i])
$display("assoc[%h]=%h",i,assoc[i]);
//使用函数遍历数组
if(assoc.first(idx))
begin//得到第一个索引
do
$display("assoc[%h]=%h",idx,assoc[idx]);
while(assoc.next(idx));//得到下一个索引
end
end
例2.21中的关联数组assoc具有稀疏分布的元素:1,2,4,8,16等等。用简单的for循环不能遍历该数组,需要使用foreach.
2.7数组的方法
SV提供了很多数组的方法,可用于任何一种非合并数组的类型。
2.7.1数组缩减方法
基本的数组缩减方法是把一个数组缩减成一个值,如例2.23所示。最常用的缩减方法是sum,对数组的所有元素求和。这里必须对SV处理操作位宽的规则十分小心。默认情况下,把单比特数组的所有元素相加,其和也是单比特。但如果使用32比特的表达式,把结果保存在32比特的变量里,与一个32比特的变量进行比较,或者使用适当的with表达式,SV都会在数组求和的过程中使用32比特位宽。
其他的数组缩减方法还有product(积)、and(与)、or(或)、xor(异或). SV没有提供专门从数组里随机选取一个元素的方法。
如果想从一个关联数组中随机选取一个元素,需要逐个访问之前的元素,原因是没办法直接访问到第N个元素。例2.24示范了如何从一个以整数作为索引的关联数组中随机选取一个元素。如果数组是以字符串作为索引,只需要将idx的类型改为string即可。
2.7.2 数组定位方法
数组中的最大值、特定值的搜索,可以使用数组定位方法,这些方法的返回值通常是一个队列。
注意,它们返回的是一个队列而非标量,这些方法也适用于关联数组。使用foreach循环固然可以实现数组的完全搜索,但如果使用SV的定位方法,则只需一个操作便可完成。表达式with可以指示SV如何进行搜索,如下例所示:
在条件语句with中,item被称为重复参数,代表了数组中一个单独的元素。item是缺省的名字,也可以指定别的名字,只要在数组方法的参数列表中列出来就可以了。
例2.28示范了几种对数组子集进行求和的方式。
在例2.28中,sum操作符的结果为条件表达式为真的次数,如果要求和则必须写成第二句的形式。注意,返回值为索引的数组定位方法,其返回的队列类型是int而不是integer.
2.7.3 数组的排序
SV有几个可以改变数组元素顺序的方法,可以对数组进行正排序、逆排序、或者打乱顺序,如例2.29所示。注意与数组定位方法的不同,排序方法改变了原始数组,而数组定位只是新建了一个队列来保存结果。
reverse和shuffle方法不能带with条件语句,所以他们的作用范围是整个数组。例2.30示范了如何用子域对一个结构进行排序:
2.7.4 使用数组定位方法建立记分板
数组定位方法可以用来建立记分板。例2.31定义了包结构(Packet),然后建立了一个由包结构队列组成的记分板。
例2.31中的check_addr()函数在记分板里寻找和参数匹配的地址。find_index()方法返回一个int队列。如果该队列为空(size==0),则说明没有匹配值。如果该队列有一个成员(size==1),则说明有一个匹配,该匹配元素随后被check_addr()函数删除。若该队列有多个成员(size>1),则说明记分板里有多个包地址和目标值匹配。
2.8选择存储类型
下面介绍基于灵活性、存储器用量、速度和排序要求正确选择存储类型的一些准则。这些准则只是一些经验法则。
2.8.1灵活性
如果数组的索引是连续的非负整数0、1、2、3等,则应该使用定宽或动态数组。当数组的宽度在编译时已经确定时选择定宽数组,如果要等到程序运行时才知道数组宽度的话选择动态数组。
2.8.2存储器用量
使用双状态类型可以减少仿真时的存储器用量。为了避免浪费空间,应尽量选择32比特的整数作为数据位宽。仿真器会把位宽小于32比特的数据存放到32比特的字里。例如,对于一个大小为1024的字节数组,则会浪费3/4的存储空间。使用合并数组有利于节省存储空间。
因为需要额外的指针,队列的存取效率比定宽或动态数组稍差。
2.8.3排序
SV能够对任何类型的一维数组进行排序,所以应该根据数组中元素增加的频繁程度来选择数组类型。如果元素是一次性加入的话,选择定宽或动态数组;如果是逐个加入的话则选择队列。
2.9使用typedef来创建新的类型
typedef语句可以用来创建新的类型。在verilog中可以为操作数的位宽和类型定义一个宏(macro),如例2.32所示:
这种情况并没有创建新的类型,只是在进行文本替换。在SV中可以采用下面代码创建新的类型。本文约定所有用户自定义类型都带后缀“_t”。
一般来说,即使数据位宽不匹配,SV都允许在这些基本类型之间进行复制而不会给出警告。用户自定义类型最有用的是双状态的32比特的无符号整数。在测试平台中,很多数值都是正整数,这种情况下如果定义有符号整数就会出问题。
2.10创建用户自定义结构
verilog最大的缺点是没有数据结构。在SV中可以使用struct语句创建结构,与C类似。但struct功能比类少,所以还不如在测试平台中直接使用类。类里面也包含数据和程序。struct只是将数据组织在一起,如果缺少可以操作数据的程序,那也仅仅是解决了一半的问题。由于struct是一个数据的集合,所以它是可综合的。如果想生成带约束的随机数据,则应该使用类。
2.10.1使用struct创建新类型
例2.36创建了一个名为pixel的结构,它有三个无符号的字节变量,分别代表红、绿、蓝。
例2.36的声明只是创建了一个结构变量。要想在端口和程序中共享它,则必须创建一个新的类型。如下:
2.10.2对结构进行初始化
你可以在声明或者过程赋值语句中把多个值赋给一个结构体,赋值时要把数值放在带单引号的大括号中。
2.10.3 创建可容纳不同类型的联合
如果需要以若干不同的格式对同一寄存器进行频繁读写,联合相当有用。但是不要滥用,不要仅仅因为想节约存储空间而使用联合,与结构相比,联合可能节省几个字节,但是必须创建并维护一个更加复杂的数据结构。
2.10.4合并结构
SV提供的合并结构允许对数据在存储器中的排布方式有更多的控制。合并结构是以连续比特集的方式存放的,中间没有空闲的空间。例2.37中的pixel结构使用了3个数值,所以占用了三个字长的存储空间,但实际上只需要三个字节。
2.10.5 在合并结构和非合并结构之间进行选择
如果对结构的操作很频繁,例如经常对整个结构体进行复制,那么使用合并结构效率会很高;如果操作经常是针对结构内的个体成员而非整体,那么应该使用非合并结构。
2.11类型转换
SV数据类型的多样性意味着需要在它们之间进行转换。
2.11.1静态转换
静态转换不对转换值进行检查。如例2.41所示,转换时指定目标类型,并在需要转换的表达式前加上单引号。
2.11.2动态转换
动态转换函数$cast允许对越界的数值进行检查。
2.11.3流操作符
流操作符《 和 》用在赋值表达式的右边,后面带表达式、结构或数组。流操作符用于把其后的数据打包成一个比特流。操作符》把数据从左至右变成流,而《把数据从右至左变成流。不能讲比特流结果直接赋给非合并数组,而应该在赋值表达式的左边使用流操作符把比特流拆分到非合并数组中。
也可以使用很多的连接符{ }来完成同样的操作,但是流操作符会更加简洁。如果需要打包或者拆分数组,可以使用流操作符来完成具有不同尺寸元素的数组间的转换。例2.43示范了队列之间的转换。
数组下标失配是数组间进行流操作时最常见的错误。数组声明中下标[256]等同于[0:255]而不是[255:0]。由于很多数组使用[high:low]的数组声明,使用流操作把它们的值赋给带[size]的下标形式的数组,会造成元素倒序。同样把非合并数组的数值赋给合并数组,则数值的顺序也会被打乱。
流操作符也可以用来将结构打包或拆分到字节数组中,如例2.44:
2.12枚举类型
枚举创建了一种强大的变量类型,它仅限于一些特定名称的集合,例如状态机中的状态名。最简单的枚举类型声明包含了一个常量名称列表以及一个或多个变量。
创建一个署名的枚举类型有利于声明更多的新变量,尤其是当这些变量被用作子程序的参数或模块端口时。你需要首先创建枚举类型,然后再创建对应变量。使用内建的name()函数,可以得到枚举变量值对应的字符串,如例2.46所示:
2.12.1定义枚举值
枚举值缺省为从0开始递增的整数,也可以定义自己的枚举值。在下面的例子中三个值对应的默认值为0,2,3。
枚举常量,如上例中的INIT,它们的作用范围规则和变量是一样的。因此,如果在不同的枚举类型中用到了同一个枚举常量名,那么必须在不同的作用域中声明它们。
如果没有特别指出,枚举类型会被当成int类型存储。由于int类型的缺省值是0,所以在给枚举常量赋值时务必小心。
在例2.48中,position会被初始化为0,这并不是合法的ordinal_e变量,因此把0指定给一个枚举常量可以避免这个错误,如例2.49所示。
2.12.2枚举类型的子程序
SV提供了一些可以遍历枚举类型的函数:
可以使用do...while循环来遍历枚举的所有值,如例2.50所示:
2.12.3枚举类型的转换
枚举类型的缺省类型为双状态int,可以使用简单的赋值表达式把枚举变量的值直接赋值给非枚举变量如int。但SV不允许在没有进行显示类型转换的条件下把整型变量赋值给枚举变量。SV要求显示转换的目的在于让你意识到可能存在的数值越界情况。
在例2.51中,$cast被当成函数进行调用,目的在于把其右边的值赋给左边的量。如果赋值成功,$cast返回1;如果因为数值越界而导致赋值失败,则返回0。例子中3对于枚举已经越界,这种强行赋值的方式不推荐。
2.13常量
verilog中创建常量最经典的方法是使用文本宏`define.它的好处是:宏具有全局作用范围并且可以用于位段和类型定义 。它的缺点同样是因为全局作用范围,在只需要局部常量时会引发冲突。
在SV中,参数可以在程序包里面声明,因此可以在多个模块中共同使用。也可以用typedef来替换宏,其次还可选择parameter.
SV也支持const修饰符,允许在变量声明时对其初始化,但不能在过程代码中改变其值。
2.14字符串
SV中的string类型可以用来保存长度可变的字符串,单个字符是byte类型。长度为N的字符串编号为0到N-1。注意与C不同的是,字符串的结尾不带标识符null,所有尝试使用字符“\0”的操作都会被忽略。字符串是动态的存储方式,所以不用担心存储空间。
例2.53示范了与字符串相关的几种操作:
在例2.53中,函数$psprintf()代替了verilog中的$sformat()。这个新函数返回一个格式化的临时字符串,并且可以直接传递给其他子程序。
2.15表达式的位宽
在verilog中,表达式的位宽是造成行为不可预知的主要源头之一。例2.54使用4种不同的方式实现1+1.在SV中可以通过对变量的强制转换来达到所需的精度。