第二章. 数据类型
2.1 基本数据类型的两个属性
双状态/四状态
:根据存储数据类型中的每一bit位的可能数分为双状态类型和四值状态类型。
- 双状态:可能值0,1,双状态默认初始值为0。双状态值具有更低的内存消耗,更好的性能。
- 四状态:可能值0,1,X(未知状态),Z(高阻态),四状态默认初始值为X。
有符号数/无符号数
:根据数据有无符号分为有符号数(signed),无符号数(unsigned)。有符号数的最高比特位代表的是符号位(0为正,1位负)。
注:如果分配的数据类型为双状态值,但是被测试设计试图产生X或Z,这些值会被转换为双状态值。可以使用$isunknown
在表达式任意位出现X或Z时返回1。
2.2 基本数据类型分类
类型 | 双/四状态 | 描述 |
---|---|---|
bit | 双状态 | 1bit无符号数 |
byte | 双状态 | 8bit有符号整数 |
shortint | 双状态 | 16bit有符号整数 |
int | 双状态 | 32bit有符号整数 |
longint | 双状态 | 64bit有符号整数 |
integer | 四状态 | 32bit有符号整数 |
logic | 四状态 | SV独有数据类型 |
real | 双状态 | 双精度浮点数 |
time | 四状态 | 64bit无符号整数 |
其中logic为逻辑类型,四状态类型,此数据类型为SV独有。对verilog的reg数据类型进行改进而引入,可以被连续赋值,门单元和模块驱动。任何使用wire的地方均可以使用logic,但是不能有多个结构性驱动,如果logic存在多个驱动,编译时就会报错(如双向总线(inout)要被定义为wire类型)。
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
注:某些有符号数可以通过unsigned
声明为无符号数。
int unsigned ui; //双状态,32bit无符号整数
2.3 数据类型(数据结构)
2.3.1 数组
- 定宽数组:声明时指定宽度,宽度在编译时就确定了,属于静态数组。
//定宽数组声明
int lo_hi[0:15]; //16个整数[0]...[15]
int c_style[16]; //16个整数[0]...[15]
//多维数组声明,第一个数为行数,第二个数为列数,下面两个声明都为8行4列的二维数组
int array2[0:7][0:3]; //完整声明
int array3[8][4]; //紧凑声明
当为数组中的元素赋值常量时,需要使用一个'
加上大括号来初始化数组,这种数组叫做常量数组。
使用常量初始化数组(注意初始化方式):
int array_const [4] = '{0,1,2,3}; //对4个元素初始化
array_const = '{4{8}}; //四个值全部是8
array_const = '{1,2,default:1}; //{1,2,1,1}
注:如果从一个越界的地址中读取数据,SV将返回数组元素的缺省值,线网在没有驱动时输出的是Z。
- 动态数组
在仿真时分配空间/调整宽度,可以在仿真中使用最小的存储量。动态数组的宽度不在编译时给出,而是在程序运行时再指定,因此必须使用new[ ]操作符来分配空间,同时在方括号中传递数组宽度。可以把数组名传递给new[ ]构造符,并把已有数组的值复制到新数组里。
//声明动态数组
int dyn[], dyn2[];
dyn = new[5]; //分配5个元素
foreach(dyn[j]) dyn[j] = j; //初始化动态数组dyn
dyn2 = dyn; //复制动态数组dyn到dyn2
dyn = new[20](dyn); //分配20个元素并进行复制
dyn.delete(); //删除dyn中所有元素
注:系统函数$size()
返回数组的宽度,动态数组有内建方法,如delete
,size
等。当基本数据类型相同,定宽数组和动态数组之间可以相互赋值。元素基本数据相同,元素数目也相同时,可以把动态数组的值复制到定宽数组。
- 关联数组
当数组元素索引不连续时,如果用定宽数组存储会消耗过大的内存空间,内存利用率低。**SV中引入了关联数组保存稀疏数组。**仿真器可以使用树或哈希表的形式来存放关联数组,但是有一定的额外开销。与其他数组不同,关联数组的索引类型可以为其他类型,如字符串。
//关联数组声明,需要在数组名后使用[]声明索引的类型
bit[31:0] assoc[bit[63:0]]; //声明了索引为64bit,元素为32bit的关联数组assoc
bit[31:0] idx = 1;
//关联数组初始化
repeat (32)
begin
assoc[idx] = idx;
idx = idx << 1;
end
//关联数组的遍历
if(assoc.first(idx)) //first函数获得assoc第一个元素的索引
begin
do $display(“assoc[%h] = %h”, idx, assoc[idx]);
while (assoc.next(idx)); //next函数返回assoc下一个元素的索引
end
注:关联数组索引具有稀疏分布(起始索引可能不为0,索引不连续)。因此不能使用for循环进行遍历,而要使用foreach循环配合数组的first,next函数进行遍历。
2.3.2 队列
队列结合了链表和数组的优点。队列和链表相似,可以在队列中的任何地方增加或删除元素,这类操作在性能上的损失比动态数组小得多,因为动态数组需要分配新的数组并复制所有元素的值。队列与数组相似,可以通过索引实现对任一元素的访问,而不需要像链表那样去遍历目标元素之前的所有元素。队列类似标准模板库中的双端队列,可以通过队列的内建方法增加和删除队列元素,当队列元素增加超过原有的空间容量时,SV会自动分配更多的空间,表现为可以扩大或缩小队列长度。SV会随时记录闲置空间,不需要对队列使用构造函数new[]
。
//队列的声明,使用[$],队列元素编号从0到$
int queue [$], queue_2 [$];
queue [$] = {1,2,3}; //队列的常量不需要使用“'”
queue_2[$] = {4,5,6};
//队列的内建方法
queue.insert(1,7); //{1,7,2,3} 在索引为1的数据前插入1
queue.insert(2,queue_2); //{1,7,4,5,6,2,3} 在queue中插入队列
queue.delete(1); //{1,4,5,6,2,3} 删除索引为1的元素
queue.push_front(8); //{8,1,4,5,6,2,3} 队列前插入数据
queue.pop_back; //{8,1,4,5,6,2} 队列尾删除
queue.delete(); //删除整个队列
注:$
可以作为队列的索引,放在左边代表最小值,放在右边代表最大值。如对于长度为3的队列,[$:2]代表[0:2],[1:$]代表[1:2]。
可以使用下标串联来替代队列的方法
int q1[$] = {3,4};
int q[$] = {0,2,5};
int j = 1;
q = {q[0], j, q[1:$]}; //{0,1,2,5} 在元素2前插入1
q = {q[0:2], q1, q[3:$]}; //{0,1,2,3,4,5} 在元素2前插入q
q = {q[0], q[2:$]}; //{0,2,3,4,5} 删除索引为1的元素
q = {6, q}; //{6,0,2,3,4,5} 队列前插入元素6,相当于q.push_front()
j = q{$}; // j=5
q = q[0:$-1]; //{6,0,2,3,4} 队列尾取出数据,相当于q.pop_back()
q = q{q, 8}; //{6,0,2,3,4,8} 队列尾插入元素8,相当于q.push_back()
q = q[1:$]; //{0,2,3,4,8} 队列头取出元素,相当于q.pop_front()
q = { }; //{ } 删除队列元素,相当于q.delete()
队列中的元素是连续存放的,所以在队列前面或者后面存取数据非常方便。在队列中增加或删除元素需要对已经存在的数据进行搬移以便腾出空间,相应操作所耗费的时间会随着队列的大小线性增加。
2.3.3 链表
SV中提供了链表数据结构,但是应该避免使用它,因为SV中的队列更加高效易用。
2.3.4 枚举
宏的作用范围太大,枚举创建了一种强大的变量类型,它仅限于一些特定名称的集合。
//定义枚举类型,使用name()函数可以得到枚举类型对应的字符串
enum {RED, BLUE, GREEN} color;
//枚举值缺省为从0开始递增的整数,可以自己定义改变
typedef enum P{INIT, DECODE = 2, IDLE} fsmtype_e;
如果没有特别指明,枚举类型会被当成int类型存储。int类型的缺省值为0,所以在给枚举常量赋值时务必小心。
//非法,position会被初始化为0
typedef enum{FIRST=1, SECOND, THIRD}; ordinal_e;
ordinal_e position;
//正确
typedef enum{BAD_O=0, FIRST=1, SECOND, THIRED} ordinal_e;
ordinal_e position;
枚举类型提供的子程序
- first/last:返回第一个/最后一个枚举常量
- next:返回下一个枚举常量
- next(N):返回以后第N个枚举常量
- prev:返回前一个枚举常量
- prev(N):返回以前第N个枚举常量
当到达枚举常量列表的头或尾时,函数next和prev会以环形方式绕回。
//枚举类型的遍历
typedef enum {RED, BLUE, GREEN} color_e;
color_e color;
color = color.first;
do
begin
$display("Color=%0d/%s", color, color.name);
color = color.next;
end
while (color!=color.first);
枚举类型转换:
typedef enum {RED, BLUE, GREEN} COLOR_E;
COLOR_E color, c2;
int c;
color = BLUE; //赋一个已知的合法值
c = color; //将枚举类型转换为整型
c++; //整型自增1
if(!$cast(color, c)) $display("cast fail for c = %0d", c);
else $display("Color is %0d/%s",color, color.name);
c++; //c=3,对枚举类型已越界
c2 = COLOR_E'(c); //不做类型检查
$display("c2 is %0d/%s", c2, c2.name); //c2=3
2.3.5 字符串
SV中string类型可以保存长度可变的字符串,单个字符是byte
类型,长度为N的字符串中,元素编号从0到N-1。与C不同,字符串的结尾并不带标识符null
。字符串使用动态的存储方式
。
常用操作
get(N)
:返回位置N上的字节toupper
:返回一个所有字符大写的字符串tolower
:返回一个小写的字符串{}
:用于串接字符串
putc(M, C)
:把字节C写到字符串的M位上,M必须介于0到len所给出的长度之间。substr(start,end)
:提取出从位置start到end之间的所有字符
2.3.6 常量
SV中有好几种类型的常量。宏是最常用的一种,优点是宏具有全局作用范围并且可以用于位段和类型定义,缺点是因为具有全局作用域,可能会引起冲突。宏定义需要`符号,这样才能被编译器识别。参数是另一种类型的常量,常被定义在程序包中,可以在多个模块中共同使用。另一种是typedef自定义常量。SV还支持const关键字定义常量。
2.3.7 结构
V中没有数据结构,SV中可以使用struct语句创建结构,跟C语言类似。但SV中struct功能比较少,不如直接在测试平台中使用类,所以只做简单的介绍。struct可以把若干变量结合到一个结构中,struct只是一个数据的集合,所以它是可以综合的。struct变量类型统一使用_t
后缀进行标识。
//创建一个pixel结构,含有3个8bit变量r,g,b
struct {bit[7:0]r,g,b} pixel_s;
//struct初始化
pixel_s = '{8'b0000_0000,
8'b0000_0000,
8'b0000_0000
};
2.3.8 联合
包含不同数据类型的数据类型,统一使用_u
后缀标识。
//定义union
typedef union {int i; real f; } num_u;
num_u un;
un.f = 0.0;
//合并结构,当希望减少存储器的使用量时可以使用合并结构
typedef struct packed {bit [7:0] r, g, b} pixel_p_s;
pixel_p_s my_pixel;
合并结构和非合并结构的选择:
- 如果对结果的操作很频繁,如经常对整个结构体进行复制,那么使用合并结构的效率会比较高。
- 如果操作经常时针对结构内的个体成员而非整体,那么就应该使用非合并结构。
2.3.9 typedef自定义数据类型
//创建新的类型,约定用户自定义类型后缀为'_t'
parameter OPSIZE = 8;
tydef reg[OPSIZE-1:0] opreg_t;
注:可以把parmeter和typedef语句放到一个程序包package里使他们能被整个设计和测试平台所共用。
2.4 数组的基本操作
- 遍历
//遍历一维数组
bit [31:0] src[5], dst[5];
//使用for循环
for(int i=0;i<$size(src);i++) src[i] = i;
//使用foreach循环
foreach (dst[j]) dst[j] = src[j] * 2;
//遍历二维数组
int md[2][3] = '{'{0,1,2}, '{3,4,5}};
//使用嵌套for循环
for(int i=0;i<2;i++)
begin
for(int j=0;j<3;j++) $display("md[%0d][%0d]=%0d", i, j, md[i][j]);
end
//使用foreach循环(注意语法中i,j间有逗号)
foreach(md[i, j])
begin
$display("md[%0d][%0d]=%d", i, j, md[i][j]);
end
//使用拆分/嵌套的foreach循环(注意对列遍历的语法)
foreach(array[i]) begin
foreach(array[ ,j]) $display("md[%0d][%0d]=%d", i, j, md[i][j]);
end
- 聚合复制/比较
可以不使用循环来对数组进行聚合比较和复制(通过对数组名称进行比较和复制来完成对数组中整个元素的比较和复制),其中比较只限于等于和不等于比较。
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;
- 同时使用数组下标和位下标
bit[31:0] src[5] = ‘{5{5}};
//src[0]: 数组的第一个元素'd5或'b101
//src[0][0]: 数组的第一个元素的第一bit 'b1
//src[0][2:0]: 数组的第一个元素的第1,2bit,'b10
$display(src[0], src[0][0], src[0][2:1]);
2.5 数组的其他方法
SV提供了其他的数组方法,可以用于任意一种非合并数据类型,包括定宽数组,动态数组,队列和关联数组。如果数组方法不带参数,可以省略方法中的()
。
- 数组缩减
最常用的sum
,对数组中的所有元素求和。**默认情况下,sum
求和的结果与数组中元素的基本类型相同。**如数组array中的元素类型为单bit,array.sum
的结果就是单bit(可能存在精度损失)。但如果将array.sum
的结果存放在int类型的表达式/变量中,其结果就是32bit(位宽拓展)。其他数组缩减方法还有product
,and
,or
,xor
。
bit on[10];
int total;
foreach(on[i]) on[i] = i; //on[i]的值为0或1
$display("on.sum = %d", on.sum); //on.sum = 1,存在精度损失
$display("on.sum = %d", on.sum+32'd0); //on.sum = 5,on.sum+32'd0将位宽拓展到32bit
total = on.sum;
$display("total = %0d", total); //total = 5,位宽拓展
if(on.sum >= 32'd5) $display("sum has 5 or more 1"); //条件成立,打印结果
$display("int sum = %0d", on.sum with(int'(item))); //使用with表达式拓展位宽
- 数组定位
数组定位方法,通常返回的是一个队列。
//min,max,unique方法
int f[6] = '{1,6,2,6,8,6};
int d[] = '{2,4,6,8,10};
int q[$] = {1,3,5,7}, tq[$];
tq = q.min(); //{1} 返回队列q中最小的元素
tq = d.max(); //{10} 返回队列q中最大的元素
tq = f.unique(); //{1,6,2,8} 返回数组f中的唯一数值
//find方法
int d[] = `{9,1,8,3,4,4}, tq[$];
tq = d.find with (item > 3); //{9,8,4,4} 找出d中大于3的元素
tq = d.find_index with (item > 3); //{0,2,4,5} 找出d中大于3的元素的索引
tq = d.find_first with (item > 99); //{ }没有找到
tq = d.find_first_index with (item == 8); //{2} d[2] = 8
tq = d.find_last with (item == 4); //{4}
tq = d.find_last_index with (item == 4); //{5} d[5] = 4;
在条件语句with
中,item
被称为重复参数,他代表了数组中一个单独的元素。item
是缺省名称,也可以指定别的名字,只要在数组方法的参数列表中列出来就可以了。
//以下四个语句都是等同的
tq = d.find_first with (item == 4);
tq = d.find_first() with (item == 4);
tq = d.find_first(item) with (item == 4);
tq = d.find_first(x) with (x == 4);
以下为数组子集进行求和的方式。
int count, total, d[] = '{9,1,8,3,4,4};
//先对with表达式求解,在计算sum结果
count = d.sum with (item > 7); //2,元素大于7的个数
total = d.sum with ((item > 7) * item); //17,表达式中(item>7)返回0或1
count = d.sum with (item < 8); //4,元素小于8的个数
total = d.sum with (item < 8? item : 0); //12
count = d.sum with (item == 4); //2
- 数组排序
int d[] = '{9,1,8,3,4,4};
//reverse,shuffle方法不能与with结合,作用范围为整个数组
//sort,rsort方法还可以与with结合使用
d.reverse(); //'{4,4,3,8,1,9} 数组元素反向
d.sort(); //'{1,3,4,4,8,9} 升序排序
d.rsort(); //'{9,8,4,4,3,1} 降序排序
d.shuffle(); //'{9,4,3,8,1,4} 元素随机打乱
- 随机选择一个元素
SV中还提供了从数组中随机取出一个元素的方法。对于定宽数组,队列,动态数组和关联数组可以使用$urandom_range($size(array)-1)
,对于队列和动态数组还可以使用$urandom_range(array.size-1)
。如果想从关联数组中随机取出一个元素,你需要逐个访问它之前的元素,原因是没有办法直接访问第N个元素(无法直接获取第N个元素的索引)。
int aa[int], rand_idx, element, count;
element = $random_range(aa.size -1 );
//遍历关联数组,获取第element个元素的索引rand_idx
foreach(aa[i])
begin
if(count++ == element)
begin
rand_idx = i;
break;
end
end
$display("%0d element aa[%0d] = %0d", element, rand_idx, aa[rand_idx]);
2.6 合并数组与非合并数组
很多SV仿真器在存放数组元素时使用32bit的字边界(字长word,可以理解为存储最小单元)当数组元素不足32bit时也分配32bit的空间(数据从低位开始存放,未使用空间为高位),像byte(8bit),shortint(16bit),int(32bit)都存储在一个字长中,而longint(64bit)则存放在两个字长中。非合并数组就是按照上述规则分配内存空间,而合并数组就是不按照SV仿真器的字边界为最小单元分配空间,简单的来说合并数组就是将数组内的数据的bit位连续存放。
//合并数组与非合并数组的声明不同(便于记忆:合并数组数据个数/数据位宽集中在一起声明)
//声明非合并数组
bit[7:0] b_unpack[3];
//声明合并数组,注意[7:0]为第一个维度,[3:0]为第二个维度,存储为4个8bit数据而不是8个4bit数据
//合并数组的数组大小的定义格式必须是[msb:lsb],而不能是[size]
bit[3:0][7:0] b_pack;
内存空间示意图:
//合并和非合并混合数组的声明
bit {3:0][7:0] barray [3] //3*32bit [3:0]和[7:0]维度为合并数组 [3]维度为非合并数组
内存空间示意图:
合并数组和非合并数组的选择
- 如果需要和标量(基本数据类型)进行相互转换(如以字节或字为单位对存储单元进行操作)时,使用合并数组。
- 任意数组类型都可以合并,包括动态数组,队列和关联数组。
- 如果需要等待数组中的变化,必须使用合并数组。当测试平台需要通过存储器数据的变化来唤醒时,使用
@
操作符,@
操作符只能用于标量或者合并数组。
2.7 选择存储类型
索引为连续非负整数,则应该使用定宽或动态数组。宽度确定使用定宽数组,宽度可变使用动态数组。索引不规则,选择关联数组。元素变化很大的数组,可以使用队列代替数组。双状态较四状态可以较少仿真时的内存用量。
2.8类型转换
如果源变量和目标变量的比特位分布完全相同,那么他们之间可以直接相互赋值。如果比特位分布不同,需要使用流操作符对比特位分布重新调整。
- 静态转换
静态转换不对转换值进行检查。转换时指定目标类型,并在需要转换的表达式前加上单引号即可。
int i;
real r;
i = int '(10.0-0.1); //转换是非强制的
r = real '(42);
- 动态转换
动态转换函数$cast
允许你对越界的数值进行检查。
- 流操作符
流操作符<<
和>>
用在复制表达式的右边,后面带表达式,结构和数组**。流操作符用于把其后的数据打包成一个比特流**。操作符>>
把数据从左至右变成流,<<
则把数据从右至左变成流。可以指定一个片段宽度,把源数据按照这个宽度分段以后再转变成流。不能将比特流结果直接赋值给非合并数组,而是应该在赋值表达式左边使用流操作符把比特流拆分到非合并数组中。
//基本的流操作
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-按字节倒序打包(按照byte分组倒序打包流)
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里
如果需要打包或拆分数组,可以使用流操作符来完成具有不同尺寸元素(打包或拆分是指定位宽)的数组间的转换。**数组声明中[256]等同于[0:255]而非[255:0]。很多数组使用的是[high:low],使用流操作符赋值给带[size]下标形式的数组,会造成元素倒序。**流操作符也可用来将结构打包或拆分到字节数组中。使用流操作把结构转换成动态的字节数组,然后字节数组又被反过来转换成结构。
2.9 数据位宽
V中,表达式位宽是造成行为不可预测的主要原因。表达式的位宽依赖于上下文。
bit[7:0] b8;
bit one = 1'b1;
$display(one+one); //0,精度损失
b8 = one + one; //2,b8对结果位宽拓展
$display(one + one + 2'b0) //2,2'b0对结果位宽拓展
$display(2'b(one)+one); //2,位宽拓展
SV中可以通过对变量进行强制转换一达到期望精度。
参考文献:
SystemVerilog验证 测试平台编写指南(原书第二版)张春 麦宋平 赵益新 译