数据结构
System Verilog引进了一些新的数据类型,它们有如下优点:
(1)双状态数据类型:更好的性能,更低的内存消耗;
(2)队列、动态和关联数组:减少内存消耗,自带搜索和分类功能;
(3)类和结构:支持抽象数据结构;
(4)联合和合并结构:允许对同一数据有多种视图(view); ——???这是什么意思?
(5)字符串:支持内建的字符序列;
(6)枚举类型:方便代码编写,增加可读性;
1、内建数据类型
相比于Verilog 将寄存器类型reg和net(线网)类型区分如此清楚,在SV中引入了logic数据类型。任何使用线网的地方均可以使用logic,但要求logic不能有多个结构性的驱动,例如在对双向总线建模时。此时,需要使用线网类型。
按值的状态分为:四值变量: 0、1、x、z四种状态;二值变量: 0、1两种状态;
按照2值和4值可分为:
四值逻辑类型有:logic,reg,integer,net-type(如wire、tri);
二值逻辑类型有:bit, byte,int,shortint,longint;
按照有无符号还可以分为:
有符号类型:integer, byte, int,shortint,longint;
无符号类型:logic,reg,bit,net-type(如wire、tri);;
对于这些变量的位宽不同:
bit b; // sigle bit
bit [31:0] b32 ; // 32bit unsigned integer
int unsigned ui ; // 32bit unsigned integer
byte b8; // 8bit signed integer
shortint s; // 16bit signed integer
int i; // 32bit signed integer
longint l; // 64bit signed integer
integer i4; // 32bit signed integer
timer t; // 64bit unsigned integer
real r; // double precision floating point
注意:
在遇到变量类型时,应注意它们的符号类型和逻辑类型,在变量的运算中,尽可能避免两种不一致的变量进行操作。
- 对于有符号类型,例如:byte变量的最大值是127,而不是255(它的范围是-128–127)。
- SV并不太常用变量类型是wire(assign语句中)还是reg(initial和always语句中)。
- logic用的比较多。可以被连续赋值语句驱动,可用在assign、initial、always语句中;
- 但logic不能有多个驱动,比如双向总线建模的时候需要用wire,主要体现在写interface时的双向端口,要定义为wire。
- 对于逻辑变量类型,四值变量的默认初始值为x,二值变量的默认初始值为0,在initial中可以直接使用~clk变成1,但是如果是logic,必须设置初值为0、或者1,如果logic变量设置为z态,则将其取反,则得到的仿真结果显示为X态.*
- 将四值变量赋值给二值变量,x和z状态会转变为0;
- 只有0 1两个状态,没有x和z态,有利于提高仿真器性能,减少内存使用。
- 对于转换方式,可以分为隐式转换和显式转换。显式转换又可以分为静态转换和动态转换;静态转换:unsigned’(signed);注意单引号。动态转换:$cast(tgt,src);
- 如果将双状态的变量连接到DUT的输出,那么如果输出x或z,将被转换成0或1,而不能检测到x z。使用$isunknown操作符可以在表达式位x或z的时候返回1(断言的用法).
参考资料
1、SystemVerilog 数据类型(一)
2、SystemVerilog——数据类型
3、system verilog(二)数据类型
4、System Verilog 系列学习(1)----数据类型
2、数组
2.1.定宽数组的声明与赋值
//一维定宽数组
int lo_hi[0:4]; //5个整数[0]...[4];
int c_style [5] ='{0,1,2,3,4}; //对5个元素初始化;
lo_hi = '{0,1,2,3,4}; //对5个元素赋值;
lo_hi[0:2] = '{0,1,2}; //对前3个元素赋值;
lo_hi[0:2] = '{3{3}}; //3个值全为3;
lo_hi = '{3,5,default:2}; //{3,5,2,2,2};
//非合并数组
int array1 [0:2][0:1]; //完整声明一个二维数组;
int array [8][4]; //紧凑的声明;
array [7][3]=2; //设置最后一个元素;
array1 = '{'{0,1},'{1,2},'{2,3}}; //数组赋值;
//合并数组
bit [3:0][7:0] bytes; //数组大小的定义格式为[msb:lsb],而不是[size];
bytes = 32'hCafe_Dada; //赋值;
//合并/非合并数组
bit [0:3][0:7]array [3]; //合并3*32比特
bit [31:0] 1w= 32'h0123_4567; //字
bit [7:0][3:0] nib; //合并数组;
array[0]=1w; //使用一个下标,可以得到一个字的数据;
array[0][3]=8'h01; //使用两个下标,可以得到一个字节的数据;
array[0][1][6]=1'b1; //使用三个下标,可以得到一个比特的数据;
nib = array[0]; //复制合并数组的元素值
注意:当测试平台通过存储器数据的变化来唤醒时,通过@操作符实现,这个操作符只能用于标量或者合并数组。例如上边代码中的非合并数组array,只可以将array[0]或者1w作为敏感信号,但不能用整个array数组,除非把它扩展成:@(array[0] or array[1] or array[2])。
可能用到的系统函数:
$dimensions(array_name) :返回数组的维度
$left(array_name,dimensions) :返回数组指定维度最左边索引值
$size(array_name,dimensions) :返回数组指定维度的大小
$bits(expression) :返回数组存储比特数
logic [1:2][7:0]word[0:3][4:1];
$left(word,1); //返回0
$left(word,2); //返回4
$left(word,3); //返回1
$left(word,4); //返回7
2.2 基本的数组操作----复制和比较
initial begin
bit [31:0] src[5]='{0,1,2,3,4},
dst[5]='{5,4,3,2,1};
if(src==dst)
$display("src==dst");
else
$display("src!=dst");
src=dst; //把dst的所有元素值赋给src;
$display("src[1:4] %s dst[1:4]",(src[1:4]==dst[1:4]?"==":"!=");//片段比较
end
2.3 基本的数组操作----for & foreach
SV中$size函数返回数组的宽度。在foreach循环中,只需要指定数组名,并在其后的()中给出索引变量,系统便会自动遍历数组中的元素,索引变量自动声明,并只在循环中有效。
initial begin
bit [31:0] src[5],dst[5];
for(int i=0;i<=$size(src)-1;i++)
src[i]=i;
foreach(dst[j])
dst[j] = src[i]*2;
end
3.动态数组
动态数组在声明时使用空的下标[ ],这意味着数组的宽度不在编译时给出,而在程序运行时指定。数组在最开始是空的,所以必须调用new[ ]操作符来分配空间,同时在方括号中传递数组宽度。
int dyn[],d2[]; //声明两个动态数组
initial begin
dyn = [5]; //分配5个元素
foreach (dyn[i]) dyn[i]= i; //对元素赋值
d2 = dyn; //复制一个数组
d2[0] =5; //d2[0]复制为5
$display(dyn[0],d2[0]);
dyn = new[20](dyn); //分配20个元素并赋值
dyn = new [100]; //分配100个新的整数值
dyn.delete(); //删除所有值
end
动态数组有一些内建的子程序,例如delete和size。
只要基本数据类型相同,比如都是int,定宽数组和动态数组之间可以相互赋值,在元素数目相同的情况下,可以把动态数组的值复制给定宽数组。也可以将定宽数组的值复制给动态数组,此时,SV会调用构造函数new[ ]来分配空间并赋值。
4.队列
队列的声明是使用带有美元符号的下标[$],但不需要使用构建函数new[ ]来开辟空间,。
队列的操作:
int j=1;
q2[$] = {3,4},
q[$] = {0,2,5},
initial begin
q.insert(1,j); //{0,1,2,5}
q.insert(3,q2); //{0,1,2,3,4,5}
q.delete(1); //{0,2,3,4,5}
q = {q[0],j,q[1:$]}; //{0,1,2,3,4,5};
q = {q[0:2],q2,q[3:$]}; //{0,1,2,3,4,3,4,5}
//下面操作执行速度很快
q.push_front(6); //{6,0,1,2,3,4,3,4,5}
j=q.pop_back; // j=5
q.push_back(8); //{6,0,1,2,3,4,3,4,5,8}
j=q.pop_front; //j=6
j =q[$]; //j=8
q ={q,8}; //{6,0,1,2,3,4,3,4,5,8,8}
q={}; //类似与q.delete
end
队列中的元素是连续存放的,所以队列的初始化赋值用{},而不加’。同样由于其是连续存放数据,所以,在队头和队尾pop/push数据非常方便,执行速度很快,这与队列的长度没有关系。但是在队列中间增加或者删除数据则需要搬移腾出空间,所以该操作耗时与队列的长度有关,应呈现线性相关。另可将动态数组,定宽数组的值复制给队列。
5.关联数组
针对与对一个非常大的地址空间寻址时,可利用关联数组,来保存这些稀疏矩阵的元素,只为实际写入的元素分配空间。
关联数组采用在 [ ]中放置数据类型的形式来声明,例如:[int], 可通过X.exists()索引某一元素是否存在。
initial begin
bit [63:0]assoc[bit[63:0]],idx =1;
repeat(64) begin
assoc[idx] = idx;
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",i,assoc[i]);
while(assoc.next(idx)); //得到下一个索引
end
end
6.数组的方法
6.1 缩减方法
所谓缩减方法是将一个数组变成一个值,常见的方法有sum(和),product(积),and(与),or(或)和xor(异或),对于以上运算,要特别注意变量的位宽。SV中没有提供专门从一个数组中随机挑选单个元素的方法,对于定宽数组,队列,动态数组和关联数组可以采用 u r a n d o m r a n g e ( urandom_range( urandomrange(size(array)-1),而对于队列和动态数组还可以采用$urandom_range(array.size()-1).
6.2 定位方法
1)定位方法可以帮助查找数组中的最大值,特定值之类的信息,但是注意这些方法的返回值通常是一个队列。
int f[6] ='{0,1,2,3,4,5};
int d[]='{2,1,6,3,5};
int q[$]= {1,5,6,5}, tq[$];
tq =f.max(); //{5}
tq =d.min(); //{1}
tq =q.unique(); //{1,6}
2)find定位方法:
int d[] ='{9,1,8,3,4,4},tq[$];
tq =d.find with(item>3); //{9,8,4,4}
tq =d.find_index with(item>3); //{0,2,4,5}
tq =d.find_first with(item>99); //{}
tq =d.find_first_index with(item==8); //{2}
tq =d.find_last with(item==4); //{4}
tq =d.find_last_index with(item==4); //{5}
6.3排序方法
所谓排序方法是对原有数组的数据进行重新定位的方法。
X.reserve() 倒序 不能带with使用
X.sort() 从小到大排序
X.rsort() 从大到小排序
X.shuffle() 随机 不能带with使用
7. typedef 自定义方法
7.1使用struct创建新类型
Verilog最大的缺陷就是没有数据结构,而在SV中可以通过struct语句创建数据结构,struct为一个数据的集合,所以它是可综合的。伴随着typedef可以用来创建新的类型,并利用新类型声明更多的变量。
struct {bit [7:0] r, g, b;} pixel; 创建了一个结构体
7.2枚举类型
枚举创建一种强大的变量类型,它仅限于一些特定名称的集合,例如指令中的操作码或者状态机的状态名。例如ADD,MOVE这些名称有利于编写和维护代码。枚举类型enum经常和typedef搭配使用,由此便于用户自定义枚举类型共享使用 。
typedef enum {INIT,DECODE,IDLE} fasmtate_e;// 定义一种数据类型,这个数据类型是fasmtate_e
fasmtate_e pstate,nstate; //声明自定义的枚举类型
initial begin
case(pstate)
IDLE: nstate=INIT; //数据赋值
INIT: nstate=DECODE;
default: nstate=IDLE;
endcase
$display("Next state is %s", nstate.name()); //显示状态的符合名
end
枚举值缺省是 为 从0开始递增的整数,如上缺省默认INIT为0,DECODE为1,IDLE为2。也可以自己定义,如下:
typedef enum {INIT, DECODE=2,IDLE} fsmtype_e; //INIT缺省为0,DECODE为2,IDLE为3
如果没有特别指出,枚举类型会被当成int类型,由于枚举类型缺省值为0,所以在给枚举常量赋值赋值时要小心。
typedef enum {INIT=1,DECODE,IDLE} fasmtate_e;
fasmtate_e pstate; //不正确
typedef enum {INIT=0,DECODE,IDLE} fasmtate_e;
fasmtate_e pstate; //正确
枚举类型的子程序:
- first() 返回第一个枚举常量;
- last() 返回最后一个枚举常量;
- next() 返回下一个枚举常量;
- next(N) 返回后N个枚举常量;
- prev() 返回前一个枚举常量;
- prev(N) 返回前N个枚举常量;
- num()返回枚举常量的格式;
- name()返回枚举常量的名字;
当到达枚举常量列表的头和尾时,函数next和prev会自动环形绕回。
注意,要在for循环中使用变量来遍历枚举类型中的所有成员并非易事。你可以使用first访问第一个成员,使用next访问后面的成员。问题在于如何为循环设置终止条件。如果使用current != current.last,则循环会在到达最后一个成员之前终止。
8. 字符串
与C不同,字符串的结尾并不带标示符null,所有尝试使用字符“\0”的操作会被忽略。字符串采用动态存储方式,所以不用担心存储空间会被用完 。
字符串常见的几种操作:
get(N):返回位置N上的字节
toupper 返回一个所有字符大写的字符串
tolower返回一个小写的字符串
{ }用于串接字符串
putc(M,C)把 字节C写到字符串的M位上
substr(start,end)提取出从位置start到end的所有字符。
string s
initial begin
s="IEEE ";
$display(s.getc(0)); //"I"
$display(s.tolower()); //ieee
s.putc(s.len()-1,"-"); //空格变为-
s={s,"P1800"}; //"IEEE-P1800"
$display(s.substr(2,5)); //EE-P
my_log($psprintf("%s %5d",s,42)); //创建临时字符串
end
流操作符
流操作符“ << ”和“ >> ”用在赋值表达式的右边(?这句话咋理解呢?没搞懂),后面带表达式、结构或数组。
流操作符用于把其后的数据打包成一个比特流。操作符>>把数据从左到右变成流,而<<则把数据从右至左变成流,如下面代码所示。
你也可以指定一个片段宽度,把源数据按照这个宽度分段以后再转变成流。不能将比特流结果直接赋值给非合并数组,而应该在赋值表达式的左边使用流操作符把比特流拆分到非合并数组。
initial begin
int h;
bit [7:0] b, g[4], j[4] = '{8'ha, 8'hb, 8'hc, 8'hd};
bit [7:0] q, r, s, t;
h = {>>{j}}; // 0a0b0c0d, 把数组打包成整型
h = {<<{j}}; // b030d050, 位倒序,具体见下示意图;
h = {<<byte {j}}; // 0d0c0b0a, 字节倒序
g = {<<byte {j}}; // 0d,0c,0b,0a, 拆分成数组
b = {<<{8'b0011_0101}}; // 1010_1100, 位倒序
b = {<<4 {8'b0011_0101}}; // 0101_0011, 半字节倒序
{>> {q,r,s,t}} = j; // 把j分散到四个字节变量里
h = {>> {t, s, r, q}}; // 把字节集中到h里
end
位倒序示意图:
也可以使用很多连接符{ }来完成同样的操作,但是流操作符用起来会更简洁并且易于阅读;
如果需要打包或拆分数组,可以使用流操作符来完成具有不同尺寸元素的数组间的转换。例如,你可以将字节数组转换成字数组。对于定宽数组、动态数组和队列都可以这样。如下示范了队列之间的转换,这种转换同时也适用于动态数组。数组元素会根据需要自动分配。
//使用流操作符进行队列间的转换
initial begin
bit [15:0] wq[$] = {16'h1234,16'h5678};
bit [07:0] bq[$];
bq = {>> {wq}}; // 12 34 56 78,把字(word?)数组转换成字节数组
bq = {8'h98, 8'h76, 8'h54, 8'h32}
wq = {>> {bq}}; // 9876 5432
end
数组下标失配是在数组间进行流操作时常见的错误。数组声明中的下边[256]等同于[0:255]而非[255:0]。由于很多数组使用[high:low](由高到低)的下边形式进行声明,使用流操作把它们的值赋给带[size]下标形式的数组,会造成元素倒序。同样,如果把声明形式为bit[7:0] src [255:0]的非合并数组使用流操作赋值给声明形式为bit [7:0] [255:0] dst的合并数组,则数值的顺序会被打乱。对于合并的字节数组,正确的声明形式应该是bit [255:0] [7:0] dst.
流操作符也可用来将结构(例如,ATM信元)打包或拆分到字节数组中。在下面例子中使用流操作把结构转换成动态的字节数组,然后字节数组又反过来转换成结构。
initial begin
typedef struct {int a;
byte b;
shortint c;
int d;} my_struct_s;
my_struct_s st = '{32'haaaa_aaaa,
8'hbb,
16'hcccc,
32'hdddd_dddd};
byte b[];
b = {>>{st}}; // {aa aa aa aa bb cc cc dd dd dd dd}, 将结构转换成字节数组
b = '{8'h11, 8'h22, 8'h33, 8'h44, 8'h55, 8'h66,
8'h77, 8'h88, 8'h99, 8'haa, 8'hbb};
st = {>>{b}}; // st=11223344, 55, 6677, 8899aabb
end