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版

posted @ 2022-06-26 16:17  咸鱼IC  阅读(3212)  评论(2编辑  收藏  举报