【chips】个人笔记系列-SystemVerilog

Title:System Verilog 学习

背景与发展

什么是SV

  • 啥是SystemVerilog?

    就是用来专门写验证和测试的 Verilog 升级版——在verilog的基础上加了些C++的思想、语法、模块。

  • 为啥要搞出一个SystemVerilog?

    设计IC (integrated circuit)时用的是 verilog,语法内有 intial 和 task 用来写 testbench. (跑过综合就明白,综合的IC code里不能有intial等的,intial等只能用于testbench或功能级IC code) 但即使如此,Verilog写testbench时也很不方便:模块化、随机化验证都不够。

    1999年OSCI推出的StstemC本质是C++库,写一些给IC code用的reference model时方便很多(尤其是算法类设计)。但C++的指针,玩不好容易让人心碎——发生内存泄露、写一些构建异常的测试用例都让人泪目。

    2002年推出的SystemVerilog取他们精华、去他们糟粕。是verilog的扩展版,加了C++的OOP(面向对象)元素(封装、继承、多态)来方便写验证代码,加了新的验证特性(随机化、约束、功能覆盖率),简化了内存管理(多了内存回收机制),调用封装好的C程序也很方便,会verilog的人贼容易上手它,是IC验证人员的好伙伴~

    (个人写习惯了C/C++,刚开始写verilog的testbench时有感触verilog磨唧…)

  • SystemVerilog和UVM的关系

    我们用SystemVerilog写验证,而UVM既是一个思考模式,也是被实现好的规范、通用的验证库。关系像是C++和其内的 std容器。 UVM文件都是SystemVerilog文件,即后缀 .sv ;

验证方法学

  • 三种验证方法
    • 断言验证 (Assertion Based Verification, ABV)
    • 覆盖率驱动验证 (Coverage Driven Verification, CDV)
    • 约束随机激励测试 (CR TB)

2 数据类型和编程结构

2.1 数据类型介绍

