SystemVerilog(1):数据类型、断言
工作中偶尔要写测试pattern和bus的性能测试,还是懂一点SystemVerilog好,不需要学得和验证一样精通,只希望能懂点基本的。声明:SystemVerilog系列博客是纯小白的笔记和流水账,没有任何营养价值,请谨慎阅读!
1、logic和bit
SV作为验证语言,不关心变量对应的逻辑应该被综合为寄存器还是线网,同时为了方便DV(IC验证)驱动和连接硬件模块,省去考虑reg和wire的精力,于是新引入了logic和bit。也就是说硬件端的reg和wire,在写SV时可以就写成是logic或bit,它们都是无符号型数据类型。
注意:logic和bit不能是多驱,即如果硬件端用的是inout wire类型,则就不能用logic/bit,只能老老实实的用wire。
1.1 logic和bit的比较
之所以有了四值逻辑logic,还要引入二值逻辑bit,是因为软件的世界即验证环境更多的是二值逻辑。相比四值逻辑logic,二值逻辑bit有利于提高仿真器的性能并减少内存的使用量。
//**************************************************************************
// *** 名称 : SV_02_bit_vs_logic.v
// *** 描述 : logic(4值)和bit(2值)的比较:
//**************************************************************************
module SV_02_logic_vs_bit;
initial begin: logic_vs_bit
bit bit_num;
logic logic_num;
$display("--------< test start >--------");
//---------------------------------------------------
logic_num = 'b1; $display("logic_num = %d", logic_num);
bit_num = logic_num; $display("bit_num = %d", bit_num);
// # logic_num = 1
// # bit_num = 1
//---------------------------------------------------
logic_num = 'b0; $display("logic_num = %d", logic_num);
bit_num = logic_num; $display("bit_num = %d", bit_num);
// # logic_num = 0
// # bit_num = 0
//---------------------------------------------------
logic_num = 'bx; $display("logic_num = %d", logic_num);
bit_num = logic_num; $display("bit_num = %d", bit_num);
// # logic_num = x
// # bit_num = 0
//---------------------------------------------------
logic_num = 'bz; $display("logic_num = %d", logic_num);
bit_num = logic_num; $display("bit_num = %d", bit_num);
// # logic_num = z
// # bit_num = 0
end
endmodule
1.2 四值逻辑的检查
如果DUT试图产生 x 或 z,采用 bit 后这些值会被转换为 0 或 1,使用 $isunknow 可以在表达式的任意位出现 x 或 z 时返回 1。
if($isunknow(iport)==1)
$display("@%0t: 4-state value detected on iport %b", $time, iport);
2、有符号数
大多数时候我们用的都是无符号数,logic 和 bit 也是属于无符号数,但有些时候我们需要用到有符号数,也要注意它们的区别。
2.1 byte和bit的比较
无符号数即所有位宽都是有效数字,例如 8'hff 表示的是 255。
有符号数的最高位是符号位,最高位为 1 表示负数,例如byte类型的最大值为 8'h0111_1111=127,最小值为 8'b1000_0000=-128,第二小的值为 8'b1111_1111=-127,最大的负数值为 8'b1000_0001=-1。
//**************************************************************************
// *** 名称 : SV_01_byte_vs_bit.v
// *** 描述 : byte(有符号数)和bit(无符号数)的比较
//**************************************************************************
module SV_01_byte_vs_bit;
initial begin: signed_vs_unsigned
byte byte_num;
bit [7:0] bit_num;
$display("--------< test start >--------");
byte_num = 'b1000_0000; $display("byte_num = %d", byte_num);
bit_num = byte_num; $display("bit_num = %d", bit_num);
end
// # --------< test start >--------
// # byte_num = -128
// # bit_num = 128
endmodule
2.2 数据类型转换
数据类型的多样性意味着可能需要在它们之间进行转换,转换分为隐式转换和显式转换两种,显示转换又分为静态转换和动态转换。
2.2.1 隐式转换
隐式转换,即直接赋值过去,不推荐这种转换,往往会带来意外的错误。
例1:
byte byte_num = 8'b1000_000;
bit [8:0] bit_num;
initial begin
bit_num = byte_num;
$display("bit_num = 'h%x", bit_num); //'h180,最高位符号位会复制1位
bit_num = unsigned'(byte_num);
$display("bit_num = 'h%x", bit_num); //'h080,正确转换,去掉了符号
end
例2:
logic [3:0] logic_num = 4'b111x;
big [2:0] bit_num;
initial begin
bit_num = logic_num;
$display("bit_num = 'b%b", bit_num); //'b110,最高位被截掉
end //x值(或z值)转为了0
2.2.2 显式转换
显式转换,即通过操作符号或者系统函数介入实现转换。
(1)静态转换
上面的例1中的 unsigned'(byte_num) 通过操作符号实现的转换便是静态转换,这种转换不会进行任何类型的检查,所以转换结果也可能会越界,并且我们难以察觉。
(2)动态转换
使用动态转换函数 $cast(a,b) 将右边的变量 b 的值赋值给 a,它会对转换时的越界进行检查,如果赋值成功, $cast 返回 1,如果因数值越界而导致赋值失败,则不进行转换,函数返回 0。
if($cast(a,b))
$display("cast sucessful !", b);
else
$display("cast failed for b = %0d" !!!, b);
如果把 $cast 当成 task 使用并且操作失败,那仿真时会打印出错误信息。
3、数组
3.1 定宽数组
SystemVerilog中的数组表示方法多了一种紧凑声明,它是和完整声明完全等价的。
3.1.1 单维数组和多维数组
SystemVerilog中的数组表示方法多了一种紧凑声明,它是和完整声明完全等价的。如果试图从一个越界的地址中读取数据,那么 SV 将返回数组元素类型的缺省值:如果元素是 logic 型,返回 x;如果元素是 int/bit,返回 0。
(1)单维数组
int array1[0:15]; //完整声明:16个32位宽的有符号整数[0]...[15]
int array1[16]; //紧凑声明:16个32位宽的有符号整数[0]...[15]
(2)多维数组
int array2 [0:7][0:3]; //完整声明:8行4列个32位宽的有符号整数
int array2 [8][4]; //紧凑声明:8行4列个32位宽的有符号整数
array2 [7][3]=1; //设置最后一个元素的值为1
3.1.2 合并数组和非合并数组
(1)合并数组
合并数组的存放方式是元素和元素之间是紧挨着的,声明时其元素个数写在变量名后面。
bit [3] [7:2] b_pack; //合并数组
(2)非合并数组
非合并数组的存放方式是每个元素都需要单独占据一个字节,可能会有空间的浪费,声明时其元素个数写在变量名前面。上面的单维数组和多维数组的示例都是非合并数组的写法,但由于int等关键字刚好占据一个字节,因此int型不会造成空间的浪费。
bit [7:0] b_unpack[3]; //非合并数组
合并数组的存放方式是元素和元素之间是紧挨着的,而非合并数组的每个元素都需要单独占据一个字节,会有空间的浪费。
3.1.3 数组初始化
写法:一个单引号加大括号来初始化数组,可以一次性地为数组的部分或所有元素赋值。
int array3[4] = '{0,1,2,3}; //定义时初始化初值
int array4[5]; //定义5个的32位宽的有符号整数
array4 = `{4,3,2,1,0}; //为这5个元素赋值
array4[0:2] = `{5,6,7}; //为前3个元素赋值
array4 = `{5{8}}; //赋值5个8
array4 = `{9,8,default:1}; //前两个元素赋值9和8,其余元素赋值为1
3.1.4 for和foreach
操作数组的最常见的方式是使用for或foreach循环,foreach循环中会自动声明内部的索引变量。
initial
bit [31:0] src[5]; //定义5个32位宽的无符号整数
for(int i=0;i<$size(src);i++) //$size返回的是数组的宽度,即5
src[i] = i; //元素值为0,1,2,3,4
foreach(dst[j]) //j效果等同于上面的i
dst[j] = src[j] * 2; //数组dst中的元素值是数组src中的元素值的2倍
end //元素值为0,2,4,6,8
在多维数组中,foreach的写法有点不一样,不同维度的变量可以写在一起,用中括号括起来,像 [i,j] ,写成 [i] 表示只遍历第一个维度,写成 [ ,j] 表示只遍历第二个维度。
initial
int md[2][3]=`{`{9,8,5},`{2,1,1}};//定义2行3列个元素,并初始化
foreach(md[i,j])
$display("md[%0d][%0d]=%0d",i,j,md[i][j]);
end
//打印结果如下所示:
//md[0][0]=9
//md[0][0]=8
//md[0][0]=5
//md[0][0]=2
//md[0][0]=1
//md[0][0]=1
3.2 动态数组
如果直到程序运行之前都还不知道数组的宽度,则可以使用动态数组。动态数组在声明时使用空的 [ ] ,必须调用 new[ ] 操作符来分配空间,意思是其宽度不在编译时给出,而在程序运行时再指定。
int dyn[] dyn2[]; //声明动态数组
initial begin
dyn = new[5]; //分配5个元素
foreach(dyn[j])
dyn[j]=j; //数组初始化
dyn2 = dyn; //数组复制,定宽数组复制给动态数组
dyn = new[20](dyn); //重新分配为20个新元素,并将旧的5个值赋值给其前5个元素
dyn = new[100]; //重新分配为100个新元素,旧值不复存在
dyn.delete(); //删除所有元素
end
只要基本数据类型相同(例如都是int),定宽数组就可以赋值给动态数组;如果元素个数相同,那么动态数组也可以赋值给定宽数组。当把定宽数组复制给动态数组时,可以省略 new[ ]。
3.3 队列
队列的声明是使用带有美元符号的下表:[$],注意不要对队列使用构造函数 new[ ] 。此外队列的初始化也不需要使用“ ` ”。
int j = 1;
int q1[$] = {3,4}; //队列初始化不需要用`
int q2[$] = {0,2,5}; //队列初始化不需要用`
initial
q1.insert(1,j); //在第1个元素前插入j:{0,1,2,5}
q1.insert(3,q2); //在第3个元素前插入队列q2:{0,1,2,3,4,5}
q1 = {q1[$:2],j,q1[3:$]};//在第2/3个元素中间插入j:{0,1,2,1,3,4,5}
q1.push_front(6); //在队列最前面插入6:{6,0,1,2,1,3,4,5}
q1 = {7,q1}; //在队列最前面插入7:{7,6,0,1,2,1,3,4,5}
q1.push_back(8); //在队列最后面插入8:{7,6,0,1,2,1,3,4,5,8}
q1 = {q1,9}; //在队列最后面插入9:{7,6,0,1,2,1,3,4,5,8,9}
j = q1.pop_front; //j等于队列最前面的元素7
j = q1[0]; //j等于队列最前面的元素7
j = q1.pop_back; //j等于队列最后面的元素9
j = q1[$]; //j等于队列最后面的元素9
q1.delete(1); //删除队列的第1个元素:{6,0,1,2,1,3,4,5,8,9}
q1.delete(); //删除整个队列
q2 = {}; //删除整个队列
end
可以把定宽数组或动态数组的值复制给队列。
4、struct结构体
结构体只是一个数据的集合,原始的写法如下:
struct{bit[7:0] r,g,b;} pixel;
然而这并没有声明卵用,要想在端口和程序中共享它,必须创建一个新的类型:typedef:
typedef struct{bit[7:0] r,g,b;} pixel;//声明结构体pixel
pixel u_pixel = {8'h00,8'hff,8'hff}; //共享为u_pixel,并初始化
可以把整数 i 和实数 f 存放在同一个结构体中,可以使用 typedef 创建联合:
typedef union {int i; real f;} union_struct; //创建联合结构体
union_struct u_union_struct; //共享
u_union_struct.f=0.0 //数值设置为浮点形式
可以为节约空间,把结构体写成合并结构:
typedef struct packed{bit[7:0] r, g, b;} pixel;
pixel u_pixel;
下面举了几个例子,说明 struct 结构体的使用:
//**************************************************************************
// *** 名称 : SV_04_struct.v
// *** 描述 : struct结构体的使用
//**************************************************************************
module SV_04_struct;
initial begin: struct_type
//声明结构体my_struct
typedef struct {
bit[ 7:0] addr;
bit[31:0] data;
int id;
} my_struct;
//声明3个结构体
my_struct u_struct_1, u_struct_2, u_struct_3;
$display("--------< test start >--------");
//--------------------------------------------------- 按序赋值
u_struct_1 = '{'h10, 'h1122_3344, 'h1000};
//全部打印用p,结果打印为10进制
//u_struct_1 data is '{addr:16, data:287454020, id:4096}
$display("u_struct_1 data is %p", u_struct_1);
//--------------------------------------------------- 按名赋值
u_struct_2.addr = 'h20;
u_struct_2.data = 'h5566_7788;
u_struct_2.id = 'h2000;
//u_struct_2 data is 2000
$display("u_struct_2 data is %0h", u_struct_2.id);
//--------------------------------------------------- struct赋值
u_struct_3 = u_struct_2; //总体赋值
u_struct_3.data = 'h99AA_BBCC;//变量修改
u_struct_3.id = 'h3000; //变量修改
//u_struct_3 data is '{addr:32, data:2578103244, id:12288}
$display("u_struct_3 data is %p", u_struct_3);
end
endmodule
5、enum枚举
枚举类型 enum 仅限于一些特定名称的集合,能够自动为列表中的每个名称分配不同的数值。使用内建的 name( ) 函数可以得到枚举变量值对应的字符串。
typedef enum{INIT, DECODE, IDLE} state_name;
state_name state_c state_n; //声明自定义类型变量
initial
case(state_c)
IDLE: state_n = INIT; //结构体赋值
INIT: state_n = DECODE;
default:state_n = IDLE;
endcase
$display("next state is %s", state_n.name()); //显示状态机名称
end
enum 可以自己定义枚举值,如果枚举值缺省,则为从 0 开始递增的整数(默认为 int 类型),例如下面的代码中使用 INIT 代表缺省值 0,DECODE 代表定义值 2,IDLE 代表缺省值 1。
typedef num(INIT, DECODE=2, IDLE) state_name;
下面举了几个例子,说明 enum 枚举类型的使用:
//**************************************************************************
// *** 名称 : SV_03_enum.v
// *** 描述 : enum枚举的使用
//**************************************************************************
module SV_03_enum;
initial begin: enum_type
//定义枚举类型my_enum,IDLE,START,PROC,END 默认对应 0123
typedef enum {IDLE,START,PROC,END} my_enum;
//声明两个新的枚举类型
my_enum u_enum_1,u_enum_2;
$display("--------< test start >--------");
//---------------------------------------------------
u_enum_1 = IDLE; //必须赋值框定的变量,否则出错,未赋值则打印不准
$display("u_enum_1 = %0d (int)", u_enum_1); //u_enum_1 = 0 (int)
$display("u_enum_1 = %s (string)", u_enum_1); //u_enum_1 = IDLE (string)
$display("u_enum_1 = %s (string)", u_enum_1.name()); //u_enum_1 = IDLE (string)
//.name是显示转换,可以省略
//---------------------------------------------------
u_enum_2 = my_enum'(1); //用原始enum进行赋值,不能直接写1
$display("u_enum_1 = %0d (int)", u_enum_2); //u_enum_1 = 1 (int)
$display("u_enum_1 = %s (string)", u_enum_2); //u_enum_1 = START (string)
//---------------------------------------------------
u_enum_2 = my_enum'(4); //4超过了原始enum默认框定范围,打印是错的
//可以改成{IDLE=1,START=2,PROC=3,END=4}来修改对应值
$display("u_enum_1 = %0d (int)", u_enum_2); //u_enum_1 = 4 (int)
$display("u_enum_1 = %s (string)", u_enum_2); //u_enum_1 = 4 (string)
end
endmodule
6、string字符串
SystemVerilog中的 string 类型可以用来保存长度可变的字符串,$sformatf( ) 和 $psprintf( ) 用于创建临时字符串,常常用于字符拼接。
//**************************************************************************
// *** 名称 : SV_05_string.v
// *** 描述 : string字符串的使用
//**************************************************************************
module SV_05_string;
initial begin: string_format
string s1, s2, s3, s4, s5, s6;//定义字符串
int i; //定义int
$display("--------< test start >--------");
//--------------------------------------------------- 赋值字符串
s1 = "Welcome";
s2 = "https://www.cnblogs.com/xianyufpga/";
//s1 content: Welcome
$display("s1 content: %s", s1);
//s2 content: https://www.cnblogs.com/xianyufpga/
$display("s2 content: %s", s2);
//--------------------------------------------------- 花括号拼接
s3 = {s1, " to ", s2};
//s3 content: Welcome to https://www.cnblogs.com/xianyufpga/
$display("s3 content: %s", s3);
//--------------------------------------------------- 函数法拼接:sformatf
s4 = $sformatf("%s to %s", s1, s2);
//s4 content: Welcome to https://www.cnblogs.com/xianyufpga/
$display("s4 content: %s", s4);
//--------------------------------------------------- 函数法拼接:psprintf
s5 = $psprintf("%s to %s", s1, s2);
//s5 content: Welcome to https://www.cnblogs.com/xianyufpga/
$display("s5 content: %s", s5);
//--------------------------------------------------- int转string:itoa
i = 2022;
s6.itoa(i);
//s6 content: 2022
$display("s6 content: %s", s6);
end
endmodule
7、断言
可以使用 SystemVerilog 断言(SVA)在你的设计中创建时序断言。断言在整个仿真过程中都是有效的,仿真器会跟踪哪些断言被激活,这样就可以在此基础上收集【功能覆盖率】的数据。
7.1 断言用法
测试平台的过程代码可以检查 DUT(待测设计) 的信号值和 TB(测试平台)的信号值,并且在有问题的时候采取相应的行动。
例如如果产生了总线请求,期望在两个时钟周期后产生应答,可以使用一个 if 语句来检查这个应答:
module test;
//......
u_bus.u_clocking.request <= 1; //clocking为时钟块
repeat(2) @u_bus.u_clocking;
if(u_bus.u_clocking.grant != 2'b01)
$display("Error: grant != 1");
//......
endmodule
如果使用断言来描述,那么语句更加紧凑,注意断言里的逻辑条件和 if 的比较条件相反,设计者应该期望括号内的表达式为真,否则输出一个错误。
module test;
//......
u_bus.u_clocking.request <= 1; //clocking为时钟块
repeat(2) @u_bus.u_clocking;
u_assert: assert(u_bus.u_clocking.grant == 2'b01);
//......
endmodule
如果产生了正确的 grant 信号,那么测试继续执行,否则仿真给出如下信息:
"test.sv",7:top.u_test.u_assert: started at 55ns failed at 55ns
offending `(u_bus.u_clocking.grant==2'b01)`
7.2 定制断言
断言有可选的 then 和 else 分句,可以定制你自己的输出消息,例如创建一个定制的错误消息:
u_assert: assert(u_bus.u_clocking.grant==2'b01)
grants_rec++; //成功则运行这句
else
$error("grant not asserted !"); //否则输出错误信息
这样如果 grant 不符合期望值,那么仿真给出如下信息:
"test.sv",7:top.u_test.u_assert: started at 55ns failed at 55ns
offending `(u_bus.u_clocking.grant==2'b01)`
Error: "test.sv",7: top.u_test.u_assert: 55ns
grant not asserted !
SystemVerilog 有四个输出消息的函数: $info (消息级别),$warning (警告级别),$error (错误级别),$fatal (严重级别),这些函数允许在断言内使用,而不允许在过程代码中使用,不过在 SystemVerilog 的后续版本中将被允许。
参考资料:
[1] 路科验证V2教程
[2] 绿皮书:《SystemVerilog验证 测试平台编写指南》第2版