第2章 数据类型
2.1 内建数据类型
在Verliog中的基本数据类型的存储都是静态的,在整个仿真的过程中都是存在的。SystemVerilog增加了很多新的数据类型,以便帮助软件世界的设计与验证。
2.1.1 逻辑(logic)类型
按照逻辑类型进行划分,将常见的变量类型划分为:
四值逻辑,可以表示0、1、X、Z四种状态,默认初始值为x;
二值逻辑,只可以表示0和1两种状态,默认初始值为0。
注:将四值变量赋值给二值变量,X和Z状态会转变为0;可使用系统函数$isunknown(变量名)检查未知值的传播,若表达式中出现存在X或Z时返回1。
四值逻辑通常用于模拟外部物理世界,主要有:
1.reg 主要用在initial和always语句中,对线路建模。在SV中常用在寄存器类型;
2.wire 线网类型主要用在assign语句中,起连接作用。在SV中用在一个信号存在多个驱动时,例如双向总线建模;
3.logic logic是SV对经典reg数据类型的改进,使它除了作为变量外,还可以被连续赋值、门单元和模块所驱动。在任何使用线网的地方也可以使用logic,但要求logic不能有多个结构性的驱动。
4.integer、time
以上除了integer外均为无符号变量。
二值逻辑用于模拟计算机验证环境,要远离DUT,主要有:
1.bit 用户自定义位宽大小,无符号变量;
2.byte 8位宽,有符号变量,取值范围:-128~127;
3.shortint 16位宽,有符号变量,取值范围:-215~215-1;
4.int 32位宽,有符号变量,取值范围:-231~231-1;
5.longint 64位宽,有符号变量,取值范围:-263~263-1。
2.2 定宽数组
2.2.1 定宽数组的声明和初始化
在Verilog中定宽数组的声明必须给出数组的上下届,SV中允许只给出数组的宽度。在Verilog-2001中也引入了多维数组,但紧凑型的声明方式是SV中特有的。
// 定宽数组的声明
int lo_hi[0:15];
int c_style[16];
// 多维数组的声明
int array2[0:7][0:3]; //完整的声明
int array3[8][4]; //紧凑的声明
array[7][3] = 1; //设置最后一个元素
2.2.2 常量数组
一个单引号加大括号来初始化数组。初始化一个数组的例子:
int ascend[4] = ’{0,1,2,3}; // 对四个元素进行初始化
int descend[5];
descend = ’{4,3,2,1,0}; // 对五个元素赋值
descend[0:2] = ’{5,6,7}; // 为前三个元素赋值
ascend = ’{4{8}}; // 四个值都赋值8
descend = ’{9,8,default:1}; // 为缺省值统一赋值 {9,8,1,1,1}
2.2.3 基本数组操作--for和foreach
for或者foreach循环是操作数组最常见的方式。在SV中 $size 函数返回数组宽度。foreach循环中只需要指定数组名和其方括号中的索引变量,SV就能自能遍历数组中的元素,索引遍历会自动声明,但只在循环内有效。多维数组的索引值不是[i][j],而是[i,j]。在多维数组中如果不需要遍历所有维度,可以在循环中忽略。
// for 和 foreach循环
...
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;
...
// 多维数组的遍历
...
int md[2][3] = ’{’{0,1,2},’{3,4,5}};
foreach(md[i,j])
...
//
byte twoD[4][6];
foreach(twoD[i,j])
twoD[i][j] = i*10 + j;
foreach(twoD[i]) // 遍历第一个维度
$write("%2d:",i);
foreach(twoD[,j]) // 遍历第二个维度
$write("%3d:",twoD[i][j]);
end
end
// 输出结果
0:0 1 2 3 4 5
1:10 11 12 13 14 15
2:20 21 22 23 24 25
3:30 31 32 33 34 35
2.2.4 基本数组操作--复制和比较
比较简单,举例说明。
bit [31:0] src[5] = ’{0,1,2,3,4};
bit [31:0] dst[5] = ’{5,4,3,2,1};
dst = src;//把src的所有元素复制给dst
$display("src %s dst", (src==dst)?"==":"!=");//(src==dst)运算为1则为"==",为0则为"!="
$display("src[1:4] %s dst[1:4]", (src[1:4]==dst[1:4])?"==":"!=");// 也可以值比较1-4个元素
2.2.5 合并数组
合并数组可以作为数组,也可以当成单独的数据。它存放的方式是连续的比特集合,中间没有任何闲置的空间。而非合并数组中,字的地位用来存放数据,高位则不使用,存在空隙。SV仿真器在存放数组元素时使用32比特的字边界,所以shoortint和int都存放在一个字中,而longint则存放在两个字中。下面是飞合并数组与合并数组存放方式的示意图。
合并数组合并的位和数组大小作为数据类型的一部分必须在变量名前面指定。数组大小的定义格式必须是[msb:lsb],而不是[size]。
// 合并数组的声明
bit [3:0][7:0] bytes; //4个字节组装成32比特
// 合并/非合并混合数组声明
bit [3:0][7:0] barray [3]; //3×32比特
其中使用一个下标可以得到一个字的数据 barry[2]。使用两个下标可以得到一个字的数据barry[2][3]。使用三个下标可以得到单个比特位的数据barry[2][3][6]。操作若以比特为单位进行时,即使是数组维数不同也可以进行复制。
2.3 动态数组
SV提供了动态数组类型,可以在仿真中动态得分配空间调整宽度,这样在仿真中就可以使用最小的存储量。动态数组在声明时使用空的下标 [ ]。声明后数组在开始是空的,必须使用new[ ]操作符来分配空间,同时在方括号中传递数组宽度。
int dyn[],d2[]; // 声明动态数组
initial begin
dyn = new[5]; // 分配5个元素
foreach(dyn[j]) dyn[j] = j; // 对dyn内的元素初始化
d2 = dyn; // 复制动态数组
d2[0] = 5; // 修改第一个元素值
dyn = new[20](dyn); // 分配20个整数值并进行复制
dyn = new[100]; // 重新分配100个整数值,旧值不复存在
dyn.delete(); // 删除所有元素
end
动态数组有一些内建函数的子程序,例如delete和size可以直接使用。
2.4 队列
队列是SV新引入的一种数据类型,结合了链表与数组的优点,可以在队列中的任何位置增加或者删除元素。队列的这类操作在性能上的损失比动态数组小,且不需要像链表那样遍历所有元素。队列的声明符号是:[$],元素编号从0到$。队列中的常量只有大括号 { },而不用单引号 ‘{ }。在队列中插入元素时,SV会自动分配空间,即使元素增加到超过原有空间容量,也会自动分配更多空间,因为SV会随时记录闲置的空间。注意队列不需要像动态数组一样使用构造函数new[ ]。下面是队列操作的一些示例。
int j=1;
q2[$] = {3,4};
q[$] = {0,2,5}; // 队列的初始化不需要单引号
initial begin
q.insert(1,j); // {0,1,2,5} 在q[1]前插入元素j
q.insert(1,q2); // {0,1,2,3,4,5} 在q[1]前插入队列q2
q.delete(1); // {0,2,3,4,5} 删除第一个元素
q.push_front(6); // {6,0,2,3,4,5} 在队列的前面插入 6
j = q.pop_back; // {6,0,2,3,4} j=5 队列末尾元素弹出
q.push_back(8); // {6,0,2,3,4,8} 在队列末尾插入8
j = q.pop_front; // {0,2,3,4,8} j=6
foreach(q[i])
$display(q[i]); // 打印整个队列
q.delete(); // 删除队列
end
把$放在范围表达式的左边或者右边分别代表最小值和最大值。队列中元素是连续存放的,在队列前面或者后面存取数据所消耗的时间是一样的,在队列中间增加或删除元素就需要对剩余元素进行搬运,所以会消耗更多时间。允许把定宽或动态数组的值复制给队列。
int j=1;
q2[$] = {3,4};
q[$] = {0,2,5};
initial begin
q = {q[0],j,q[1:$]}; // {0,1,2,5}
q = {q[0:2],q2,q[3:$]}; // {0,1,2,3,4,5}
q = {q[0],q[2:$]}; // {0,2,3,4,5}
q = {6,q}; // {6,0,2,3,4,5}
j = q[$]; // j = 5
q = q[0:$-1]; // {6,0,2,3,4}
q = {q,8}; // {6,0,2,3,4,8}
j = q[0]; // j=6
q = q[1:$]; // {0,2,3,4,8}
q = {}; // 删除整个队列
end
2.5 关联数组
SV提供了关联数组类型,当对一个非常大的地址空间进行寻址时,SV只为实际写入的元素分配空间。仿真器使用树或哈希表的形式来存放关联数组。
//关联数组的声明、初始化
bit[63:0] assoc[bit[63:0]],idx = 1;
assoc[idx] = idx;
foreach(assoc[i])
$display("assoc[%h] = %h",i,assoc[i]);
2.6 链表(SV中避免使用它)
2.7 数组的方法
SV提供了很多数组方法,适用于任何一种非合并的数组类型,包括定宽数组、动态数组、队列和关联数组。
2.7.1 数组缩减方法
常用的方法有 sum(求和),product(积),and(与),or(或),xor(异或)。随机选取元素可以用函数$urandom_range( ),具体在6.10节中参考。
2.7.2 数组定位方法
数组定位方法返回的通常是一个队列而不是标量。
// 数组定位方法:max、min、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}
tq = d.max(); // {10}
tq = f.unique(); // {1,6,2,8} 排除数组中重复的值
find表达式搭配with条件语句可以帮助进行搜素,with条件语句中item是重复参数,也可以指定其他的名字,只要在参数列表中指出即可。
//数组定位方法:find
int d[] = ’{9,1,8,3,4,4}, tq[$];
tq = d.find with (item > 3); // {9,8,4,4} 找出所有大于3的元素
tq = d.find_index with (item > 3); // {0,2,4,5} 找出所有大于3的元素的指针
tq = d.find_first with (item > 99); // {} 找出第一个大于99的元素
tq = d.find_first_index with (item == 8); // {2} 找出第一个元素为8的指针
tq = d.find_last with (item == 4); // {4} 找出最后一个4元素
tq = d.find_last_index with (item ==4); // {5} 找出最后一个元素为4的指针
//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);
2.7.3 数组的排序
//对数组排序
int d[] = ’{9,1,8,3,4,4};
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} 乱序
2.8 适用typedef创建新的类型
typedef语句可以用来创建新的类型。通常自定义类型要带后缀 " _t "。
parameter OPSIZE=8;
typedef reg[OPSIZE-1 : 0] opreg_t;
opreg_t op_a,op_b;
2.9 创建用户自定义结构
SV中可以适用struct语句创建结构,和C语言类似。但struct功能较少适用的不多,常用类在后续章节有详述。
2.10 类型转换
类型转换可以分隐式转换和显示转换,隐式转换不需要操作符或系统函数转换,显示转换需要操作符或系统函数,显示转换包括静态转换和动态转换。
2.10.1 静态转换
静态转换不对转换值进行检查,转换时指定目标类型,在需要转换的表达式前加上单引号即可。
int i;
real r;
i = int ’(10.0 - 0.1);
r = real ’(42);
2.10.2 动态转换
动态转换函数$cast(tgt, src)允许对越界的数值进行检查,如果不越界返回1,否则返回0。
2.10.3 流操作
流操作符>>和<<用于把其后的数据打包成一个比特流。>>是把数据从左到右变成数据流,<<是把数据从右到左变成数据流。
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字节倒序
b={<<{8’b0011_0101}}; //10101100位倒序
b={<<4 {8’b0011_0101}};//0101_0011半字节倒序
{>>{q,r,s,t}}=j; //将j分散到四个字节变量里
h={>>{t,s,r,q}}; //将四个字节集中到h里
end
2.11 枚举类型
枚举类型声明包含了一个常量名称列表以及一个或多个变量,枚举类型中的各个变量默认类型为int型。枚举值缺省为从0开始递增的整数,可以自己定义枚举值。通常在我们把0指给枚举常量,可以避免一些不必要的错误。允许显示指定各个变量的基础类型。枚举类型通常以"_e "结尾。
enum {RED,BLUE,GREEN} color_e;
enum bit {RED,BLUE,GREEN} color_e;
enum logic[1:0] {RED,BLUE,GREEN} color_e;
// 把0赋给RED 1赋给BLUE 2赋给GREEN color_e是枚举变量名
2.11.1 枚举类型的子程序
SV提供了一些可以遍历枚举类型的函数。
(1)first() 返回第一个枚举变量
(2)last() 返回最后一个枚举变量
(3)next() 返回下一个枚举变量
(4)next(N) 返回以后第N个枚举变量
(5)prev() 返回前一个枚举变量
(6)prev(N) 返回以前第N个枚举变量
当到达枚举常量列表的头或尾时,函数next和prev会自动以环形方式绕回。枚举类型更推荐使用do...while循环来遍历,避免使用for循环,for循环容易导致死循环。
2.11.2 枚举类型的转换
枚举类型的缺省类型为双状态int。可以使用简单的赋值表达式把枚举变量的值直接赋给非枚举类型变量。但SV不允许在没有显示类型转换的情况下把整型变量赋值给枚举类型。
typedef enum {RED,BLUE,GREEN} COLOR_E;
COLOR_E color,c2;
int c;
initial begin
color = BLUE;
c = color; // c = 1; 把枚举类型直接赋值给整型是允许的
c++;
if (! $cast(color,c)) // 将整型转换为枚举型
$display("Cast failed for c = %0d", c);
$display("Color is %0d /%s", color, color.name);
c++; // c=3 对于枚举类型已经越界
c2 = COLOR_E ’(c);
$display("c2 is %0d", c2, c2.name);
// 静态的类型转换不做类型检查 实际上是转换失败的 因为已经越界了 所以尽量避免使用静态类型转换
end
2.12 字符串
SV中的string类型可以用来保存长度可变的字符串。单个字符是byte类型。长度为N的字符串,元素编号从0到N-1。字符串是动态存储的。下面示例了几种字符串的操作。
string s;
initial begin
s = "IEEE ";
$display(s.getc(0)); //显示:73(代表‘I’) getc(N) 返回位置N上的字节
$display(s.tolower()); //显示:ieee tolower() 返回小写的字符串 toupper()返回大写的字符串
s.putc(s.len()-1,"-"); // s = "IEEE-" putc(M,C) 把字节C写到字符串M位上
s = {s,"P1800"}; // s = "IEEE-P1800" {}大括号用于串接字符串
$display(s.substr(2,5)); //显示:EE-P substr(start,end) 提取从start到end的所有字符
my_log($psprintf(%s %5d),s,42); // $psprintf()这个函数返回一个格式化的临时字符串
end
task my_log(string message)
$display("@%0t: %s", $time, message);
endtask
2.13 一些补充
2.13.1 数制格式
<size> '<base><number>
'b 二进制 'd十进制 'o 八进制 'h十六进制
用“_”分隔符 提高阅读性