数值类型分类

  • SV内新增的数据类型——图示

    P.S. 阿这,SV里没有char类型,只有byte类型

    请添加图片描述

    • Verilog 1995的数据类型

      • 基本数据类型:reg 和 wire

        四值类型:(0/1/X/Z)

      • 其他得注意的类型:integer (四态、有符号

      变量统统默认是static的。

    • SV内新增的数据类型

      两态类型、枚举/联合类型、自定义类型、动态/关联数组、队列、字符串、类;

      • 两值类型(only 0/1):bit、byte、shortint/int/longint;

        1位、8位、16位/32位/64位;

        3种int和byte都是默认有符号的.

      • 四值类型:logic

  • 二值/四值的区别

    1. Z和X态是什么?

      • Z是高阻态,悬空未驱动,电阻相当于无穷大;

      • X是不定态,需要自行check逻辑确保一下;

    2. 2值的意义是比4值占用空间少一半,故module的端口用4值、module内变量用2值.

  • 二值/四值分类总结

    • 二值逻辑:(bit, byte, shortint/int/longint)

      默认初值为0.

    • 四值逻辑:(logic, reg, wire, integer, time)

      默认初值为x.

    • 符号类型:byte, shortint, int, longint, integer

    • 无符号类型:bit, reg, logic, wire

    • 其他类型:(实数 real,初值为0)

SV的logic类型

  • 作用

    四态数据类型,可以代替reg.

  • 引入logic类型的意义

    SV中引入了logic类型,因为dv人员无所谓变量是wire还是reg,用logic表示单纯赋值操作;logic类型是基于reg增强的,但又具有了连续赋值assign、当端口的各种特性,故dv人员用起来很方便——缺陷:不能被多个驱动

  • 限制

    双向总线和多驱动的情况下不能使用,此场景只能用wire.

  • logic和 reg、wire 的区别

    • wire是net类型,映射到硬件 → 导线;

      作用:传递、驱动、必须用assign连续赋值;

      兄弟类型:tri、wand、supply0、inout等

      不可赋初值

    • reg是variable类型,映射到硬件 → 寄存器(没被综合优化的话);

      作用:存储结构、必须用过程赋值;

      兄弟类型:integer、time、real、realtime等;

      可赋初值,但初值不可综合

    • logic类型

      编译器可自行推断logic是 reg还是wire. 可综合.

      但logic只能有1个输入,不可多重驱动(不可既是in口也是out口).

      注:Verilog有专门的inout端口写法,是用wire来实现的.

枚举类型 enum

  • 语法

    //使用方法1:
    	//定义
    enum 类型{		   //  `类型`可略,默认是 int
    name0 = value0,		// 不写 `value` 则默认 从0递增 
    name1,
    name2 = value2    
    }var_name;		// 是变量,不是类型!
    
    	// 赋值
    var_name = name0;	// 不用加域
    	// 打印
    $display("变量var_name的name: %s, 值: %0d", var_name, var_name);
    
    //使用方法2:
    	//定义
    typedef enum 类型{
    name0 = value0,
    name1,
    name2 = value2    
    }var_type;		// 是类型,不是变量!
    
    	// 赋值
    var_type var_name = var_type::name0;	// 需要加域
    	// 打印
    $display("变量var_name的name: %s, 值: %0d", var_name, var_name);
    
    • 枚举的“子变量类型”与“子枚举内部值”可略

      “子变量类型” 默认是 int

      “子枚举内部值” 默认 是 从0递增接着前者增加

    • 枚举变量的赋值:可自动转换为int值;

      赋值为内部定义的枚举值时,不用加域

    • 枚举变量的打印,%s%d,会自适应打印name或value.

  • 内建函数

    • .first() 返回此类型第一个枚举值 (enum);
    • .last() 返回此类型最后一个枚举值 (enum);
    • .next(int N) 返回下N个枚举值,默认是下1个 (enum);
    • .prevt(int N) 返回上N个枚举值,默认是上1个 (enum);
    • .num() 返回此类型枚举值的个数 (int);
    • .name() 返回此枚举值的名称 (string);

自定义类型 typedef

  • 作用

    1. 起别名

      可以给任何已有的数据类型、联合体等,起新名字(别名)。

    2. 自定义新的变量类型

    3. 提前声明 (forward typedef):

      用户自定义类型,必须在使用前被定义(同C/C++)。

      故,可以在使用前,用typedef提前声明,后续再定义。

  • 语法

    //起别名
    typedef int hyp_defined_super_dilicious_type;	// 给原有类型起别名
    parameter SIZE = 666;
    typedef reg [SIZE-1:0] usr_type;	// 自定义类型:封装为向量
    typedef int Array[100];	// 自定义int的数组类型,大小100;使用时: Array a;
    typedef int Queue[$];	// 自定义int的队列类型;使用时:Queue q;
    
    // 提前声明
    typedef class usr_class;		// 提前声明自定义的 类或其他类型 
    typedef enum usr_enum;
    typedef 
    

字符串类型 string

  • 声明/初始化语法

    string name1;
    string name2 = "abc";
    

    串若未初始化,默认为空串

  • 运算符

    • == / != 字符串判等 or 判不等
    • < / <= / > / >= 字符串按字典序比大小
    • {} 字符串拼接
    • {n {Str}} 字符串复制n次
    • 串[i] 打印串的第i个字符(第i个字节)
  • 内建函数

    • .len() 返回串的长度,不含终结字符;

    • .putc(index, c) 把index的字符替换成c字符;越界访问不报错,不改原串

      :SV中单个字符也得用"",别用'';或者用ASCII码的整数。

    • .getc(index) 返回串[index]字符的ASCII;越界访问不报错,不改原串

    • .toupper() 返回串的大写形式;不改原串

    • .tolower() 返回串的小写形式;不改原串

    • .compare(string) 和其他串按字典序比较;类似C语言的strcmp()

    • .icompare(string) 和其他串按字典序比较,不区分大小写

    • .substr(i, j) 返回index是 [ i , j ] [i,j] [i,j] 的子串;越界则返回"" 空串.

    • .atoi() 将串转为数字——从串的头部(左边)寻找数字前缀,遇到非数字就停止;没找到就返回0.

    • .atohex() 同上,找前缀的十六进制串,遇到非数字就停止;没找到就返回0.

    • .atooct() 同上,找前缀的八进制串,遇到非数字就停止;没找到就返回0.

    • .atobin() 同上,找前缀的二进制串,遇到非数字就停止;没找到就返回0.

    • .atoreal() 同上,找前缀的浮点串,遇到非浮点数就停止;没找到就返回0.

    • .itoa(value) 把整数value,转为串存下来;

    • .hextoa(value) 把十六进制数value,转为串存下来;

    • .octtoa(value) 把八进制数value,转为串存下来;

    • .realtoa(value) 把实数value,转为串存下来;

结构体

  • 作用

    和C里的struct一样。

  • 定义与赋值语法

    1. 直接声明变量;

    2. typedef声明自定义类型,再用类型定义变量;

      (和别的数组啥,flow一样其实)

    // 方法1:
    struct{
        // 自定义成员
        int a;
        int b;
    }usr_var;
    
    // 方法2:
    typedef struct{
        // 自定义成员
    }usr_Type;
    usr_Type tmp;
    
    // 赋值方式
    c = '{a:0, b:0.0};		// 用成员名进行赋值
    c = '{default:0};		// 设置默认值
    c = '{int:1, real:0.0};	 // 为所有“此类型”的成员,设置默认值.
    
  • 结构体数组

    typedef struct {
      int a;
      real b;
    }Node;	// 定义结构体
    Node arr[1:0] = '{ '{1, 1.0}, '{2, 2.0}};  // 结构体数组的初始化
    
    // X: 非法的赋值过程(不能“并”在一起赋值)
    Node arr[1:0] = '{ 1, 1.0, 2, 2.0};  // 结构体数组的初始化
    
  • 压缩(packed)、非压缩(unpacked)与存储结构

    结构体可被定义为“压缩” 或 “非压缩” 的状态。这将影响其内成员的存储方式与计算方式。

    • 压缩结构体:内部成员将无缝拼接在一起,相当于1个自定义vector,参与运算时也可整个以vector进行计算——“压缩”的概念.

      • 压缩结构体可被视为1个vector,故可分为signed / unsigned,默认是unsigned

      • 压缩结构体内只要含有四态成员,整体struct即为四态类型;

      • 压缩结构体内不允许用real等非整数类型变量、非压缩数组

        (“压缩/非压缩”数组的内容,见后面)

    • 非压缩结构体:内部成员以字节为最小粒度进行存储,不足1字节就补齐。

      默认是非压缩结构体.
      请添加图片描述

    // 压缩类型、带符号
    struct packed signed{
        //...
    }packed_var;
    
    // 默认是非压缩结构体 (unpacked)
    // 就算是压缩结构体,默认也是 unsigned 的
    
    struct{
        bit [3:0] data[4][5];
    }mem;
    initial begin
        $display("%x",mem);		// 非法! 这可是非压缩结构体,不能当vector用.
        $display("%x",mem.data[2][1]);		// ok!这下用的data的压缩部分.
    end
    

常量

  • 定义

    不会被改变的数据。

  • 分类

    simulator中,一般分为compile time(编译环节)、elaboration time(映射计算hierarchyparameter的override传递 环节)、run-time(具体执行代码、仿真的环节)的三个运行环节。

    SV中提供了 3种elaboration-time类型 和 1种run-time类型的常量

    • elaboration-time类型

      必须定义时初始化,将在elaboration-time中被赋值并一直固定。

      • parameter
      • localparam
      • specparam

      可用于 module、interface、program、class、package.

    • run-time类型

      • const
  • 语法格式

    • parameter

      • 普通定义

        parameter [数据类型/默认int/也可以为type类型] 参数名 = 参数值;

      • 模板内类型定义

        parameter type T=shortint; 定义模板里的类型.

      • 使用$,表示无上限的边界

        parameter rv = $;
        property inq(lv,rv);
        endproperty
        assert inq(3);
        
    • localparam

      同上,但localparam不能在例化时通过传入进行更改,只能内部使用.

    • specparam

      只能在specify block内使用,用于定义延时参数.

      先略。

    • const

      • 特点

        虽然类似localpram,但因为是run-time阶段赋值,故可赋更多元的变量类型(甚至可以为OOP对象)、被层次化引用.

      • 定义方式

        const 类型 变量名 = 值;

        • 右边可以用new()、new()内实参必须是常量表达式
        • 但此变量不可被重写。

文本

  • 数值

    • 指定位宽

      e.g. 4'b1001

    • 指定位宽

      e.g. 4356默认十进制

      e.g. 'b10000110 自行确定位宽.

    • 批量所有位赋值

      使用'符号、并不指定基数:

      bit [7:0] data;
      data = '1;		// data为 8'b11111111;
      

      我靠,我一直以为是等于1!!!

      • 注意'1'b11 三个的区别!
  • 字符串

    • 语法:用""包裹.

    • 多行字符串:行尾 \来换行. (新行空白格也将被算作字符

    • 末尾间隔符:似乎会插入\0作为间隔符(和C一样,ASCII值为0).

      我很怀疑有没有…因为打印不出此间隔符…

  • 时间表示

    • 时间文本被解释为 realtime类型,将根据当前的时间精度四舍五入。
    • 可选的时间单位有:s、ms、us、ns、ps、fs.

注释

  • 同C,有行注释//和块注释\*, *\.

2.2 赋值语句

变量间赋值

  • 四态值赋给两态值

    这是合法的;所有XZ都会变成0.

    • 但关键是要检查是否有“未知态X传播”:

      Solution: 用$isunknown(),若表达式内有XZ,则返回1.

参数传递与设置

SV提供了四种参数设置/传递的方式:

  1. 顺序赋值

    类型 # (值1, 值2, ..., 值i);

  2. 显示引用形参名赋值

    类型 # (.形参名1(值1), .形参名2(值2), ..., .形参名i(值i));

  3. defparam语句(不推荐)

类型强制转换

  • 语法

    // 取一个极端的例子,强转成自定义类型
    typedef enum{red, green, blue}color;
    initial begin
        color tmp;
        tmp = color'(2);	// 把2转成color类型.
    end
    

模块例化的信号传递 ⭐️

Verilog/SV提供了三种模块例化的信号传递方式:

  1. 自动的默认连接:同名信号自动连接

    模块名 实例名(.*);

  2. 位置关联

    按定义顺序传递给module端口形参.

    模块名 实例名(实参1, 实参2, ...);

  3. 名称关联

    可不按定义顺序传递给module端口形参,便于日后端口修改后的维护.

    模块名 实例名(.形参名(实参名),...);

module Test( input A, input B, output C );
    ...
endmodule
Test test1( .* );					// 默认连接
Test test2( a1,b1,c1 );					// 位置关联
Test test3( .B(b2), .C(c2), .A(a2) );	// 名称关联

信号强制

  • 语法

    // 强制赋固定值
    force 变量 = 值;	//"值"可以使用变量
    // 释放变量
    release 变量;
    

2.3 操作符/运算符与表达式

逻辑运算符、关系运算符

同C,Verilog新增有“全等===”、“非全等!==”;

仅SV支持、Verilog不支持:“通配等于==?”、“通配不等于!=?”;

  • SV的 =======? 的区别?

    都是判断“不等”,但判断的数据范围不同。

    ===能处理xz态数据,==不能。

    • ==和``!= 是两值运算符,但<font color=red>若有xz态数据,返回结果是x`。

      不管是不是两边的x一样,返回结果都是x

    • ===!==是四值运算符,要求两边的数据的xz态也一毛一样才行

      不管是不是两边的x一样,如果希望===返回的结果是1,两边数据的xz必须一样。

    • ==?!=?会忽略两边的x和z数据,进行比较。

      但我做实验,发现VCS虽然不报错,但还是把他俩按照==!=来用,没啥用啊

算数运算符

同C,新增有“幂运算 **”,支持自增++、自减--、自己运算自己的操作符*=, +=

位运算符

同C,新增有“异或非(同或)~^/^~”;

  • 注意'1|'x='1, '0|'x = 'x,0和1的强逻辑属性.

归约运算符

  • 释义

    (i.e. 对单操作数执行的“位运算操作”)

  • 运算符

    同上.

移位运算符

左移<<、右移>>

仅SV支持、Verilog不支持:算数左移(含符号)<<<、算数右移(含符号)>>>

拼接运算符

  • 非重复拼接:{I1, ..., I2}

  • 重复n次拼接:{n {I1, ..., I2}}

    可“嵌套使用”.

  • index成员访问

    单个:num[i]

    固定区间:num[i:j]

    仅SV支持、Verilog不支持:动态向右num[i+:len]/num[i-:len]

  • SV的“串拼接”与C++的区别

    C++和Python可以用+SV不能用+,而是用{}的…

2.4 控制语句与结构

条件选择

  • if-else

    if(条件1) begin
    end
    else if(条件1) begin
    end
    else begin
    end
    
  • case

    有三种case:

    1. case(普通case);
    2. casex(表达式中的x,都不参与比较);
    3. casez(表达式中的xz,都不参与比较).
    case(表达式)
        // value可以用 'x' 和 'z' 进行多选.
        value1, value2: begin end
        value3: begin end
        value4: begin end
        default: begin end
    endcase
    
    • SV的case 与 C的switch 的区别

      1. C里,若没有break,就会执行多个case

      2. SV里,默认一个case只执行一行指令、会自动break

        若想多个case执行同一块代码,多个case就用逗号隔开(如上述 value1、value2).

循环语句

  • for

  • while

  • do-while

  • repeat

  • forever

  • foreach ——需要传入循环变量.

    repeat(次数) begin
    end
    
    forever begin
    end
    
    foreach(数组名[循环遍历i]) begin
        $display("%0d, %0d", i, num[i]);
    end
    

2.5 数组与队列

  • 概述

    数组有三种:

    1. 静态数组(static array);

      大小在声明时,确定。

    2. 动态数组(dynamic array);

      可以多维或一维。但必然有一维,大小未指定,在使用前才分配。

    3. 关联数组(associative array)。

      hash数组,相当于C/C++的map.

    然后,还有队列(queue)。

  • 共用的系统函数

    • $size(...) 查看数组元素个数,空则为0.

静态数组

  • 声明语法

    又分为:压缩数组(packed array)与非压缩数组(unpacked array)

    • 压缩数组:维数的定义在变量标识符之

      所谓"压缩”部分,即“视为一个vector数据”.

    • 非压缩数组:维数的定义在变量标识符之

    bit [7:0] data1;	// packed array
    bit data2 [7:0] ;	// unpacked array
    
    // 非压缩数组的初始化:
    int num[0:2][1:2] = '{'{1,2}, '{3,4}, '{2{5}}};
    int num[0:2][1:2] = '{2{'{3{1,2}}}}; // 相当于 '{'{1,2,1,2,1,2}, '{1,2,1,2,1,2}}
    // 使用默认值
    int num[0:2][1:2] = '{1:1, default:0}; // index-0为1,其他为0
    

    :变量名左边压缩部分右边非压缩部分.

    类型 [Prange1]...[PrangeN] 变量名 [Urange1]...[UrangeM];

  • 大小定义

    压缩部分的维数必须写范围

    非压缩维数(右边)可以不写成范围、直接写大小:

    // 以下俩等价
    bit [7:0] data1 [999:0][1:0];
    bit [7:0] data2 [1000][2];
    

    若数组被定义为有符号数,则:压缩数组内是有符号数、非压缩数组内算无符号数。

  • 类型定义

    • 压缩数组的限制

      被“预定义位宽”的数据类型不能声明成压缩数组:byte、shortint/int/longint、integer.

      可以认为它们已经固定了隐含了压缩数组部分——左边index>0、右边index是0.

      // 以下俩映射后是一致的.
      integer data1 [2];
      logic signed [31:0] data2 [2];
      
    • 非压缩数组的限制

      无.

  • 访问方式

    • 普通访问

      先非压缩维数、再压缩维数

      变量名[Urange1]...[UrangeM][Prange1]...[PrangeN]

    • 部分访问

      • 整体读写:A = B

      • 部分读写:A[i:j] = B[i:j]

        i到j范围的index.

      • 可变切片读写:A[i+:j] = B[i+:j]

        从i的index开始,向上数j个的范围;同理,减号也行.

      • 单个成员的读写:A[i] = B[i]

      • 整体/部分的比较:A == B 或者 A[i:j] == B[i:j]

  • 存储的映射关系

    先存非压缩、再存压缩. (同访问顺序)

    注:这里的由低到高还是由高到低,我也不太清楚.

    // e.g. 32位的向量——写成4个字节大小的寄存器.
    bit [3:0][7:0] reg_32;
    reg_32 = 32'hdead_beef;
    bit [3:0][7:0] mix_array[3]
    

    请添加图片描述

  • 内建函数

    先略.

动态数组

  • 概念定义

    数组的非压缩部分的一个维度,其大小可动态

  • 声明语法

    类型 数组名 [];

    但使用前,必须得 new[N] 确定大小. ——❗️ 只有动态数组要new.

    bit [3:0] list [];
    integer mem[2][];	// 二维的动态数组,或理解为 “一个动态数组”的数组
    
  • 声明大小/数组拷贝/数组扩容

    int num1[], num2[];
    num1 = new[100];			// 数组-声明大小
    num2 = new[100](num1);		// 动态数组-拷贝:方法1
    num2 = num1;				// 动态数组-拷贝:方法2 (是深拷贝)
    num1 = new[200](num1);		// 动态数组-扩容
    

    注意:四态动态数组,定义完大小后,元素初值为x而不是0

  • 内建函数

    • .delete() 删除数组
    • .size() 查看数组大小

关联数组

  • 概念定义

    就是hash数组.

  • 声明语法

    value类型 数组名 [key类型];

    • key类型可以使*,则表达“key可为任意类型”.
    • 未定义的key,value是x0,取决于是四态还是二态——不会报错.
    integer num1[*];			// key可为任意类型
    initial begin
        num1[0] = 6;
        num1["abcd"] = 66;
        num1[10] = 666;
    end
    int num2[int unsigned] = '{1,2, 3, 4};
    
  • 内建函数

    • .size() / .num() 查看数组大小,空则为0;
    • .delete(inedx) 删除某个元素,默认清空整个数组;
    • .exist(key) 查看元素是否存在,存在1不存在0;
    • .first(ref index) index的值将变为首个key的位置、返回值是数组是否为空的标识,数组空返回0,非空返回1;
    • .last(ref index) index的值将变为末尾key的位置、返回值是数组是否为空的标识,数组空返回0,非空返回1;
    • .next(ref index) 传入index,查找下一个index并更新此变量,若更新成功返回1,已经是最后了返回0;
    • .prevt(int N) 传入index,查找上一个index并更新此变量,若更新成功返回1,已经是最后了返回0;

队列

  • 概念定义

    队列,类似动态数组,是一维的非压缩数组。

    首元素index是0,最后元素是$.

  • 声明语法

    • 普通队列

      类型 队列名[$];

    • 限制最大长度len的队列

      类型 队列名[$:len];

    reg [4:0] q[$];				// a queue
    typedef int Q1[$:99];		//最大长度为99的queue
    typedef int Q2[$];
    Q1 q1;
    Q2 q2;
    
  • 初始化

    int Q1[$] = {};				// 空队列
    int Q2[$] = {1, 2, 3};		// 含元素队列
    
  • 存储关系

    SV会先分配小空间,待queue不够后,再自动分配额外空间。

  • 内建函数/操作方式

    请添加图片描述

    // $在左边表示min index,在右边表示max index
    
    //初始化
    q1 = '{1,2,3};
    
    //选取
    q2 = q1[0:$];	//全选, $表示len-1
    q2 = q1[$:len]; //全选, $表示0
    
    //遍历
    foreach(q[i]) 
        $display("queue[%2d] = %d",i,q[i]);
    //清空
    q ={};
    
    //队首、队尾: e.g.
    q[0] = q[$];
    
    //拼接
    q1 = '{q1, 666};	// 入队头
    q1 = '{666, q1};	// 入队尾
    
    //入队头
    q.push_front(item);
    //入队尾
    q.push_back(item);
    
    //出队头,并返回此元素
    item = q.pop_front()
    //出队尾,并返回此元素
    item = q.pop_back();
    
    //在指定index上插入
    q.insert(index, item);
    //删除指定index元素
    q.delete(index);		// 不能一次性clear 
    					// 和find_index一起用时要注意
    
    //查看个数/长度
    q.size();
    
    //队内去重
    q2 = q.unique();		// return new queue
    
    //队内升序——直接在自己这 reorder
    q.sort();
    //队内降序——直接在自己这 reorder
    q.rsort();
    //队内乱序——直接在自己这 reorder
    q.shuffle();
    //队内逆序——直接在自己这 reorder
    q.reverse();
    
    //队内查找 		return index queue !
    //必须写条件,不能直接 find_index(x),不合法
    int tmp_q[$];
    tmp_q = q.find_index() with (item==5);
    tmp_q = q.find_first_index() with (item>10); // 没找到就是空queue
    tmp_q = q.find_last_index() with (item>"z");
    // 找到了也不能用 q.delete(tmp_q)哈,delete不能这样用,一次只能删一个
    
    //求和 
    total = q.sum;
    //带条件求和
    total = q.sum with (item>5);
    
    //点积
    total = q.product;	//带条件的用法同上
    
    //求最大值最小值
    tmp = q.min();
    tmp = q.max();
    

2.6 任务task 和 函数function

区别

  • task

    1. 返回值;
    2. 可以执行时序等“消耗仿真时间”的相关逻辑:如@#5ns 等;
    3. 可以调用function;
  • function

    1. 可以(不必须)返回值;
    2. 不可以执行时序等“消耗仿真时间”的相关逻辑:如@#5ns 等;
    3. 不可以调用task;
    function 返回类型 函数名(形参表);		// 注意分号
    	//...
        return ...;
    endfunction
    

形参表

形参定义

同Verilog,有两种形参定义方式:括号内定义、任务/函数内定义;

  • 括号内定义

    【详见Verilog】

  • 任务/函数内定义

    【详见Verilog】

形参的端口类型
  • input (默认类型)

    输入类型,值传递.

  • output

    输出类型,值传递.

  • inout

    输入输出时,各进行值传递.

  • ref

    引用(指针/句柄)传递.

  • 默认类型

    1. function/task的默认变量类型logic

    2. [经验]function/task传递对象时,除非加了 input/output/inout关键字,对象形参都默认是引用ref类型.

    3. 定义类型后,后方形参的端口/变量类型将都将是此类型,例如:

      // a,b 将是 input logic
      // c,d 将都是 output logic [15:0]
      function void (a, b, output [15:0] c, d);
      endfunction
      

返回值

  • 忽略返回值

    Verilog内,有返回值的函数,若在使用时不接受返回值,会报Warning.

    消除Warning的办法:强转为void返回——value = void'(fun());

automatic关键字(动态分配存储)

  • Verilog与SV的变量特点默认“静态分配”(非堆栈分配)

    与C/C++/Python等语言不同,Verilog核心是用于描述硬件电路,故所有函数局部变量/变量/对象,都默认是静态分配——默认存放在内存而不是堆栈(尤其是同function/task的形参与局部变量)

  • 坑点

    C中函数形参是动态变量,故每次调用函数形参都默认是传值;SV中task/function的形参是静态变量,多次调用task/function时的形参是会对同变量进行修改

  • 解决方案——automatic关键字

    • 作用将本地变量/局部变量定义为“堆栈变量

    • 使用方式

      1. 加在变量上——堆栈的局部变量;

      2. 加在function/task上——全function/task的变量都是堆栈类型;

        可以叠加用ref.

      // 定义变量
      automatic reg [0:7] tmp;
      // 定义automatic的function/task
      function automatic [0:7] fun;
      endfunction
      
      • 反之,automatic 的function/task内,可单独声明static的变量.

2.8 块语句

都是用begin-end进行包裹.

过程快

initial、always(分为always、always_comb、always_ff、always_latch)、final(仅SV支持).

  • initial 是仿真程序开始时,运行;

  • final 块是程序结束前,才执行;

    程序是否正常结束都会执行$stop$fatal都会执行.

    可以作为仿真结束的辅助判断信息的打印block~

语句块

在if/for/fork-join等语句内,加begin-end.

程序块 program

  • 大概作用

    1. 是整个dv的入口,分隔design与dv平台;
    2. 可封装整个dv的程序、函数、任务;
    3. 提供了语法,以调度Reactive区域内的执行逻辑,避免竞争;
  • 语法

    program 名称(信号, interface xx_if);
        //...
    endprogram
    
  • program语法特点

    1. 不能使用always、UDP,只能用initialfinal
    2. program内的initial全部结束时,会自动调用$finish结束仿真;
    3. 不能例化moduleinterface、嵌套program
    4. 必须用非阻塞赋值<=clockingblock进行驱动;
    5. 因为代码全运行在Reactive region set中,故可看见当拍中所有module的信号变化结果;
    6. 养成好习惯,program多用automatic修饰;
  • 细节作用

    见【3 SV的调度机制与program】

2.9 强制转换

是SV新增的功能。

SV中不像C,SV不支持隐性强转,而是需要写出来的,是为了让程序猿意识到自己在做什么。

SV内根据“检查强制转换成功与否”,分为静态强转和动态强转。

静态类型转换

  • 特点

    编译时直接进行,不论转换的范围是否合法,不会报错

    浮点类型转整型,会自动舍入;压缩类型之间的强制转换,按整型的强转要求来即可。

  • 语法——用单引号运算符'

    类型'(表达式)

    e.g. c=int'(a+b)

动态类型转换

  • 特点

    编译时不检测,仿真运行时进行,若失败了可以提供错误返回值、并不改变原式结果。

  • 语法——调用系统内建函数

    function int $cast(目标变量, 原表达式);
    task $cast(目标变量, 原表达式);

    将右边的表达式强制赋值给左边的目标变量。

    强转成功,返回1;强转失败,返回0,且原变量值不变。

  • 在OOP下的妙用——向下转换/检测继承关系 ⭐️

    OOP内,父指针指向儿子,是多态语法,是子类对象的向上类型转换

    OOP的儿子指针指向父亲,是父类对象的向下类型转换,默认是非法的(儿子ptr易越界),但SV内可用$cast(儿子ptr, 父亲对象);来实现。

    若传入实参不符合继承关系,将$cast(...)返回0。故可用于OOP的向下强转,或检测OOP的类继承关系。

3 SV的内生机制

3.1 生命周期与作用域

定义

  • 基本概念

    local(局部)和global(全局)指的是变量的作用域(作用范围),static(静态)和automatic(动态)指的是变量的生命周期(何时被回收)——是两个概念。

  • 变量的作用范围

    局部变量/全局变量:概念同C/C++;

    局部变量生命周期与作用域共存亡,全局变量生命贯穿始终。

  • 变量生命周期:静态/动态

    • 静态变量 (static):变量在整个仿真程序开始执行后被创建内存空间,保存至程序结束(只会被初始化1次)。
    • 动态变量(自动的,automatic:变量仅在代码的作用域范围内,动态创建内存空间、开辟在stack中;程序结束时,会自动、动态释放内存空间。
  • function/task追加static/automatic修饰

    • automatic方法: 其内部的所有变量默认也是automatic,即伴随automatic方法的生命周期建立和销毁。(但依然能以显式声明,改变内部单个变量的生命性质)

    • static方法: 其内部的所有变量默认是static;

    • 注意

      1. function/task是不是automatic、static,只影响其内部定义的变量,影响不到形参。
      2. function/task 默认到底是automatic还是static,见function所在的module,是什么类型(但module默认也是static).
  • Verilog不支持递归

    verilog是为硬件综合服务的,默认只有static变量,故其没有堆栈来进行变量存储;各task/funcstion访问的都是相同的变量,还递归个锤子。

    所以,SV有了automatic关键字。

    • 为什么SV又增加了static关键字?

      因为有时你就是需要在一个automatic的module中,进行一些“verilog的行为”…使得你需要“在automatic的module中定义static的变量”…( ̄▽ ̄)"…

SV的默认规则 ⭐️

请添加图片描述

  • SV的默认的作用范围/生命周期规则

    • program/module/interface内定义的变量/function/task,都默认是局部变量(且static);在其内部的块语句定义的处定义的、或外部定义的,默认为全局变量(且static);

    • 块语句内 定义的变量为局部变量,块语句本身与其内的变量皆默认static;

      可以给块语句加关键字automaticstatic来显式定义其生命周期,会同时改变其内部变量生命周期。

    • 不考虑OOP,所有变量/function/task,默认为static

      故一般function/task内的局部变量,都是被多个进程、方法的调用所共享的。

      Verilog是为硬件服务的,和C/C++不一样;SV首先得兼容Verilog,因此也是默认static,后来才推出了automatic关键字。

    • 考虑OOPclass内的function/task,默认为automatic

      我看网上说:“类的成员方法默认且必须是automatic的”,但我试了一下可以用static修饰,也有static的实际效果,也不会报错,不知道为啥。

    • for循环内定义的临时循环变量,默认为automatic

    • automatic变量不能用非阻塞赋值<=.

  • static/automatic对function使用的语法

    • static/automatic的变量

      加在 function/task 的==右边==.

      function static void print();

    • OOP内的静态成员方法

      加在 function/task 的==左边==.

      static function void print();

    class A;
        // illegal
        function void static fun1(int x);
        endfunction
        
        // static类型的方法
        // 据说class内function/task必须为automatic
        // 但我试了一下不会报错...也确实为staic类型...
        function static void fun2(int x);
        endfunction
        // 静态方法,类内共享
        static function void fun3(int x);
        endfunction
    endclass
    

个人牢骚

  • Ref:

    SV中的automatic与static ——CSDN 相对清晰,罗列了的大部分情况。

    what is the exact difference between static tasks/functions and automatic tasks/functions ? ——verification academy 没有很详细的罗列各个情景,但讲明白了automatic的历史意义。

    SYSTEM VERILOG STATIC AND AUTOMATIC LIFETIME OF VARIABLE AND METHODS 做了简要的情景分类,除了method variables说的没看懂,其他的和我总结的一样;同时,他提到了我找了半天的,for循环的临时循环变量的特点。

    【IEEE 1800-2017 SV 6.21 Scope and lifetime】 内有详细说明,可惜我整理完才看到… 后面关于SV的问题可以去查一下。

  • 和C/C++其他软件语言的区别

    • 为什么分不清: C/C++里,“静态”的概念只存在在OOP中,为“静态类方法”与“静态类”。而SV在OOP外居然也有static的概念,把我整蒙了。这是Verilog/SV特有的特点

    • 核心区别:

      1. 大多数编程语言内(C/C++),局部变量就已经是动态创建与回收(对应SV里的automatic类型)了,局部变量与动态回收是绑定的;
      2. 只有Verilog,出发点是描述电路,因此独立于OOP的概念外引入了static概念,且将局部变量默认定义为static类型。
      3. 由于SV添加了OOP编程的特点,因此SV只有OOP部分(class内的成员函数)向C/C++看齐,默认是automatic类型。

      不知道就容易翻车。

  • fork-join中使用automatic定义变量的意义

    见【4.1 fork-join】

bug疑问解答

  • Q1:为什么for循环内的定义的初始化过程只会执行1遍?如下述代码。

    initial begin
        int i;
        for(i=0; i<5;i=i+1) begin
    		int j = i;
            ...call fun(j)...		//实际上永远是传入 fun(0)
        end
    end
    

    解答: 因为这些代码都是verilog的功能,故initial块以及内部变量,都默认是static的。而static的变量,有且只会初始化1次。

    因此,上述代码中的j其实是static的,只会初始化1次,即int j = 0,后面都不会再初始化了。导致后续循环中,传入结果都是 fun(0) 而不是各种fun(i).

  • Q2:为什么不同的写法,运行结果不同?

    // Case 1:
    initial begin
        for(int i=0;i<5;i=i+1)
            $display(i);
    end			// 结果:0 1 2 3 4
    
    // Case 2:
    initial begin
        for(int i=0;i<5;i=i+1) begin
    		int j=i;
            $display(j);
        end
    end			// 结果:fatal: Illegal reference to automatic variable
    
    // Case 3:
    initial begin
        int i;
        for(i=0;i<5;i=i+1) begin
    		int j=i;
            $display(j);
        end
    end			// 结果:0 0 0 0 0
    
    // Case 4:
    initial begin
        int i;
        for(i=0;i<5;i=i+1) begin
    		int j=i;
    		print(j);
        end
    end
    task print(int i);
        $display(i);
    endtask		// 结果:0 0 0 0 0		 	// Case 4与 Case 3同,后略。	
    
    // Case 5:
    initial begin
        int i;
        for(i=0;i<5;i=i+1) begin
    		automatic int j=i;
    	$display(j);
        end
    end			// 结果:0 1 2 3 4
    
    // Case 6:	可以和 Case 2 对比
    initial begin
    	A a = new;
        a.fun();
    end			// 结果:fatal: Illegal reference to automatic variable
    class A;
    	function fun();
    		for(int i=0;i<5;i=i+1) begin
    			int j=i;
    			$display(j);
    		end
        endfunction
    endclass	// 结果:0 1 2 3 4
    

    解答:

    1. Case 1是很正常是单进程串行执行的结果;
    2. Case 2Case 3理由差不多。默认的initial块,是static的,其内定义的变量int j自然也是static变量。但for循环内临时定义的循环变量int i是automatic的。导致类型匹配不上。编译器可能是出于保护的目的,进行了报错。
    3. Case 3理由同上,j是static变量,故只会初始化一次,即j=0。后续for循环,每次j都是0.
    4. Case 4 是本来是为了表明,task的默认类型也是automatic. 但这个代码不能实现这一点,因为task/function定义为static或automatic,是影响不到形参和实参的。
    5. Case 5把变量定义为automatic,就获得了预期的结果。
    6. Case 6Case 2的function几乎一样,为什么结果不一样?因为SV的class内的所有成员函数,默认也必须,都是automatic类。所以就不会有static的烦恼了。

3.2 调度机制与program

时间片与调度机制

  • time slot、Verilog/SV的宏观调度flow

    1. Verilog与SV仿真的底层实现中,将仿真时间以多个离散的“时间片(time slot)”单位进行具体的调度与执行。

      SV是离散事件执行模型,即不是每个time slot都执行,只会对基于事件、离散的、有效time slot进行执行

    2. 单个time slot内,将划分为多个Event Region(e.g. Active、Inactive、NBA…),它们分别对应不同代码行为的“具体落实”,自上向下进行。

    3. 不同的块语句alwaysinitialassertionprimitiveassign等)都将分别创建1个进程(不是基于语句创建进程,而是基于块),在各自对应的region上等待被执行。

      在语法上,Verilog/SV毕竟是不检测竞争冒险的。因此早期RTL编程不规范,1个块语句内混用阻塞赋值与非阻塞赋值也不会报错。

    4. 当time slot内所有Area被执行完毕,当前time slot的信号值就确定了;同时进入下一time slot的更新。

  • timescale、time step、time slot的区别

    时间片(time slot) 与 timescale 是不一样的。

    • timescale
      1. timescale规定了仿真局部“最小单位”与“仿真精度(time precision)”;
      2. 可以在不同文件、不同module中定义不同的timescale的“精度部分”,实现差异化精度;
    • time step
      1. time step是工程中,全局最小的timescale的“仿真精度”的值
      2. RTL中的#1,延后的是当前timescale的局部仿真精度的单位;RTL中的#1step,延后的是全局timescale的最小仿真精度的单位;
    • time slot
      1. time slot是Verilog/SV底层实现调度的最小单位,它的重点是内涵盖的多个region的概念,重点不在时间间隔本身;
      2. 两个time slot间隔1 time step.

Verilog的调度机制 (4 regions)

请添加图片描述

  • Active Region

    核心是负责有关组合逻辑的更新:(以下执行顺序随机)

    1. 进行阻塞赋值、连续赋值、primitive UDP的赋值
    2. 计算非阻塞赋值的右侧表达式(RHS, right-hand-side),进程顺滑入NBA区;
    3. 调用$display
  • inactive Region

    执行#0延迟下的阻塞赋值。

    • 理论意义:因为多个进程的阻塞赋值间顺序随机,于是Verilog提供了#0,可以至少实现“这条阻塞语句是最后执行”的效果。
    • 现实意义没人用。因为,规范的RTL综合规范中,本就不应该 在多个块中对同个变量进行阻塞赋值
  • NBA Region

    1. 赋值非阻塞赋值的左侧表达式(LHS, left-hand-side);

      若此赋值更改了其他组合逻辑进程的监听信号,则其他进程需要重新被迁入Active Region.

  • Postponed Region

    1. 所有信号确定后的值,准备进入下个time slot.
    2. 调用$monitor$strobe

SV的调度机制

不考虑PLI (9 regions)

请添加图片描述

  • Preponed Region

    是上个time slot内Postponed Region的值,为Observed服务.

  • Observed Region

    用于断言检查,计算 (evaluate) 断言内property的事件是否发生、表达式等.

  • Pre-Active Region

    1. 执行断言,确定断言的pass/fail结果
    2. 执行program包裹的验证程序中,Active的行为的赋值;
  • Pre-Inactive

    1. 执行program包裹的验证程序中,Inactive的行为的赋值;
      • 意义:这里Pre-Inactive中的#0还是有点用的——TB的fork-join中,在父进程中加入#0,可以实现“让子进程先于父进程执行”。
  • Pre-NBA

    1. 执行program包裹的验证程序中,NBA的行为的赋值;
  • #1step for input

    SV中,新引入了#1step的概念:

    即有关的输入信号的初值采样,提前1个time step,在上个time slot的postponed region进行采样,等价于在当前time slot的prephoned region进行采样。

    :这个概念,在clockingblock的input、output信号中也有专门的设置,但那是和clock skew有关,侧重点不太一样。

考虑PLI (9+8 regions)
  • PLI

    Programming Language Interface

  • 懒得画了。

  • 新增region

    分别在:Preponed后、Postponed前;NBA前后、Observed前后、Re-NBA前后,新增了PLI专用的Regions.

program的作用

  • 竞争冒险(race hazard)

    同一个变量的同一个上升沿的不同阻塞赋值的“写”、“写读”操作,其值到底如何是随机、难以确定的。具体会产生不同竞争冒险的情景即为竞争条件(race condition);信号的同一时刻的赋值在微观上、事实上的传输过程中存在先后关系,叫“竞争”;导致客观结果上,可能会产生预期外的尖峰脉冲的现象,为“冒险”;若真有尖峰信号的波形,波形为“毛刺”。

    • 竞争条件 race condition

      有很多,可以见上面的Reference.

      但究其原因,是因为在边沿触发下,没采用“非阻塞赋值”而用“阻塞赋值”对同一个变量,进行多地同时的写、同时的一读一写。因此,规范的RTL用法能减少此类问题。

  • 意义

    综上所述,在早前Verilog的编程尚未规范时,为了避免设计代码与验证代码的“竞争冒险(race)”,IEEE在SV的语法中引入了program以及新增的Reactive Region Set,来对设计代码/验证代码的赋值进行分割,以降低“DUT与DV之间的竞争冒险”

    program字段内包裹的initial块代码,都将被识别为验证代码,统统运行在Reactive Region Set区域.

    • program内只能写initial块. ( ̄▽ ̄)"…
  • 现实意义——萝卜青菜各有所爱

    随着Verilog的编程/综合规范的发展,以及验证多采用Interface、UVM等框架进行规范话,大家都很少在代码中使用program;有人觉得代码风格好,program就没什么软用,看个人喜欢。

    因此,基于SV的dv,用programmodule两种来进行包裹,都行。

    其中:

    1. UVM是基于uvm_pkg::run_test()来启动的,其运行region可自定义。

      此方法的调用在programinitial块中,还是在moduleinitial中,将直接决定UVM的各phase的代码,是运行在Active区域还是Re-active区域.

      Cadence推荐在module中调用run_test();Synopsys中推荐在program中调用,从而实现design、dv的区分。

    2. “clocking块” 之前没提

      不论它定义在program还是module中,它的input都是在preponed region进行sample的、其ouput都是在reactive region进行driven的

编程规范

经过上述思考,为避免竞争冒险,也是为了更好的顺利综合,RTL的编程规范是需要记住的。

  • Guideline

    1. 时序逻辑(sequential logic) ,要用非阻塞赋值(nonblocking assignment);

    2. Latches,要用非阻塞赋值;

    3. always块实现的组合逻辑,要用阻塞赋值;

    4. 不要在同个always块中,混合写时序逻辑与组合逻辑;

    5. 不要再多个always块中,对同个变量进行赋值(仅用1个always完成对1个变量的写操作);

      :在always_combalways_latchalways_ff中,这已经是强制性要求了.

    6. 不要使用#0的任何过程性赋值(<==都是过程性赋值);

      Design中不允许,TB中自己根据需要。

    7. 需要打印非阻塞赋值的变量时,用$strobe 而不是$display

      • $display是在此time slot的active region打印,对应在代码中就是“写在哪,就在哪打印”;

      • $write$display一样,但末尾不自动加换行符'\n'

      • ==$strobe是在此time slot的prephoned region打印,==不管调用在何时,结果都是一样的;

      • $monitor,只要加一句,它会在要打印的变量一发生变化,就打印一次。

        但只能存在1条,否则打印1次后只有最后一条monitor才会驻留发挥效果。

  • 其他收集的编程好习惯

    1. 组合逻辑多用assign,少用always(*)

      SV LRM中写着,assign赋值将在0时刻生效,但always(*)没说。

4 并发编程与进程同步

fork-join

语法与功效

  • 作用

    SV可以用fork-join系列语句,启动多个进程进行并发执行;begin-end语句块内是单个线程、顺序执行

    • :并非并行,故fork-join的并发和多个initialalways块并发、time slot的调度执行思想都是一样的——SV的本质——宏观上是并发,微观上都是串行的
  • 语法

    三种fork语句:

    • fork / join:父线程被阻塞,等待所有子线程完成,之后父进程再继续执行fork外的语句。

    • fork / join_any:父线程被阻塞,任何一个子线程完成,则父线程开始执行fork外语句。

    • fork / join_none:父线程不阻塞,直接继续与所有子线程同时执行后续语句。

    可手动调用wait fork;让父进程等待所有进程执行结束.

    task case1();
    	for(int i=0;i<4;i++)
    		fork
                call_thread(i);
    		join_none	//父进程不等子进程
    endtask
    task case2();
    	for(int i=0;i<4;i++)
    		fork
                call_thread(i);
    		join  		//父进程等所有子进程
    endtask
    
    task case3();
    	for(int i=0;i<4;i++)
    		fork
                call_thread(i);
            join_none
        wait fork;		//父进程手动等子进程
    endtask
    
  • 运行flow图:SV在for循环中使用fork_join和fork_join_none的区别 —— CSDN

细节探究

  • 父子进程-变量共享

    子进程可以直接修改/访问fork结构外、父进程的变量

    但不推荐,多个子进程对父进程变量的修改/访问,会导致竞争。

  • 坑点:fork外的静态变量共享导致的异常

    SV会先展开外部的for循环,将多个子进程展开后,再统一启动执行。

    即,fork会将子进程添加入“进程管理器”中,当父进程结束/挂起时,再执行子进程的内容。

    见下方code——所有子进程id都将为N ❌:

    // fail case
    int id;
    for(id=0; id<N; id++)
        fork
            my_process(id);		//所有子进程id都将为N
    		int k=id;			// k是static变量,只会被实例化1次,故为0???10???
        join
    

    这就是为什么 要在fork-join内使用automatic来声明中间变量

    // pass case
    automatic int id;
    for(id=0; id<N; id++)
        fork
            my_process(id);		//所有子进程id都独立、不同
        join
    
  • fork-join与for的内外嵌套的区别

    之前我的理解是对的——开的进程个数不一样。

    // Case1:
    initial begin
    	for(int i=0;i<5;i++)begin
    		fork
    			// 开了5个进程
    			// 代码
    		join
    	end
    end
    // Case2:
    initial begin
       fork
    		for(int i=0;i<5;i++)
    			// 开了1个进程,默认算1个语句
    			// 代码
       join
    end
    

进程控制

  • 进程提前终结disable

    • 作用:终止当前进程的所有子进程,递归所有子孙后代进程

    • 语法

      1. disable fork终结当前父进程以及其子孙进程;

        fork-join any等父进程非阻塞的fork联用。

      2. disable fork_name终结某个子进程(得先定义fork_name);

      3. disbale task_name:相当于task的return函数,甚至能用于always块.

        • task的output/input实参、assign、force以及task内未执行到的非阻塞赋值都会是未知态
        • 不可用于funtion.
  • 父进程等待所有子进程运行结束:wait fork

    fork-join any等父进程非阻塞的fork联用。

mailbox 信箱

  • 是什么?

    请添加图片描述

    一种SV内提供的进程同步通信的方式。本质是一个封装好的模板FIFO类,相当于fifo+semaphore。

    一边由一批生产者(producer)进程负责存data、另一边由一批消费者(consumer)进程负责读data。当FIFO空或满时,读/写操作会自动阻塞

    mailbox可以自定义fifo是有界的(bounded)还是无界的(unbounded queue).

  • 声明语法

    由于mailbox的内建方法支持对存取fifo的msg类型进行check,故多采用参数化的mailbox,“1个mailbox传递1个类型的data”,便于编译检查。

    mailbox 变量名;

    mailbox #(任意类型) 变量名;

    // 实例化法1:
    mailbox #(string) mbx = new(100);
    // 实例化法2:
    typedef mailbox #(string) mbx_t;
    mbx_t mbx = new;
    
  • 内建方法

    • 实例化:.new()

      function new(int bound=0);

      实例化成功,返回指针;失败,返回null。

      形参bound值是mailbox的len,为0表示是无界queue;负数则报错。

    • 当前个数:.num()

      function int num();

    • 压入数据:put系列

      • task put(val);

        压入数据,满则阻塞.

      • function int try_put(val);

        尝试压入数据——未满则压入,返回1;满了则不压,返回0.

    • 读出数据并pop:get系列 (形参是ref)

      • 阻塞型

        task get(ref val);

        读出数据,空则阻塞;类型不匹配则报错.

      • 非阻塞型

        function int try_get(reg val);

        尝试读出数据——未空则读出,返回1;空则放弃,返回0;有数据可读出但类型不匹配,返回负数.

    • 读出数据但不pop:peek系列(用于复制) (形参是ref)

      • 阻塞型

        task peek(ref val);

        读出数据,空则阻塞;类型不匹配则报错.

      • 非阻塞型

        function int try_peek(reg val);

        尝试读出数据——未空则读出,返回1;空则放弃,返回0;有数据可读出但类型不匹配,返回负数.

semaphore 信号量

  • 作用

    用于用户自定义实现同步互斥的信号量。

  • 声明语法

    semaphore 变量名;

    semaphore s1 = new;
    semaphore s2 = new(10);
    
  • 内建方法

    • 实例化:.new()

      function new(int count=0);

      实例化成功,返回指针;失败,返回null。

      形参count值是semaphore的初值;可以手动put超过count值

    • 释放:.put()

      • task put(int count=0);

        释放count个数.

    • 抢占:get系列

      • 阻塞型

        task get(int count=1);

        一次性抢占数量为count的信号量,失败则阻塞.

      • 非阻塞型

        function int try_get(int count=1);

        尝试一次性抢占——成功,返回1;失败,返回0.

    • 读出数据但不pop:peek系列(用于复制) (形参是ref)

      • 阻塞型

        task peek(ref val); 读出数据,空则阻塞;类型不匹配则报错.

      • 非阻塞型

        function int try_peek(reg val); 尝试读出数据——未空则读出,返回1;空则放弃,返回0;有数据可读出但类型不匹配,返回负数.

event 事件

  • 作用

    可以通过定义event来以类似中断形式,阻塞进程,从而进行多任务同步控制。

    其功能有:定义事件、等待事件被触发、触发事件等。

    Verilog内的event,只有立即的“边沿”性;SV给event追加了time slot内的持续可检测属性,避免事件的竞争的问题

  • 语法

    • 定义event

      event 变量名;

      不用 new.

    • 触发事件

      1. 立即(阻塞赋值形式)触发此事件->事件名

        此触发行为,类似边沿,是不可用表达式去观测的,即,无法用if(posedge clk)的形式去检测.

      2. 无阻塞赋值形式触发此事件:->>事件名

        我很怀疑是否有这个语法,网上都搜不到…

        试了下真的有…确实是无阻塞赋值的形式…虽然感觉有点鸡肋,不如wait(event.trigger)

    • 等待事件被触发

      1. 检测“当下被立即触发”@ 事件

        缺陷:事件被触发是边沿性质的,一旦“事件等待”行为,没精确在“触发行为”之前,就会永远阻塞。

      2. 检测“此time slot内被触发”wait(事件.triggered)

        优点:避免了因为“event触发与检测的竞争”导致事件没检测到;只要是在同time slot内被触发的event,都能检测到。

5 OOP

  • 与C++/Java区别

    语法基本和Java一样,和C++语法不同。思想一样。

  • SystemVerilog的指针/句柄/引用

    SystemVerilog内,“对象名”的机制和Java有点类似,像C++的引用,故可叫做指针、句柄、引用——默认是浅拷贝。

    例子见下:

    // C++
    A a;		// 变量的定义
    a = new();	// 变量的实例化 create an instance
    A b = a;	// 默认:深拷贝
    
    // Java & SystemVerilog
    A a;		// 变量的定义
    a = new();	// 变量的实例化 create an instance
    A b = a;	// 默认:浅拷贝
    

    上述区别是:

    1. C++里,对象的等于号= 的赋值默认是 “深拷贝”,对象a和b是分别两个内存空间,但值相等;

    2. SystemVerilog 和 java 中,对象的等于号= 的赋值默认是“浅拷贝”变量 a 和 b 是指向同一个内存空间,除非用 copy() 函数。

      故一些SV书,把OOP变量的赋值称做“指针变量的指向”,SV没有专门的“指针”概念. ( ̄▽ ̄)"

封装

  • 基本语法

    class 类名;
        // 成员变量
    	int id;				// 默认:public型
    	local string name;	// local表:private型 
    	protected ...		// protected表:protected型
        // 构造函数
        function new(形参);
            super.new(形参);	//调用爹
        endfunction
    endclass
    
    类名 对象名 = new();		// 实例化
    
  • 类内引用自身(对象

    this.变量

    类似java,C++里this是指针.

  • 对象调用成员函数与成员方法

    .运算符

构造/析构函数

  • 构造函数

    • 特点

      1. 用于分配空间;
      2. 同C++/Java,无返回类型与返回值;
      3. 对象变量未调用new()时,默认为null.
      4. 构造函数内未初始化的变量,用系统默认值(不是随机数).
    • 语法

      见上面demo.

  • 析构函数

    无析构函数。

    同Java一样,对应内存无指针指向时,自动回收空间。

浅拷贝与深拷贝

SV的=号,仅仅对“基本数据类型”是默认深拷贝,对OOP数据默认浅拷贝.

  • 浅拷贝(shallow copy)——只复制了指针.

    Node 对象2= 对象1;

  • 深拷贝(deep copy)——真正地复制了内容.

    Node 对象2= new 对象1;

    :SV没有实现.copy(..)深拷贝函数,要用户自己实现。

    • 嵌套的指针,深拷贝的话,内部的指针是浅拷贝还是深拷贝?

      a=new b;只能实现对基本数据类型的深拷贝;类内若有嵌套的指针变量,则new对它们的拷贝依然是以浅拷贝的形式进行的。

      这就是为什么,自定义的class最好还是自己写copy()函数.

    • struct 封装的数据,用=后,默认是浅拷贝还是深拷贝​?

      struct默认是深拷贝!

      哇,struct和class不一样,struct像是基本数据类型.

    • 复杂的OOP对象,一般要用户自己实现copy()函数实现深拷贝。

静态对象/方法

  • 作用:类内共享。

  • 用法:同C++/Java.

    1. 静态方法不能是virtual 方法、不可访问非静态变量、不可用this,否则编译报错.

    2. 静态方法 ≠ \neq = 静态生命周期方法.

      这个之前章节论述过。主要是static位置不同.

    class Node;
    	static int num=0;
    	static task fun1();	//静态方法,默认是动态生命周期
    	endtask
    	task static fun2(); //静态生命周期
    	endtask
    endclass
    

类外定义方法

  • 类外定义function/task

    1. 类内声明function/task,extern 关键字以及其他需要的关键字;
    2. 类外定义function/task,不加任何extern关键字、但追加类作用域::
    3. 类内类外的声明定义,仅需要且必须形参表一致,virtual等关键字可略.
    class A;
        extern protected virtual function void fun();
    endclass
    function void A::fun();
    endfunction
    
  • 与C++的异同

    1. C++不用extern关键字,直接类名::函数名()即可;
    2. C++内extern不是“类外定义”意思,而是“文件外定义”的意思;
    3. C++内extern "C"更是另一个意思:此段代码按C的语法编译。

随机化

SV支持对“对象”调用.randomize()函数,实现自定义的随机化过程;但得对待随机的成员变量进行一些预定义。

  • 随机初始化成员变量 以及 约束

    • 类内定义

      rand 类型 成员变量名;
      
    • 随机初始化

      // 对象 new() 完以后,加上assert只是好习惯罢了.
      assert( 对象名.randomize() );
      
    • 定义约束(即 随机初始化的区间)

      • 方法①:类内定义约束体

        constraint 约束名{
        	data>10;	// example
        	data<30;
        }
        //然后类外正常调用 assert( 对象名.randomize() );
        
      • 方法②:直接在随机初始化时,进行动态定义约束

        assert( 对象名.randomize() with {data>10;data<30;} );
        

继承

子类

  • 普通语法

    class 儿子 extends 父亲;	
    	...
    endclass
    
  • 参数化类的继承

    class A #(type T, int len) extends B #(T);
    endclass
    
  • 儿子访问父类方法

    super.父类方法;

    不允许嵌套supersuper.super.成员变量/方法;

  • 注意事点

    1. 父指针指向儿子,只能访问到父亲内的成员变量与成员函数。

      重点case儿子的同名变量,父亲访问不到.

      下方demo是否如此?

      class A;
          int value;
      endclass
      class B extends A;
          int value;
      endclass
      initial begin
      	A a;
          B b = new;
          b.value = 10;
          a = b;
          $display("%0d", a.value);
          // 结果:
          // 为0而不是10,父亲访问不到.
      end
      
  • 父类的无参数构造函数,是否会被子类默认调用、只有有参数父类构造函数才需要手动调用?还是必须手动调用?

覆写overwrite

  • 覆写意义与访问特性

    1. 子类可以覆写父类的同名成员变量、成员方法、constraint;

    2. 父类的成员变量/成员方法依然存在,只是访问不到;

    3. constraint享受多态的效果;

      故父类指针指向子类,调用.randomize(),生效的子类的constraint.

多态

overload/override/overwrite

  • SV这仨支持性上与C++/Java区别
    • SV不支持子类函数的重载(overload);
    • SV支持子类函数的覆写(overwrite/override);
    • SV支持动态多态的virtual function.

虚函数

  • 作用:略

  • 虚函数

    作用与语法,同C++/Java.

    1. 父类定义时,function/task上 virtual关键字;

    2. 子类实现时,可省略 virtual,但加上方便识别,是好习惯.

      子类即使不加,孙子继承过来的也是虚函数!

  • 虚函数与普通函数的嵌套/混合调用,触发的核心要义

    1. 爹class 间接调用的 virtual 函数,也会触发多态!

      只要是按多态的形式调用,几经辗转调用了虚函数,那么触发结果即为多态的运行结果。

    2. C++也是如此。

      我之前估计记混了,多次实验后——证实C++亦是如此,依然遵循上面的“多态触发要义”。

    #include<stdio.h>
    class A{
    public:
        void print(){
            printf("print A\n");
            v_print();
        }
        virtual void v_print(){
            printf("v_print A\n");
        }
    };
    class B :public A{
    public:
        void print(){
            printf("print B\n");
            v_print();
        }
        virtual void v_print(){
            printf("v_print B\n");
        }
    };
    int main(){
        B b;
        A *a=&b;
        a->print();		// 间接调用v_out(),也会触发多态!
        return 0;
    
        // the answer is:
        // print A
        // v_print B
    }
    

虚类(抽象类)与纯虚函数

概念同Java里的abstract类。

  • 声明语法

    pure必须在virtual类里使用

    virtual class base;
        function new();
        endfunction
        
        //纯虚函数
        pure virtual funciton test();
    endclass
    

父类中转之$cast

  • SV的OOP中引入的$cast玩法

    • 爹指向儿子,很正常,本就是多态的用法,直接用=号即可;

    • 儿子指向爹,语法上直接=是不行的,但用cast函数在满足条件就可行:

      1. 爹=son1;
      2. $cast( son2,爹);

      其中,son1与son2是同class,结果上实现了son2=son1的指向.

      即cast的应用本质是 “爹handle作为桥梁,帮助两个儿子handle间实现互指”。

参数化类(模板类)

  • 声明语法

    class Node #(type T, int len = 10) extend;
        local T items[];
        typedef int queue[[$:len];		//变量定义可以用传入参数
    endclass
    initial begin
        Node#(bit [0:7], 10) node;
    end
    
  • 特例、模板类内的静态变量

    • 通用类:模板类确定了具体的传入参数后,得到1个通用类。

    • 特例:1个通用类?

      没看懂…

6 接口与虚接口

各关系介绍

  • 为什么要引入interface?

    引入interface的目的:封装整个端口信号、便于连接与修改.

  • 为什么要引入虚接口 virtual interface?

    interface仅能服务于Verilog硬件,不支持在OOP内实例化。SV为兼容verilog的前提下,支持OOP功能,则需要在OOP内引入virtual interface,使OOP内可访问interface内的端口。

  • interface内引入modport的意义

    1. interface可用于将所有可能要用到的端口信号进行封装;

    2. 但interface内部可通过modport,进一步划分成可重叠的端口子集,按不同module类别进行划分。

      e.g. 一个interface类,用modport分为master信号集合、slave信号集合。(AXI协议的端口)

    3. 不同modport,其内的端口信号可以重复出现,但direction注意别矛盾、冲突.

  • interface内引入clocking块的意义

接口 interface

  • 什么是interface?

    Verilog/SV内允许将端口信号作为“通道”进行专门、标准化的“封装”,一方面,能类似class或module一样,便于例化与复用;另一方面,可用于module与SV的OOP进行通信

    • 注意
      1. interface不是“类”,作用类似,和module、class平级;但毕竟是电路逻辑.
      2. 能定义module的代码位置,就能定义interface;
      3. 能定义class的代码位置,却不一定能定义interface和module(如SV的program-endprogram).
  • 定义语法

    同样,形参表可以采用多种方式。

    // 端口direction可以后续定义,也可以定义内部变量
    interface 接口类名(input 变量名, input ... );
        // 定义端口信号变量:
    	reg ...
        logic ...
        // 写assert/function/task内容:
        task xxx();
        endtask
        // 暂时不知道是啥
        modport master(...);
        modport slave(...);
    endinterface
    
    • 形参的端口方向(direction)如何设置

      形参的input/output类型可以直接定义、可以不定义(相当于内部变量)、也可以留置于在后续的modport中具体定义。

  • 使用语法

    interface实例化后,直接作为实参给module传入即可.

    // 假设定义了 interface IF类...
    module TB;
    	IF top_if;			// 实例化interface
    endmodule
    
  • 内部兼容功能

    内部可用assert、function/task、其他端口类型的嵌套;

  • 关系介绍

    1. SV引入了interface,用于封装端口;

    2. 引入了virtual interface,是为了SV的OOP;

    3. 同时SV在interafce中,新增了modpot分组、clocking块的功能,用以支持interface内更细分的功能。

虚接口 virtual interface

  • virtual interface的本质

    virtual interface是指向interface的指针

  • 定义与使用的语法

    成指针类型的成员变量,在类内进行定义与赋值(指向)即可

    // pkg内
    interface IF;
    endinterface
    class Node;
    	virtual IF vif;
        function new(virtual IF vif);
            this.vid = vif;		// 和成员变量初始化没区别.
        endfunction
    endclass
    // TB内
    IF top_if;	// 普通的IF
    initial begin
        Node node = new(top_if); //构造函数时进行传入即可.
    end
    

    使用flow

    1. 类外定义interface
    2. top_tb内定义interface对象,传入DUT;
    3. OOP类内定义virtual interface的成员变量,指向top_tb的interface对象;
    4. 按指针进行赋值即可——秉持着“virtual interface是interface的指针”理念.
  • 备注

    virtual interface是SV内多出来的语法,是SV为了兼容verilog的interface、又兼容OOP的操作。

端口模式

  • 意义

    见【6.1 各关系介绍】;一句话总结:信号分组,并规定方向。

  • 定义语法

    1. 端口的方向direction,可挪动到modport内进行规定;
    2. 不同modport的端口可以有交集;
    3. 不同modport的端口若有交集,注意direction别矛盾.
    interface 接口类名(input clk);
        // 形参表:
        logic clk, a, b, c, d, tmp1, tmp2;
        modport master(clk, input a, b, tmp1, output c, d);
        modport slave(clk, input c, d, tmp2, output a, b);
    endinterface
    
  • 实例化的使用语法

    定义与实例化时,若基于端口模式采用名称关联,直接传入interface实例名,会自动查找对应的端口模式.(modport名别写错了)

    // 假设定义了 interface IF类...
    module TB;
    	IF top_if;		// 实例化interface
        father father(.master(top_if));		// 使用modport
        son son(.slave(top_if));			// 使用modport
    endmodule
    module father(IF.master);	//module定义时就可用modport
    endmodule
    module son(IF.slave);
    endmodule
    

clocking块

意义与语法

  • 意义

    • 引入了skew:SV在功能级仿真中,对时钟、各输入信号的“采样时机”、输出信号的“驱动(driven)时机”都引入“偏差skew”的概念,目的不仅是能少量模拟现实中的时钟偏差,更是为了通过限定,来避免信号边沿触发(采样or驱动)导致的竞争冒险
    • 提供新操作符##封装指定clocking block后,SV支持使用## N代替# Nns,具有“延后N个时钟”的效果。(仅能用于dv,不可综合
    • 置于interface内的意义:将各信号、尤其是clock的“skew配置控制”剥离到clocking block内,使其他SV代码能专心将重点放在dv的逻辑flow上。

    请添加图片描述

    • 输入偏差:SV使输入信号在时钟事件(上升沿or下降沿)提前多少时间进行采样.
    • 输出偏差:SV使输出信号在时钟事件(上升沿or下降沿)延后多少时间进行采样.
  • 纯语法

    // 加了default后,##1、##N的时钟就默认是此clocking block.
    [可选default] clocking 新时钟名 (原时钟event);
        default input 延迟 output 延迟;
    	// 单独设置信号的延迟
    	// 也可在此对interface的信号direction进行声明,从而生效skew.
        input 信号列表 延迟;
        output 信号列表 延迟;
    endclocking
    
    1. input/output skew的可以是ps、ns等单位的仿真时间,也可以是1step全局最小time precision),其意义在**【3.2 SV的调度机制】**中有说明.
    2. default input skew1step:表示输入信号,将在event发生之前采样,微观上是在上一个time slot的postphone region进行采样.
    3. default output skew0:表示输出信号,将在event发生时进行采样,微观上是在当前time slot的re-NBA region进行driven.
    4. 可以设置 input skew #0:表示输入信号,将在event发生时进行采样,微观上是在当前time slot的observed region进行采样.
  • 特点

    1. interface内可以有多个clocking block,且不同clocking block内的信号可以重复出现。但一次只能指定运行1个clocking block.

      • 可以在interface的模块中,指定默认的clocking block.

        加了default关键字后,##1、##N的时钟就默认是此clocking block了.

    2. module若想使用某个clocking block,module内就得使用interface内的“新时钟名称”了;module内若想驱动clocking block内的output信号,就得使用 “接口.新时钟名.信号+非阻塞赋值”进行赋值.

      嗯,很麻烦,可以用 define 辅助…减少这么多级的层次引用…

  • 总结

    我还是不明白,这么麻烦的clocking block,使用的必要性是啥…看的我血压上去了…

Demo

  • Demo

    // 定义interface
    interface IF (input clk)
    // 定义变量... 略
        default clocking tb_clk @(posedge clk);
            // Case1:系统默认(可以不写)
            default input #1step output #0;
            // Case2:输出在下降沿进行driven
            default input #1step output negedge;
            // Case3:
            default input #2 output #3;
            // 单独设置信号的延迟
            input #1step addr;
            output negedge ack;
        endclocking
        clocking clk2 @(posedge clk);
    		// ...
        endclocking
    
        // 使用clocking block的地方——让TB的clk提前sample
        modport master(tb_clk, input ..., output ...);
        modport slave(clk, input ..., output ...);
    endinterface
    
    module top
    	bit clk;
        IF top_if(clk);
    	//TB是用pragram写的
        TB tb(top_if.master);
        DUT dut(clk, top_if.slave);
    endmodule
    

未整理

7 随机测试与随机约束

先略。

8 功能覆盖率

9 断言(SVA)

10 OVM

11 PLI/DPI

99 系统内建函数

  • $typename(变量)——获取变量类型
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值