Dskpimc? · 2020年08月11日

SystemVerilog与功能验证

目录

一、功能验证流程

二、验证手段、验证技术、验证方法学

三、数据类型与编程结构

四、并发进程与进程同步

五、面向对象编程

六、虚接口

七、随机测试

八、继承与多态

九、功能覆盖率

十、断言

一、功能验证流程

功能验证流程主要分为三部分:1、制定验证策略和验证计划;2、创建验证平台,运行和调试;3、回归测试和覆盖率分析。

1.1 制定验证策略和验证计划

制定验证策略和验证技术主要处理以下三个问题:

(1) 主要测试点和测试用例

首先需要从设计中提取出有实际意义的、可管理的测试空间集合,并且没有损害其期望的功能;然后根据这些测试点,拟定验证策略和验证用例;最后具体化到一个详细的、可执行的验证计划中,作为整个验证功能的指导。

(2) 验证平台的抽象层次

验证平台的抽象层次将决定它主要的处理对象:比特、包或者更高层次的数据类型。高层次的抽象建模需要让验证平台中低层次的功能自动化。
(3) 激励生成和结果检查原则

这些原则定义了输入到验证平台的激励是如何提供的,结果是如何检查的,并判断测试是否通过。

1.2 验证平台的搭建和调试

验证平台的搭建要以可重用为基本原则,而且方便设计工程师和验证工程师添加测试用例。在该阶段,需要搭建验证平台,书写测试用例并调试。

1.3 回归测试和覆盖率分析

回归测试要求能够周期的批处理运行,激励必须能够容易得到重现,成功或失败能够自动检查。覆盖率显示出该设计被测试的程度,是验证收敛的重要标准。

二、验证手段、验证技术、验证方法学

三种常用验证手段:白盒、黑盒和灰盒验证;三种主要验证技术:形式验证、仿真验证和硬件加速验证;三种重要的验证方法学:随机激励生成、断言验证和覆盖率驱动验证。

2.1 验证手段

功能验证的最终目标是验证一个设计是否能够像预期的那样工作。然而,并非在设计中的任何一个设计缺陷都能通过激励在其输出边界上被检测到,为此可能会被遗漏。这样的错误可能包括下列几种情况。

  • 从来没有被激发过的错误;
  • 被激发的错误没有被传递出来;
  • 多个潜在的错误掩盖了其他错误;

       黑盒验证(black-box verification)指的是只通过其边界信号来验证一个模块或者设计的功能。黑盒验证需要参考模型,激励可以被应用到设计和参考模型中,在某个抽象层次(例如事务级、指令集、时钟周期级等),通过被测设计和参考模型的输出被校对。由于做到足够精细的参考模型来检测设计的所有内部错误难度较大,为此不可能完全依靠这种验证手段;
    

    白盒验证(white-box verification)和灰盒验证(grey-box verification)提供了另外一种途径来解决黑盒验证的局限性。在白盒验证中,可以通过在设计内部放置监控器和断言来保证设计操作的正确性,可以不需要参考模型。

    灰盒验证综合了白盒和灰盒,在设计内部添加监控器和断言来减少对参考模型的精确要求,在错误发现的时候也减少调试的难度,即可以采用较简单的参考模型。这种方法在实际验证中最常使用。

2.2 验证技术

可以用来作为功能验证的技术主要可以分成以下三类:

1) 形式验证(formal verification);

2) 仿真验证(simulation based verification);

3) 硬件辅助加速验证(hardware assisted verification/acceleration and emulation);

    形式验证采用逻辑和数学公式的方法来证明或者否定硬件的特性。其中要重点强调的是,形式验证通过等式和分析系统而不是测试向量,形式验证技术不需要测试向量作为输入。形式验证主要有等价性检查(equivalence checking)和属性检查(property checking)两个分类。

1) 等价性检查:等价性检查要证明在所有可能的输入组合和序列下,两个硬件实现的功能是等价的。例如有限状态机,在所有可能性的输入序列和同样的初始条件下,会产生完全一样的输出序列。等价性检查工具的输入一般是设计的两种表达形式——在给定转换前后设计实现形式(例如,综合前的RTL代码和综合后的网表);

2) 属性检查:属性检查(断言验证)是对设计的特性形式实现,例如RTL描述,属性检查验证给定属性描述的时序逻辑(temporal logic:时序逻辑表示一个系统的规律和符号,来代表和辨析两个逻辑变量在时间上的关系)。

  仿真验证的基础步骤是在给定当前状态和输入的情况下,通过计算下一个状态设计信号的值,同时考虑信号的延迟,把未来值赋给设计中的信号。

    硬件辅助加速验证主要应用于加速仿真速度,硬件辅助加速最基本的思想就是把设计映射到可配置平台,例如FPGA或者通用运算单元,以便数字设计部分可以在接近最终产品的时钟速度下运行。


2.3 验证方法学

所谓方法学就是:在某种科学、艺术或学科采用的方法、流程、运作概念规则和基本原理;功能验证的方法学就是验证电子系统的技术和科学。

验证一个设计需要回答两个问题:“Does it work?” (DUT能够正常工作么?)(在验证计划中可以分解成:测试什么功能点?怎么测?最后功能对不对?)和"Are we done?"(我们做完了么?)。第一个问题属于最基本的验证概念,也就是说设计能否符合设计者的意图;第二个问题就是问我们的验证是否充分和完备,验证是否达到收敛。

基于断言验证和覆盖率驱动验证就是解决这两个问题主要的方法学。基于断言的验证关注断言如何被一致的使用在整个设计流程和不同的工具上,以协助工程师更快的定位错误。覆盖率驱动主要关注的是如何制定一个可衡量的标准和验证计划,以此作为指导更快地达到验证收敛。这些方法学存在交叉的地方,例如断言可以作为覆盖点成为覆盖分析的一部分。

1) 断言验证(Assertion Based Verification,ABV)主要的目的是保证设计和设计期望之间是一致的。断言是对设计期望行为(也就是属性: property)的声明或者描述。属性可以从设计的设计规范和功能描述中推知,并转化为断言(属性描述)。

2) 覆盖率驱动验证(Coverage Driven Verification,CDV)是一个基于仿真的验证方法,专门被开发用来解决当前功能验证项目面临的效率和完备性挑战。覆盖率驱动验证的方法最重要的特点是基于随机激励产生。随机激励生成是提高效率的最主要的原动力。覆盖率驱动验证方法学把下面几个概念和技术融合起来:事务级验证、约束随机激励产生、自动化结果比较、覆盖率统计分析和直接测试。

a. 事务级验证(Transaction Based Verification,TBV)

在仿真中,设计的所有数据流以低层次的信号比特或者比特向量的形式存在,在这个层次创建验证场景,通常效率很低。在事务级验证中,低层次信号活动被抽象成一个事务操作,以致可以通过这些高层次事务操作来描述各种验证场景。事务处理器就是把验证环境中事务级抽象层次的活动转换成低层次的信号活动,以致可以被DUT接受。基于事务级的验证遵循下面一些原则:

I、数据和数据流在较高的抽象层次定义(例如:帧、包);

II、验证场景在较高的抽象层次描述(例如:写存储器、执行指令);

III、事务处理器吧这些抽象层次的数据和活动转换成低层次的操作和信号,以便应用到被测设计中。事务处理器可以是总线驱动器。接口驱动器或者就是一个层次到另一个层次的数据协议转换器。

b. 约束随机激励生成(Constrained Random Generation):

随机激励生成指的是利用随机生成技术来产生一个事务交易中所有的数据内容,同样产生一系列事务交易来形成一个特定的验证场景。随机场景的生成要求验证计划需要的场景能够被随机生成,包括自动检查和覆盖率统计。随机生成提高了验证效率,因为一次随机仿真可以验证更多的场景和数据值的组合,因此减少了验证工程师搭建、执行和验证每个场景的时间。另外随机验证是在验证环境中的各种随机组合交叉,使得在验证计划中没有指定的场景可能也会被生成,因此随机激励产生也有助于验证的完备性。

随机生成验证场景,要求每个随机生成的场景能够自动地被验证;为此,在任何随机生成环境中自动化检查是必须的。这也就意味着这一个随机激励生成的环境需要一个参考模型(RM),这样各种验证场景的仿真结果能够被预测和自动检查。另外,一个随机的验证环境应该可以能够识别通过随机场景产生的所有类型的行为,这些行为包括各种操作场景;也就是通过覆盖率统计分析机制。总而言之,一个包含了随机激励生成的验证环境,必须也有自动比较和覆盖率统计分析机制。例子如下:

本例子是验证一个有限状态机:随机激励生成、自动比较、覆盖率统计和分析。整个过程分析如下:

(1) 随机激励产生

I、初始化,把有限状态机初始化到一个随机合法的状态;

II、生成随机输入;

(2) 自动化比较

I、检查下一个仿真状态是否和预期的一致;

II、检查仿真输出是否和预期的一致;

(3) 覆盖率统计

I、统计所有覆盖的状态;

II、统计状态的变迁;

III、统计每个状态上的输入;

(4) 分析

I、检查所有有限的状态是否被覆盖;

II、检查所有的跳转是否被覆盖。

c. 自动化结果比较

就如前面提及的,对于随机化的验证环境,自动化结果比较是一个必备条件。自动化结果比较可以采用监视器(monitor)和积分板(socreboard)等技术,监控器常用做协议检查和搜集设计数据流。积分板用来做端到端的行为和数据比较。

d. 覆盖率统计和分

    随机激励生成潜在提高了验证的效率,然而若没有一个清晰的验证策略来衡量验证过程,这些行为将很难被控制和实现。准确的衡量验证过程要求对于每个随机仿真的运行做下列分析:
  • 哪些场景在验证计划列出的,在一个仿真生成运行;
  • 哪些没有在验证计划中列出的特定场景被生成了;
  • 每次仿真运行对整个验证过程的贡献;
  • 为了生成遗漏的场景,约束修改是必须的。

       在每个随机仿真的过程中,覆盖率信息统计是用来决定这次仿真对于整个验证过程的贡献;另外,若现存的测试用例运行更长的周期而没有任何更多的贡献时,对仿真环境进行修改或者添加新的测试用例是必要的。
    

    e. 直接测试

    一个单一的随机环境无法创建所有的验证场景,为此通常存在多个环境或者测试用例,每个新的变化都关注在创建前面没有覆盖过的新场景。最后留下来的场景必须通过直接测试用例,这需要创建一个特殊的验证环境或者一个高度约束的测试用例。

三、数据类型与编程结构

3.1 数据类型
1、基本数据类型(bit、wire、reg、logic、byte、int...)

Verilog 1995有reg(变量)和wire(网线)两种基本类型,都是四态数据类型(0、1、Z、X),默认值是x态。RTL代码使用reg来存储组合逻辑和时序逻辑的数值,可以是标量或者向量(reg [7:0] bus_addr)、有符号数32位变量(integer)、无符号数64位的变量(time)和浮点数(real)。变量也可以用来定义一个固定大小的数据。网线wire用来连接两个设计模块,如门级元件或者模块例化。

SystemVerilog引入了两态的数据类型来减少仿真器对内存的使用和提高仿真的运行效率,有shortint、int、longint、byte和bit,它们只能取值0或1,默认值是0。其中bit是无符号数,其它四个是有符号数。对于有符号和无符号类型都可以使用unsigned和signed来进行转换。

SystemVerilog还对reg的数据类型进行改进,可以被连续赋值语句、门逻辑和模块直接驱动。另外,SystemVerilog引入了一个新的四态数据类型logic,可以代替reg,但它不能用在双向总线和多驱动的情况下,此时只能使用网线类型,例如wire。

2、枚举类型和用户自定义类型

a. 枚举类型

枚举类型可以用来声明一组整型的命令常量,定义具有强类型的枚举变量。枚举类型还可以使用枚举名字而不是枚举值来方便地引用或显示。

一个枚举类型可以定义如下:

enum [data_type] {name1=value1, name2=value2..., nameN=valueN} var_name;

当没有指定数据类型的时候,缺省的数据类型是int。在枚举类型中使用的任何其他数据类型都要求显示地声明。枚举类型定义了一组具有名字的值。SystemVerilog枚举类型是一种强类型,枚举变量在赋值、传递和关系操作符中进行类型检查,因此枚举变量不能被在枚举集合范围以外的数值直接赋值,除非使用墙纸类型转换或者该枚举变量是一个联合体的成员。枚举变量可以自动转换成整型值,在表达式中作为常量使用,并且运算结果也可以赋值到任何一个兼容的整型变量。

SystemVerilog为枚举类型提供了如下内置方法(method)来方便操作:m.first()返回第一个枚举常量;last返回最后一个枚举常量;next返回下一个枚举常量;next(N)以当前成员为起点返回以后第N个枚举常量;prev()返回前一个枚举常量;prev(N)以当前成员为起点返回前第N个枚举变量;当到达枚举常量列表的头或尾时,函数next和prev会自动以环形方式绕回;m.num()返回该枚举类型的成员数目;m.name()以字符串的形式返回该成员名字。m为枚举变量。

b. 用户自定义类型

在SystemVerilog中,可以通过typedef关键字进行用户自定义类型的扩展。定义新的数据类型可以提高代码的可读性,复杂的数据类型(结构体、联合体等)和特定的数组可以通过使用一个简单易懂的名字(别名)被定义为一个新的数据类型,例如:

typedef int my_favorite_type;

这个新的类型就可以定义对应的变量:

my_favorite_type a, b;

其实你并未创建一个新的数据类型,而只是在做文本替换;将一个特定的数组定义为新的书类型,例如:
parameter OPSIZE=8

typedef reg [OPSIZE-1 : 0] opreg_t;

opreg_t op_a, op_b;

如果事先使用空的typedef事先对一个数据类型作出标识,那么它就可以在其定义之前使用。

typedef foo;

foo f=1;

typedef int foo;

有时,一个用户自定义类型需要在类型的内容被定义之前声明。这对于由enum、struct、union和class派生出的用户定义类型很有用处,如下:

typedef enum type_declaration_identifier;
typedef struct type_declaration_identifier;
typedef union type_declaration_identifier;
typedef class type_declaration_identifier;
typedef type_declaration_identifier;

3. 数组与队列

SystemVerilog提供了下面三种数组类型与队列:

  • 静态数组(static array/fixed - size arrary)
  • 动态数组(dynamic array)
  • 关联数组(associative array)
  • 队列(queue)
    静态数组是指其数组的大小在定义时被显性地指定。动态数组有一维或多维,其中非压缩部分的一维的大小在定义的时候未被指定,在使用前才被分配。关联数组允许通过任何类型的索引来访问数组的成员。队列被用于定义一个顺序的数据集合。
    a. 静态数组与压缩数组
    SystemVerilog有两种数组类型:压缩数组(packed array)和非压缩数组(unpack array)。压缩数组指的是维数的定义在变量标识符之前,非压缩数组指的是维数的定义在变量标识符之后。如下所示:
    bit [7:0] c1; //压缩数组(bit类型)
    real u [7:0]; //非压缩数组(实型)
    静态数组的通用定义方法是:
    element_data_type [PRange1]...[PRangeN] arrary_name [Urange1]...[UrangeM];
    在array_name前面指定的维数是压缩部分,在array_name后面指定的维数是非压缩部分,下面是静态数组的使用方法。
    1) 对单个压缩数组的引用可以用来访问一个整体的数组;
    2) 数组成员访问的方式如下:
    Array_name[Urange1]]...UrangeM...[PrangeN]
    3)静态数组的非压缩维数可以通过[Number-1:0]或者通过[Number]的方式来定义:
    int_array0:7; //通过范围来定义数组
    int array[8]][32]; //通过指定上限定义数组
    4) 如果一个数组被定义为有符号数,那么其存储的所有压缩数组都被默认为有符号数,而每个压缩数组的成员是无符号数;
    5) 压缩数组被指定为粉盒网线类型或者标量变量类型(如:reg、logic和bit)。具有预定义宽度的整数类型不能声明成压缩数组,这些类型包括:byte、shortint、int、longint以及integer。一个具有预定义宽度的整数类型可以被看作一个一维的压缩数组
    integer i; //如同:logic signed [31:0] i
    byte c2[4:0] //如同:bit signed [7:0] c2 [4:0]
    数组可以通过下列方式进行访问:
    对数组的整体读写,例如:A=B;
    对数组做部分读写,例如:A[i:j]=B[i:j];
    对数组的可变片段读写,例如:A[x+:c]=B[y+:c];
    对数组的一个成员读写,例如:A[i]=B[i];
    对数组部分或整体做比较操作,例如:A==B,A[i:j]!=B[i:j]。
    压缩数组的维数只能通过[hi:lo]方式来定义。个人理解压缩数组是连续存储的,非压缩数组可以分开存储的。
    SystemVerilog还提供了系统内置方法来访问数组成员:$left、$right、$low、$high、$increment、$size、$dimensions和$unpacked_dimensions。
    b. 动态数组
    若在编译的时候并不知道数组大小,只有在运行过程才能确定,可以采用动态数组,在仿真过程中动态分配大小。动态数组可以是一维或多维的,其中非压缩部分的一维的大小在定义的时候未被指定,其存储空间只有当数组在运行时被显示分配之后才会存在。最简单的动态数组的声明语法如下:
    data_type array_name[];
    其中,data_type是数组成员的数据类型,动态数组与静态数组支持相同的数据类型。空的"[]"意味着我们不需要在编译的时候指定数据的大小,在运行的过程中可以动态分配,在使用前必须通过调用new[],并在"[]"中输入期望的长度数值来分配空间。

bit [3:0] nibble[]; //4比特向量的动态数组;

    integer mem[2][];  //固定大小的非压缩数组中带2个动态的整型子数组;
    系统函数$size()可以返回一个静态数组或动态数据的大小。另外,动态数组还有几个特殊的内置函数,例如delete()和size();这两个函数是不适用静态数组的。如果一个动态数组与一个静态数组的维度及深度相同,那么这个动态数组可以被赋值为一个具有兼容类型的静态数组。在这种情况下,赋值会自动分配一个新的动态数组,这个新的动态数组的长度等于赋值数组的长度。
 **   c. 关联数组**
    当集合的大小是未知或者数据空间紧缺的时候,使用关联数组是很好的选择。关联数组在使用之前不会分配任何存储空间,并且索引表达式不仅仅是整型表达式,而且可以是任何数据类型。关联数组是一种通过标号来分配空间和访问的数组,其好处就是只分配使用到的特定地址空间,也就是当你访问某一个较大地址的数组时,SystemVerilog只针对该地址分配空间。

关联数组实现了一个所声明类型成员的查找表,用索引的数据类型作为查找表的查找键值,并强制了其排列顺序。关联数组的声明语法如下:

    data_type array_id [index_type];
    其中:data_type是数组成员的数据类型,静态数组可以使用的任何类型都可以作为关联数组的数据类型;array_id是关联数组的变量名;index_type是用做索引的数据类型或者是*。如果指定了*,那么数组可以使用任意大小的整型表达式来索引。采用数据类型作为索引的关联数组将索引表达式限制为一个特定的数据类型。下面是关联数组的例子:

integer i_aaray[*]; //整型关联数组(未指定索引类型)
bit [20:0] array_b[string]; //21比特向量的关联数组,索引为字符串

    除了索引操作符外,SystemVerilog提供了几个内建方法让用户分析和处理关联数组,同时提供了对关联数组的索引或键值的迭代处理。
    function int num() /function int size():返回关联数组中成员的数目。如果数组是空数组,那么它返回0;
    function void delete([input index]):其中index是一个可选的适当类型的索引,删除特定索引的成员,如果没有指定索引,则删除数组的所有成员;
    function int exists(input index):其中index是一个适当类型的索引,如果成员存在返回1,否则返回0;
    function int first(ref index):其中index是一个适当类型的索引,将指定的索引变量赋值为关联数组中第一个(最小的)索引的值。如果数组是空的,则返回0,否则返回1;
    function int last(ref index):其中index是一个适当类型的索引,将指定的索引变量赋值为关联数组中最后一个(最大的)索引的值。如果数组是空的,则返回0,否则返回1;
    function int next(ref index):其中index是一个适当类型的索引,查找索引值大于指定索引的条目。如果存在下一个成员,索引变量被赋值为下一个成员的索引,并且函数返回1,否则,索引值不会发生变化,函数返回0;
    function int prev(ref index):其中index是一个适当类型的索引,查找索引值小于指定索引的条目。如果存在前一个成员,索引变量被赋值为前一个成员的索引,并且函数返回1,否则,索引值不会发生变化,函数返回0;
   ** d. 队列**
    队列是一个大小可变,具有相同数据类型成员的有序集合。队列能够在固定时间内访问它的所有元素,也能够在固定时间内对队列的尾部和头部插入和删除成员。队列中的每一个成员都通过一个序号来标识,这个序号代表了成员在队列内的位置,0代表第一个成员,$代表最后一个成员。队列类似于动态数组,是一个一维的非压缩数组,它可以自动地增长和缩减。因此,与数组一样,队列可以使用索引、串联、分片、比较操作符进行处理。队列适用于实现FIFO和堆栈之类的数据结构。队列通过以下方法进行定义:
    data_type queue_name[$];
    data_type queue_name[$:maxzise];
    其中,data_type是数据成员的数据类型,与静态数组和动态数组支持的一致;queue_name是定义的队列变量,若给定max_size,那么队列的最大数将受到约束。例如:
    byte q1[$];  //字节队列
    string names[$] = {"Bob"};  //字符串队列
    integer Q[$] = {2,3,4};  //初始化整型队列
    bit q2[$:255];  //最大长度为256的比特队列
    空队列文本{}可用来指示一个空队列。如果声明时没有提供初始值,那么队列变量被初始化为一个空队列。
    SystemVerilog提供了下列预定义的方法来访问和操作队列:
  • function int size():返回队列中成员的数目。如果队列是空的,它返回0;
  • function void insert(int index, queue_type item):在指定的索引位置插入指定的成员;
    Q.insert(i,e)等效于Q={Q[0:i-1],e,Q[i,$]};
  • function void delete(int index):删除指定索引位置的成员;
    Q.delete(i)等效于Q={Q[0:i-1], Q[i+1,$]};
  • function queue_type pop_front():删除并返回队列的第一个成员;
    e=Q.pop_front()等效于e=Q[0]; Q=Q[1:$];
  • function queue_type pop_back():删除并返回队列的最后一个成员;
    e=Q.pop_back()等效于e=Q[$]; Q=Q[0,$-1];
  • function void push_front(queue_type item):在队列的前端插入指定的成员;
    Q.push_front(e)等效于Q={e,Q};
  • function void push_back(queue_type item):在队列尾部端插入指定的成员;
    Q.push_back(e)等效于Q={Q,e};
4. 字符串

SystemVerilog引入了一个字符串类型(string),它是一个大小可变、动态分配的字节数组。在verilog中,字符串文本为一个具有宽度为8的整数倍的压缩数组。当一个字符串文本被赋值到一个大小不同、整型压缩数组变量的时候,它或者被截短到变量的大小或者在左侧填补0。

在SystemVerilog中,字符串文本的表现行为与verilog相同。然而,SystemVerilog还支持字符串类型,我们可以将一个字符串文本赋值到这种数据类型。字符串类型变量的声明语法如下:

string variable_name [=initial_value];

其中,variable_name是一个有效的标识符,可选initial_value可以是一个字符串文本,也可以是一个空字符串(" ")。例如:

string myName = "John Smith";

如果在声明中没有指定初始值,变量会被初始化成空字符串(" ");SystemVerilog提供了一组操作符,这些操作符可以用来处理字符串变量和字符串文本。字符串类型的基本操作符如下:

str1 == str2:检查两个字符串是否相等;

str1 != str2:检查两个字符串是否不等;

str1 < str2, str1 <= str2, str1 > str2, str1 >= str2:比较、关系操作符。按字母表顺序对两个字符串做比较,如果对应的条件为真则返回1;

{str1,str2,...,strn}:将几个字符串串联,生成一个新的字符串;

{n{str}}:对字符串str赋值n次,生成一个新的字符串;

str{index}:索引,返回一个字节;

str.method(...)点(.)操作符用来调用字符串的内置方法。

字符串的内置方法如下:

function int len():str.len()返回字符串的长度,也就是字符串中字符的数据(不包括任何终结字符),如果str是" ",那么str.len()返回0;

task putc(int i, byte c):str.putc(i, c)将str中的第i个字符替换成指定的类型;putc不会改变str的尺寸,如果i<0或i>=str.len(),则str不会发生改变;

function int getc(int i):str.getc(i)返回str中的第i个字符的ASCII码:如果i<0或i>str.len(),则str.getc(i)返回0;

还有很多其他的内置方法:如touper、tolower、compare、icompare、substratoi、atohex等等,具体查看下书中P36页。

5. 结构体和联合体

类似c语言,SystemVerilog提供了结构体(struct)和联合体(union)两种结构。结构体的成员被连续地存储,而联合体的所有成员共享同一片存储空间,也就是联合体中最大成员的空间。

结构体和联合体的声明遵从c语言的语法,但在"{"之前没有可选的结构体标识符,如下所示:

struct {

bit[7:0] opcode;
bit[23:0]addr;

} IR //未命名结构体,定义结构体变量IR

IR.opcode=1; //对IR的成员opcode赋值

其他一些声明结构体和联合体的例子如下:

typedef struct{

bit [7:0] opcode;

bit [23:0] addr;

} instruction; //命名为instruction的结构体类型

instruction IR; //定义结构变量

typedef union{

int i;

shortreal f;

} num; //命名为num的联合体类型

num n;

n.f=0.0; //以浮点数格式设置n

一个结构体可以作为一个整体赋值,并且可以作为一个整体作为接口参数在一个函数或任务中传递。通过packed关键字,结构体也可以类似压缩数组那样呗定义为压缩结构体;压缩结构体的所有成员在存储器中被无缝地压缩在一起。联合体也可以被定义为压缩联合体,这时其内部所有成员的大小(位宽)必须一致。

6. 变量

常量是一个永远都不会改变的命令数据变量,SystemVerilog提供了三个elaboration-time常量(parameter、localparam、specparam)和一个run-time常量(const)。通常我们需要在程序中对同一个对象的不同的例化做定制,这些参数可以在SystemVerilog中的module、interface、program、class、package结构中定义使用。每个参数必须在定义时给定初值,参数在elaboration的过程中设置(创建模块例化层次)并且保持该值直至整个程序执行结束。参数可以定义成任何数据类型,例如:

parameter [data_type] param_name=default_value;

parameter string file_name="testcase_1";

数据类型也可以定义为参数,这样使得module、interface或者program等可以定义一些数据类型参数化的端口和数据对象,在具体例化的时候才指定具体的数据类型。(注意,该类参数不可以通过defparam来重写),如源代码如下:

module ma #(parameter p1=1, parameter type p2=shortint) (intput logic [p1:0] j, output logic [p1:0] o);

p2 j=0; //j的类型通过参数来设置(若没有重定义,默认为shortint)

...

endmodule

module mb;

logic [3:0] i, o;

ma #(.p1(3), .p2(int)) u1(i, o); //将p2重定义为int类型

endmodule
本地参数(localparam)和parameter类似,但是本地参数不能直接通过defparam和例化时修改。本地参数有其特定的作用,可以在generate模块、package和class内部定义使用。

const常量与本地参数类似,但const常量在elaboration后才能通过const关键字定义的静态常量可以通过文本表达式、参数、本地参数、genvar、枚举名等赋值。层次化引用也允许使用,因此常量在elaboration之后才被确定下来。

specparam延迟参数只能在延迟说明块(specify block)中出现,而且只能定义延时参数。

SystemVerilog提供了四种方法来设置参数的值,每一个参数在声明时必须指定一个缺省值。我们可以使用下列方法之一来改变每一个模块实例的缺省参数值:

  • 按顺序赋值(例如:foo # (value, value) u1(...); );
  • 显示引用名字赋值(例如:#(.name(value), .name(value)) u1(...); );
  • defparam语句,使用层次化引用来重新定义每一个参数。
7. 文本表示

a. 数值表示

数值文本和逻辑文本可以是带有位宽的,也可以是不带位宽的。对于值的符号、截短和向左扩展,SystemVerilog与verilog-2001遵守相同的规则SystemVerilog可以使用撇号( ' )作为前缀,来说明一个文本的所有位可使用相同的数值来填充,如'0、'1,此时撇号( ' )与值之间没有基数限定符。

b. 字符串表示

 字符串文本由引号( " " )包围并且拥有自己的数据类型。对于字符串的长度没有预定义的限制,一个字符串文本必须存在一个单行中,除非新的行紧跟着一个反斜杠( \ ),在这种情况下反斜杠和新行字符会被忽略。非打印字符或其他特殊字符都使用一个反斜杠,也就是转义符作为开始。一个字符串文本可以赋值给一个字符数组,就如c语言的用法一样,会在末尾插入null结束符,若字符串长度与数组的大小不一致,则向左调整。

c. 结构体表示
结构体文本是结构体通过常量成员表达式的赋值方式或表达式。

d. 数组表示
数组文本在语法上与C语言的初始化设置类似,但数组文本允许使用赋值操作符(n{ })。例如:
int n1:2 = '{'{0, 1, 2}. '{3{4}}};
与C语言不同的是,括号的嵌套必须符合数组的维数。我们也可以嵌套复制操作符,复制嵌套中的内部括号对会被移除,复制表达式仅仅在一维空间上使用。
e. 时间表示
时间文本使用整数或定点格式的数紧跟着一个时间单位来表示(fs、ps、ns、us、ms、s),在时间单位和数之间没有空格,例如:
2.1ns、40ns
时间文本被解释成realtime类型的值,按照当前的时间单位按比例缩放,并根据当前时间精度四舍五入。
f. 注释方式
SystemVerilog有两种方式的注释。单行注释以“//”开始并在新一行前结束。模块注释以“/”作为开始并以“/”作为结束。模块注释不可以嵌套,在模块注释中,单行注释符“//”不能有其他特殊含义。

8. 操作符和表达式

SystemVerilog支持以下三种操作符类型。

一元操作符:一个操作符在一个操作数前面

   a=~b; //~是一元操作符

二元操作符:一个操作符在两个操作数之间

   a=b||c; //"||"是二元操作符

三元操作符:一组操作符把三个操作数间隔开来

   a=b?c:d; //"? : "是三元操作符

SystemVerilog提供了来自Verilog和C的操作符,有很多 操作符可供使用:逻辑比较操作、条件比较操作、通配比较操作。具体见P44页。

3.2. 过程语句

一个过程模块有自己的执行进程,其内部的语句将顺序执行,就如C和C++程序一样。过程模块由过程语句组成。SystemVerilog提供了下列几种可以在过程模块中使用:

  • 赋值语句
  • 条件选择语句
  • 循环语句
  • 跳转语句
  • 子程序调用
  • 事件控制

     1、赋值语句

    赋值语句用来对程序中的变量进行赋值。SystemVerilog提供了下面几种赋值语句:

  • 阻塞赋值
  • 非阻塞赋值
  • 自加/自减赋值
  • 过程连接赋值语句

     2、控制结构

    SystemVerilog中支持的条件选择、循环、跳转等编程控制结构,其使用方法与Verilog和C类似。

a. 条件选择语句

  if...else语句:根据不同的条件执行不同的分支。

if(expression)
<begin> ... <end>
else
<begin> ... <end>
case语句:为程序提供了分支选择控制功能。case要求分支表达式和case条件表达式做全等比较(===)而不是逻辑比较(==)。一个分支只有在其表达式完全匹配case条件表达式时,才被选中执行。
case(expression)
constant_expression: <statement_block>;
constant_expression: <statement_block>;
......
default: <statement_block>;
endcase
SystemVerilog也提供了两个有更多变化的case结构:casex和casez。
casex:case条件表达式中所有的x值都不参与比较;
casez:case条件表达式中所有的x和z值都不参与比较

**b. 循环语句**

Verilog提供了for、while、repeat以及forever循环。SystemVerilog增强了Verilog的for循环,并加入了一个do...while循环和一个foreach循环。
for循环语句:
for循环语句可以实现条件循环,基本格式如下:
for(<initializing_expression>; <terminating_expression>; <loop_increment_expression>)
<begin>
//循环体
<end>
在Verilog中,用来控制for循环的变量必须在循环体之前声明。如果两个或多个并行程序中的循环使用相同的循环控制变量,那么有可能出现一个循环修改其他循环还在使用的循环控制变量情况。在for循环中,SystemVerilog添加了声明for循环控制变量的能力。这种方式会在循环内产生一个本地变量,其他并行循环不会偶然地影响整个循环控制变量。
Verilog仅允许单个初始化语句以及在for循环中的单个步值赋值语句。SystemVerilog允许初始声明或赋值语句可以是一个或多个用逗号隔开的语句。步值赋值也可以是一条或多条用逗号隔开的语句。

while循环语句:

   while循环语句执行循环体,直至条件表达式为假。如果一开始条件表达式就为假,那么循环语句永远都不会被执行。格式如下:

while(<expression>)
<begin>
//循环体
<end>
do...while循环语句:
do...while循环语句和while循环语句有一点区别,也就是do...while循环语句在循环体的结束处评估循环条件,也就是do...while循环至少执行一次循环体。
do
<begin>
//循环体
<end>
while(<expression>);
repeat循环语句
repeat循环对循环体执行固定的次数,如果表达式被评估为未知或者高阻,那么应该认为是零次,不应该执行循环体。
repeat(<expression>)
<begin>
//循环体
<end>
forever循环语句
forever循环语句持续执行一个循环体。
forever
<begin>
//循环体
<end>
为了防止循环体在delta时间内产生无限循环,导致仿真挂起,forever循环体内部必须带有时序控制或者disable语句。
foreach循环语句
foreach循环语句中指定数组后,程序会逐个遍历数据成员。
foreach(<array_name>[<loop varibles>]) <statement>
它的自变量可以是一个指定的任意类型数组(固定尺寸的、动态的及联合数组),然后紧跟着一个包围在方括号内的循环变量的列表。每一个循环变量对应于数组的某一维。foreach结构类似于一个repeat循环,它使用数组范围替代一个表达式来指定重复次数。循环变量的数目必须匹配数组变量的维数。空循环变量可以用来表示在对应的数组维数上没有迭代,并且处于尾部的连续空循环变量可以被忽略。循环变量是自动的、只读的、并且它们的作用范围对于循环来讲是本地的,每一个循环变量的类型被隐含地声明成与数组索引的类型一致。

3、跳转语句

SystemVerilog增加了C语言中的跳转语句:break、continue和return。

  • break:像C语言一样跳出本层循环体;
  • continue:像C语言一样跳转到本次循环的尾部;
  • return(expression):退出一个函数并返回函数值;
  • return:退出一个任务或void函数。
    continue和break只能使用在循环体中。break语句会中断最近一层循环并跳出此层循环;continue语句结束本次循环,跳转到循环体的尾部,如果尾部存在循环控制语句,就执行这个循环控制语句。continue和break语句不能在fork...join语句中用来控制位于fork...join块之外的循环;return语句只能使用一个在一个任务或函数当中。在有返回值的函数中,return必须具有一个正确类型的表达式。
3.3 函数和任务

SystemVerilog提供了两种子程序调用(通称方法):函数和任务。

1、函数和任务区别

它们拥有各自的命名空间;任务没有返回值,而函数可以有 返回值;任务可以带时序语句,而函数不可以,也就是任务的执行可以消耗仿真时间。

2、子程序参数

在SystemVerilog中,任务/函数的形式参数可以选择下列方向属性之一:

  • input:在开始的时候复制值;
  • output:在结束的时候复制值;
  • inout:在开始的时候复制,在结束的时候输出;
  • ref:传递引用(句柄或者指针)。
    在SystemVerilog中,如果没有指定参数的方向,那么它的缺省方向是输入。一旦指定了一个方向,那么它就成为后续参数的缺省方向。每一个形式参数都有其自身的数据类型,可以是显示声明或者是缺省类型,任务/函数的参数缺省类型为logic。

SystemVerilog提供了两种方式来为函数和任务传递参数:值传递和引用传递。值传递是在子程序调用的时候,类型为input或者inout的实际参数的数值被复制到本地变量,在子程序退出的时候,类型为output或者inout的实际参数的数值被更新。引用传递(reference)就如C++的句柄或者指针,也就是变量的入口地址。此时参数的属性应定义为ref。ref传递通常用于数组,ref只是获取该数组的入口地址(句柄/指针),操作速度快,减少内存使用。如果在ref前面加上const关键字,则可以防止一个函数或者任务改变一个通过ref类型传递的变量。使用ref的另一个好处是,在子程序修改ref参数变量的时候,其变化对于外部是立即可见的。这是很有用的特别在几个进程并行地执行而想通过一个简单的方式来传递信息。
参数可以通过名字或者位置来传递。任何和函数还可以指定缺省值,这就使得调用任务或函数的时候可以不用传递参数。

3、子程序返回

在Verilog中,当任务/函数运行到endtask/endfunction的时候退出,而在SystemVerilog中,可以使用retrun语句在任务体或函数体的任意一点退出子程序。SystemVerilog允许将函数声明为void类型,它没有返回值。此外其他函数想Verilog一样,可以通过为函数名字赋值来返回一个值,或者使用return语句实现。
在Verilog-2001中,函数返回的值必须使用在赋值语句或使用在一个表达式中。在单个语句中调用非void类型的函数会导致一个警告信息,仿真器会将函数返回值强制转换成void类型,SystemVerilog允许使用void数据类型来忽略一个函数的返回值,如下所示:
void'(some_function());

4、自动存储

在SystemVerilog中,子程序仍然默认使用静态存储方式,对于所有的模块(module block)和程序块(program block)也一样:对于声明在class中的子程序和变量默认是动态存储的。另外,SystemVerilog还允许在一个静态任务中将特定的形式参数和本地变量声明成自动(automatic)的,也允许在一个自动任务内将特定的形式参数和本地变量声明成静态(static)的。

3.4 编程结构

SystemVerilog程序层次包括模块(module block)、程序块(program block)、过程块和数据对象4个部分。
模块是用来为层次结构的设计建模的。在基于模块的验证环境中,模块也可以用来搭建层次化的验证平台。模块可以包含其他模块的例化、过程化语句、数据对象。程序块也用来定义一个验证平台和被测设计的边界。程序块可以有类型和变量定义或者一个或者多个initial模块。过程化块可以包含其他过程化或者数据对象。数据对象,指的不仅仅是前面提到的数据类型,还包括类(class)、接口(interface)、mailbox等。

1、模块(module)

一个模块的简单定义格式如下:
module module_name<(module_port list)>;
<delclaration_block;> //<include:port_list, parameter, data object>
<function_task_delcaration>
<module_instance;>
<procedule_block> //include(continuous assign, procedural block)
...
endmodule
模块例化存在三种方式:采用*对同名的信号做默认连接;端口名称关联;端口位置关联。模块除了可以为硬件建模外,也可以用来封装验证平台,在模块内部,我们可以例化采用模块定义的DUT和采用program或者class封装的验证平台。

2、接口(interface)

在SystemVerilog中,引入了interface用于简化模块之间的连接,也实现类和模块之间的通信。interface的定义独立于模块的,通过关键字interface和endinterface包起来。此外,interface里面还可以带时钟、断言。方法等定义。

3、过程块和语句块

在模块中,除了连续赋值语句(assign)之外,其它主要由过程块组成。其中包括initial块、always块的变型(always_comb, always_ff, always_latch)。SystemVerilog还增加了finial块。initial块是在程序开始的时候运行,final块是在程序结束前,所有执行进程结束后才执行。always块和其变型在其敏感条件满足的时候开始运行,在敏感条件不满足的时候被挂起。过程块可以是串行执行(begin...end),也可以并行执行(fork...join、fork...join_any、fork...none)。

3.5 数据的生命周期和作用域

SystemVerilog中,数据有静态(static)和动态(automic)两种生命周期,作用域有全局和本地两种作用域。
一个动态变量在程序执行的时候在其定义作用范围创建生成。例如一个在函数内部定义的动态变量在程序执行进入该函数的时候被创建,在程序执行返回退出该函数的时候被释放删除。静态变量在程序开始执行的时候回被创建并保持它数值直到被重新赋值,在整个程序执行过程中都存在。
在模块、接口、任务或函数体外声明声明的任何数据都具有全局的作用范围(可以在其声明后的任何地方使用)。并且具有一个静态的声明周期(在整个确立和仿真时间内存在)。在模块或接口内,但在任务、进程或函数之外声明的变量具有本地的作用范围,并且具有静态的声明周期(在模块或接口的声明周期内存在)。在自动(automic)任务、函数或块内声明的变量具有调用期或激活期内的生命周期,并且具有本地的作用范围,在一个静态(static)任务、函数或块内声明的变量默认情况下具有静态的声明周期并且具有本地的作用范围。
SystemVerilog允许一个静态(static)任务或函数内的特定数据被显式地声明成自动的(automatic)。声明成自动的变量具有调用块内的声明周期,并且在每次进入调用或块内的时候进行初始化。
对于在模块、接口或程序中定义的任务、函数或块,SystemVerilog加入了一个可选的限定符来指定其中声明的所有变量的缺省生命周期。生命周期限定符可以是automatic或static,缺省的生命周期是static。
类的方法(task/function)以及声明在for循环内部的变量缺省是自动的,无论它们所在的范围的生命周期属性是什么。注意:自动或动态变量不能使用非阻塞赋值(<=)或连续赋值语句(assign)来赋值。自动变量以及动态结构(对象句柄。动态数组、关联数组、字符串以及事件变量)应被限制到过程关联文中。

3.7 数据类型转换

Verilog是一种弱类型语言,允许一个数据类型的值赋给另一个数据类型的变量或网线。赋值时,按照Verilog标准中定义的规则进行数据类型的变换。SystemVerilog增加了强制数据类型转换的能力。强制类型转换不同于在赋值时自动转变,使用强制类型转换,不用赋值,在一个表达式内,一个数值就可以转换成一个新的类型。

1、静态类型转换

SystemVerilog加入了一个强制类型转换操作符( ' )来改变一个表达式的数据类型:
<type> '( <expression> );
需要强制类型转换的表达式必须包含在圆括号内,或者必须包含在串联或复制花括号内。
int '( 2.0*3.0 )
shortint '{ 8'hFA, 8'hCE }
如果强制类型转换中的表达式需要改变位宽或符号,那么该表达式必须具有整型值。当改变位宽的时候,符号不会发生变化,当改变符号的时候,宽度不会发生变化。

2、动态类型转换

上面我们介绍的SystemVerilog静态类型转换是一种编译时的转换,转换操作总会运行,而不会检查结果的有效性。如果需要进一步的检验,SystemVerilog提供了一个新的系统函数$cast,这是动态的,在仿真运行时进行数据类型的检查,$cast可以作为任务或函数来调用。$cast的语法如下:
function int $cast(singular dest_var,singular source_exp);
或者:
task $cast(singular dest_var, singular source_exp);
dest_var是目标变量,source_exp是源变量的表达式。
因此函数可以有返回值,任务没有返回值,所以将$cast作为任务和函数调用不一样;当作为任务调用时,$cast试图将源表达式赋值给目的变量;如果赋值是无效的,会出现运行时错误并且目的变量保持不变。当作为函数调用时,$cast试图将源表达式赋给目的变量,如果赋值成功则返回1;如果赋值失败,$cast不会进行赋值操作而是返回0。当作为函数调用时,不会出现运行时错误,并且目标变量保持不变。
需要注意:$cast执行的是仿真运行时检查,除了检查目的变量和源表达式是否为单一类型外,编译器不会进行类型检查。

四、并发进程与进程同步

硬件设计可以看成是由很多并发或者并行的进程组成的。验证环境模拟硬件运行的能力越强,就越容易对硬件建模和验证。SystemVerilog提供了下列处理并发进程的能力。

  • fork...join并发结构
  • 通过mailbox实现进程间的通信
  • 通过semaphore实现进程互斥和仲裁
  • 通过event实现进程之间的同步
4.1 fork...join

fork...join能够启动产生多个并发进程,fork_join块可以指定一个或多个语句块,每一个语句块都应该作为并发进程执行。fork...join的语法结构如下:
fork
code_block_1;
code_block_2;
......
code_block_n;
join/join_any/join_none
fork...join内的每个语句块称为子进程,执行这段fork...join代码的称为父进程。

1、三种并发方式

verilog中的fork..join将执行fork语句的进程,并阻塞父进程的执行,直到fork...join中所有进程(子进程)终止。通过加入join...any和join...none关键字,SystemVerilog提供了三种选择来指定父进程何时恢复执行。
fork...join结构中,父进程会被阻塞直到所有的子进程结束;join...any结构中,父进程会被阻塞直到其中任意一个子进程结束;join...any结构中,父进程会被阻塞直到其中任意一个子进程结束;join...none父进程会立即与产生的所有子进程并发执行。

2、进程与变量

子进程可以在其内部定义本地变量,也可以访问在fork结构以外的变量。默认情况下,父进程和子进程都可以修改父进程定义的变量,而且都可以看到变量的变化。这类共享变量可以用来作为进程之间的通信。采用共享变量作为通信会导致进程之间的竞争,可以采用自动变量的方式来避免这种情况出现,保证每个独立的进程可以保存其独立的变量值。

3、进程控制

SystemVerilog中提供了对进程的控制语句,有disable、disable fork、wait fork。disable fork语句与disable的不同之处在于,disable fork会考虑动态的进程父子关系,而disable则使用静态的语法信息。因此,disable终止执行一个特定快的所有进程,可以是命名块,无论进程是否由fork调用产生分支;而disable fork则仅仅终止那些由fork调用产生的进程。wait fork语句被用来确保所有子进程(调用进程产生的进程)的执行都已经结束。

4.2 mailbox

变量不是进程之间唯一的通信方式。mailbox可以看成是一个先进先出(FIFO)的存储数组。通常有一个或者多个进程把数据送入一个mailbox,有一个或者多个进程从mailbox读出数据,送入数据的进程称为生产进程(Producer thread),读出数据的进程称为客户进程(Consumer thread)。客户进程可以被挂起,直到mailbox有可用的数据,也就是说,mailbox可以实现生产进程和客户进程的同步。
1、mailbox的基本操作
在创建mailbox的时候,它可以是有界的队列(bounded queue),也可以是无边界的队列(unbounded queue)。当一个有界mailbox存储的数据达到了其边界数目的时候,mailbox会变满,试图向已经满了的mailbox放置数据的进程会被阻塞,直到在mailbox队列有足够的空间,无边界mailbox永远也不会阻塞一个发送操作进程。定义一个mailbox的例子如下:
mailbox mbxRcv;
mailbox是一个内建的类,它提供了下列方法:

  • 创建一个mailbox:new();
  • 将一个数据放置到mailbox中:put();
  • 尝试将一个数据无阻塞地放置到mailbox中:try_put();
  • 从mailbox中获取一个数据:get()或者peek();
  • 尝试无阻塞地从mailbox中获取一个数据:try_get()或try_peek();
  • 查询mailbox中数据的个数:num()。
    mailbox的基本操作,分为写入(put/try_put)、取出(get/try_get)和复制(peek/try_peek)。这三种基本操作又分成两种不同的性质:阻塞和非阻塞,带try_*前缀的为非阻塞,表示在操作条件不满足的时候,放弃该操作而进入下一条执行语句。

2、参数化mailbox
缺省的mailbox是无类型的,也就是单个mailbox可以发送和接收任何类型的数据。然而这样会导致存储数据与用来获取数据变量间的类型不匹配而导致运行错误。一个mailbox经常被用来传输一个特定类型的数据,在这种情况下,如果能够在编译时发现类型不匹配,将十分有用。参数化mailbox与参数化类、模块和接口使用相同的机制,其定义如下:
mailbox #(type=dynamic_type)
其中dynamic_type代表一个特殊的类型,它能够执行运行时的类型检查(缺省情况)。
一个特定类型的参数化的mailbox通过指定类型来声明,如下所示:
typedef mailbox #(string) s_mbox;
s_mbox sm=new();
string s;
sm.put("hello");
...
sm.get(s); //s <- "hello"
参数化mailbox提供了所有与通用mailbox相同的方法,通用mailbox与参数化mailbox之间唯一的不同之处是:对于参数胡mailbox,编译器能够确保put()、get()、peek()等与mailbox指定的类型兼容,从而所有的类型不匹配都可以被编译器发现而不是在运行时发现。
3、旗语(semaphore)
多个进程可能需要使用一个稀缺的资源,当出现资源竞争的时候,获取一个共享资源就必须通过仲裁。SystemVerilog通过semaphore提供了仲裁的功能。从概念上讲,semaphore可以理解成一个存储桶。当为semaphore分配内存的时候,会创建一个带有一定数目key的存储桶。使用semaphore的进程在继续执行之前必须首先从存储桶中获得一个key。如果一个特定的进程需要一个key,那么同时可以执行的进程数目与key的数目一致。所有其他进程必须等待直到足够数量的key被放回到存储桶中。一般情况下,semaphore被用来控制对共享资源的互斥和实现基本的同步。创建一个semaphore的如下:
semaphore smTx;
semaphore是一个内建的类,它提供了下列方法:

创建一个具有指定数目key的semaphore:new();

  • 从存储桶中取出一个或多个key:get();
  • 向存储桶中放回一个或多个key:put();
  • 尝试无阻塞地获取一个或多个key:try_get();
    4、event(事件)
    当进程等待一个事件被触发的时候,进程被放入到一个在同步对象内部的维护队列当中。进程可以或者通过“@”操作符,或者通过wait()结构检查它们的触发状态来等待一个SystemVerilog事件被触发。事件通过使用“->”或“->>”操作符来触发。事件还可以作为参数传递给任务,而且可以相互赋值和比较;SystemVerilog的事件还可以作为同步队列的句柄。
    event_tigger :: = -> hierarchical_event_identifier; | ->> [delay_or_event_control] hierarchical_event_identifier;

1、事件触发
命名事件可以通过"->"操作符触发。触发一个事件可以解除当前所有等待这个事件被阻塞的进程。非阻塞事件使用“->>”操作符触发。"->>"操作符的效果是语句会无阻塞地执行,并且在延迟控制过期或事件控制发生的时候,会产生一个非阻塞的赋值更新事件。这个更新事件的效果应该是在仿真周期的非阻塞赋值区域触发被引用的事件。
2、等待事件
等待一个事件被触发的基本机制是通过事件控制操作符”@”:
@hierarchical_event_identifier;
"@"操作符阻塞调用进程直到指定的事件被触发。对于用来解除一个等待事件的进程的触发,等待进程在触发进程执行触发操作符“->”之前必须执行“@”语句。如果触发先执行,那么等待进程会永远处于阻塞状态。
3、事件的触发属性
verilog命令块事件的触发状态没有持续时间,而在SystemVerilog中,这个触发状态在时间被触发的整个时间片内持续。SystemVerilog能够区分事件触发(瞬时的)和事件触发状态——在整个时间片持续(也就是直到仿真时间继续)。事件的triggered属性允许用户检查这个状态。triggered属性使用一个类似内置方法的语法来调用:
hierarchical_event_identifier.triggered
如果指定的事件在当前的时间片已经被触发,那么事件的triggered属性为真,否则为假。如果事件标识符为null,那么事件的triggered属性为假。事件的triggered属性在一个wait结构中最为有用:
wait(hierarchical_event_identifier.triggered)
使用这种机制,一个事件的触发可以解除所有被阻塞的等待进程,无论它在触发操作之前执行还是在与操作相同的仿真时间上执行。因此,当触发和等待同时发生的时候,事件的triggered属性能够消除一个常见的竞争。根据等待和触发进程的执行顺序,一个等待事件触发和阻塞进程有可能(可以或者不能)解除阻塞。然而,无论等待和触发操作发的进程顺序如何,一个等待触发状态的进程总是能够解除阻塞。

五、面向对象编程

   面向对象编程和过程编程有所不同,面向对象编程数据和对数据的操作封装到一个独立的对象中。对象就是封装数据和对数据操作(方法)的编程结构,它包括了数据成员和方法(method),方法也就是一个任务或者函数,对对象内的数据进行操作。过程编程语言就如verilog和C,数据和对数据的操作的函数和任务时各自独立的。
5.1 类(class)

类可以在SystemVerilog中的program/module/package内部或者外部定义,可以在program或者module内使用。类将数据和第数据的操作封装在一起。

5.1 类的基本概念
  • 属性(property):也就是类中定义的数据(变量)成员;
  • 方法(method):也就是类中定义的函数或者任务;
  • 句柄(handle):也就是指向对象的指针,即该对象的地址入口。

      对于用户使用对象需要做如下的三个基本步骤:

    1) 定义类。例如下面定义的类就是提供了一个包对象的模板:
    class packet;
    ...
    endclass
    class long_packet;
    ...
    endclass

2) 在module或者program、class、function。task等地方声明对象。
下面声明了三个对象,两个packet的对象,一个是long_packet的对象。
packet my_packet;
packet packet_array[32];
long_packet my_l_packet;
对象的标识符(my_packet/packet_array/my_l_packet)是该例化对象的句柄。当该对象被创建的时候,该句柄才有效,默认情况下句柄将为空(null)。
3) 通过构造函数new创建对象的例化。
下面是通过new()这个构造函数给对象分配内存空间,并且把入口地址赋给对象的句柄:
my_packet = new(168);
my_l_packet = new();
一个类也可以在其定义中引用自身,自我引用可以用来声明比较复杂的数据结构,例如双链表或者查找树,这种引用有别于使用this操作符。
对于类中的数据成员和方法,我们可以通过“.”操作符来访问。

5.2 构造函数

SystemVerilog中的对象可以通过构造函数new来初始化。如果一个数据成员没有在构造函数new中初始化,它将是系统的默认值。在构造函数中,数据成员可以被赋予初始值,如下面所示:
function new();
addr=3;
foreach(data[1])
data[i] = 5;
endfunction
也可以通过参数传递,使用参数对数据成员进行初始化,例如:
function new(logic [31:0] addr=3,d=5);
this.addr=addr;
foreach(data[1])
data[i] = d;
endfunction
当存储空间不再被某一个对象占用时,SystemVerilog像Java一样,能够自动的回收对象例化(例如,内存分配)。为此,验证平台无须采用析构函数来释放空间,只要该例化没有被应用,系统就能自动析构。当然,用户也可以通过将null赋值给一个对象,这样也可以将其分配的空间收回。

5.3 静态属性与方法

静态属性——每一个类的例化(也就是每一个对象)都拥有它自身每一个变量的复制,有时要求所有的对象共享一个变量,我们可以通过使用static变量来声明数据成员。对于类的静态属性,无需产生一个该类型的对象就可以直接使用;而对于所有类的例化它们共同拥有该静态属性的一份复制。我们可以这样理解:类的静态属性是存放在类中的,而不是在每个类的对象中。
方法也可以被声明成静态的,即静态方法。静态方法对于类的所有对象都可以访问,它就像一个常规的方法,可以在类的外部被调用——即使没有该类的例化。静态方法不能访问非静态的成员(类的属性和方法),但它可以直接访问类的静态属性或调用同一个类的静态方法。在静态方法内部访问非静态成员或访问特殊的this句柄是不允许的,并且会导致一个编译错误。静态方法不能被声明为虚方法(virtual)。
静态方法不同于一个具有静态生命周期的方法。前者指的是类内部方法的生命周期,而后者指的是方法内部的参数和变量的声明周期,如下所示:
class TwoTasks;
static task foo(); ... endtask //具有自动变量生命周期的静态方法
task static bar(); ... endtask //具有静态变量声明周期的非静态方法
endclass
默认情况下,类的数据成员以及方法的参数和变量具有自动的声明周期。

5.4 this操作符

当使用一个变量时,SystemVerilog会在当前程序范围内查找,接着回到上一层范围查找,直至该变量被找到。假如一个类的内部要明确的指定引用的就是该类的成员,SystemVerilog提供了this这个操作符(关键字)。关键字this被用来明确地引用当前类的属性或方法,关键字this对应着一个预定义的对象句柄,这个句柄可以在该对象内部使用,并访问内部成员。关键字this只能在非静态方法中使用,否则会出现错误。

5.5 对象的赋值与复制

声明一个类的变量仅仅定义了对象的名字(标识符、句柄)。如:
Packet p1;
上述例子定义了一个变量p1,它是一个类型为Packet类的对象的句柄,其初始值为空(null)。在Packet类型的例化被创建之前,对象并不存在,并且p1位空句柄。
p1=new();
如果声明了另一个变量并且将先前的句柄赋值给新的对象,如下所示:
Packet p2;
p2=p1;
那么,仍然只有一个对象,它可以使用p1或p2引用,因为new只执行一次,所以只产生一个对象,即两个句柄指向同一个对象。
如果将上面的例子按如下的方式改写,那么会产生一个p1的复制,如下所示:
Packet p1;
Packet p2;
p1=new;
p2=new p1;
最后一条语句是构造函数new的第二次执行,因此创建了一个新的对象,它内部的属性全部复制自p1的内容,这种方法的复制称作浅复制(shallow copy, 或称作浅拷贝)。所有的变量都被复制:整数、字符串、对象的句柄等。然而,其中包含的对象没有被复制,复制的只是它的句柄;与前面例子一样,存在相同对象的两个名字;即时在类的声明中包含了构造new的例化。
另外需要注意:第一,类的属性和例化对象可以直接在类的声明中初始化;第二,浅复制不会复制对象(不会创建新的对象);第三,在需要的时候实例限定可以被串接起来以便到达对象或穿越对象层次。
a1.a.j //访问到a对象
p.next.next.next.val //通过一系列句柄的串接来访问到val
为了实现完整(深度)复制(或称为深拷贝),其中每一个数据成员(包括嵌套的对象)都要被复制,一般情况下需要使用者定制特殊的复制程序。

5.6 快外声明

如果需要将方法定义放在类声明体的外部,可以通过两个步骤完成:首先,在类内声明方法的原型(无论它是任务还是函数)、任何限定符(local、protected或virtual)以及完整的参数说明加上extern限定符。extern限定符指示方法体(它的实现)在类的外部定义。接着,在类声明体的外部定义完整的方法实现(与原型类似但没有限定符),并将这个方法绑定回它所属的类,通过使用类名和一对冒号(::)对方法名做限定。
快外方法声明必须与它的原型声明完全一致;语法上的唯一不同点是方法名字前面加入了类名字和范围操作符(::);快外方法声明必须在与类声明相同的作用范围内声明。
SystemVeriog增加了一个被称为$root的隐含的顶级层次。任何在模块边界之外的声明和语句都存在于$root空间中。所有的模块,无论它处于哪一个设计层次,都可以引用$root中声明的名字。这样,如果某些变量、函数或其它信息被设计中的所有模块共享,那么我们就可以将它们作为全局声明和语句。全局声明和语句的一个使用实例如下:
reg error _flag; // 全局变量

function compare (...); // 全局函数
always @(error_flag) // 全局语句

...

module test;
     chip1 u1 (...)

endmodule
module chip1 (...);

          FSM u2 (...);
          always @(data)
          error_flag = compare(data, expected);
   endmodule

module FSM (...);

          ...
          always @(state)
                 error_flag = compare(state, expected);
   endmodule


六、虚接口

6.1 端口模式和时钟控制块

通过端口模式可以指定接口中信号的方向,而时钟控制块可以指定某些信号被某个时钟同步(采样和驱动)。

1、端口模式

为了限制在一个模块中对接口的访问,我们可以在接口内定义端口模式列表,通过采用关键字modport,在其内部定义信号的方向。在顶层连接的时候,我们可以通过实体接口作为传递对象,而例化可以自动查找自己对应的端口模式。

2、时钟控制块

接口定义了验证平台与被测设计进行通信的信号,然而接口并没有任何显示的时序规范、同步要求或时钟控制范例。SystemVerilog添加了时钟控制块(clocking block),它能够识别时钟信号,并能够获取该模块中指定的时序和同步要求。时钟控制块封装了同步于一个特定时钟的信号,并且为其显示地指定时序。时钟控制块是实现基于周期(cycle based)验证方法学中的一个关键元素,使得用户能够在一个更高的抽象层次上编写验证平台。
时钟控制块中所有的输入或双向信号都在对应的时钟事件发生的时刻被采样。同样,时钟控制块中所有输出或双向信号都在对应的时钟事件发生的时刻被驱动。双向信号既被驱动也被采样。时钟偏差指定了信号偏离时钟事件多少个时间单位被采样或驱动。输入偏差默认是负的,也就是说它们总是指向时钟事件之前的一个时间点,而输出则总是指向时钟事件之后的一个时间点。

七、随机测试

7.1 随机生成机制

SystemVerilog有四种机制可以创建随机激励,实现随机数的产生和随机控制。
1)采样SystemVerilog内置的系统函数来产生随机数,其中有$urandom和$urandom_range等,除此之外,还有一些列标准概率分布的系统函数:$random、$dist_uniform、$dist_normal、$dist_exponetial、$dist_possion、$dist_chi_square、$dist_i和dist_erlang,
2)randcase和randsequece结构来实现随机的分支选择;
3)基于对象的随机生成,随机地初始化对象的数据成员的值;
4)标准随机函数std::randomize()可以随时对任意变量进行随机化并添加约束。
1、随机系统函数
系统函数$urandom提供了生成伪随机数的机制。该函数返回一个32位的无符号数,其语法如下所示:
function int unsigned $urandom[ ( int seed ) ];
系统函数$urandom_range()可以在指定的范围内返回一个无符号数。其语法如下所示:
function int unsigned $urandom_range(int unsigned maxval, int unsigned minval=0);
该函数会返回一个在(maxval, minval)之间的无符号整数。
系统函数$random可以生成一个32位的伪随机数,该随机数是有符号数,为此可以是正数或者负数。
random_function::=$random[(seed)]。
除了上面提到的可以产生伪随机数的系统函数外,SystemVerilog还提供了产生标准随机概率分布的系统函数,见P120页。
2、randcase/randsequence
randcase关键字的引入,使得条件语句可以根据权重随机地选择执行某一条分支。每个分支的条目表达式是非负的整数,代表分支权重。每个分支的权重除以所有权重的和就是该分支被选中的概率。例如:
randcase
3:x=1;
1:x=2;
4:x=3;
endcase
如果一条分支的权重被指定为0,那么这条分支就不会被执行。如果所有的randcase分支的权重都被指定为0,那么没有分支会被选中,仿真工具会发布一个警告信息。randcase的权重可以是任意表达式,而不仅仅是常量。
除了随机分支选择(randcase)之外,SystemVerilog还提供了随机序列生成(randsequence)。见P121页。
3、基于对象的随机生成
SystemVerilog可以使用一个面向对象的方法来为对象的成员变量初始化随机值,它通过rand或者randc关键字来对数据成员进行定义。基于对象的随机生成包含了三个部分:定义随机变量、指定约束条件(可选)、通过调用内置randomize()方法产生随机。
a. 随机变量
类中的数据成员可以使用rand和randc关键字来声明为随机的变量。其语法如下:
class_propert::={property_qualifier} data_declaration
property_qualtifier::=rand | randc
使用rand关键字声明的变量是标准的随机变量。它们的值在其取值范围内均匀分布。
使用randc声明的变量是周期随机变量,它在声明范围内的一个随机排列中循环地选择所有的值。randc的基本思想是在取值范围内的所有值中随机的遍历,并且在一次遍历中不会有重复的值。当本次遍历结束后,会自动启动一个新周期的遍历。
在定义随机变量的时候需要注意的事项如下:
1) 解析器可以随机化任何整型类型的单一变量;
2) 数组可以声明成rand或randc,在这种情况下,数组的所有成员元素都被当做rand或randc看待;
3) 动态数组和关联数组可以使用rand或randc声明。数组中的所有元素都被随机化,并会重写先前的任何数据;
4) 使用rand或randc声明的动态数组的大小也可以被约束,在这种情况下,数组应该根据大小的约束被重新调整,并且会接着随机化所有的数组元素;
5) 对象句柄可以使用rand声明,在这种情况情况下,该对象的所有变量和约束都被并发地求解,对象不能使用randc声明。
b. 约束定义
在随机变量被定义后,可以用约束块来限制变量的取值空间,像任务、函数和变量一样,约束块是类的成员,在一个类中,约束块的名字必须是唯一的,语法如下:
constraint constraint_identifier { constranit_block_item }SystemVerilog的约束支持以下两种方式:
1) 指定取值范围,通过指定随机值的上限或者下限,或者某一个集合,或者某一特定值来限制范围;
2) 分布式约束,为随机值的取值范围指定特定的权重,来影响其取值的概率分布。
下面我们讨论在约束块中使用的几种操作符和条件或者循环语句:inside,dist,->,if/else,foreach及其应用。
inside操作符:
约束支持使用inside操作符指定取值在整数值集合以及枚举集合。如果没有其他的约束,inside操作符在取值的时候,所有的可选值(包括单一值或范围值)都具有相等的概率。inside操作符的否定形式如下所示:!(expression inside {set})表示其取值范围在表达式的集合范围之外。
rand interger a, b, c;
constraint c2 { a inside {b, c}; }
rand integer x,y,z;
constranit c1 {x inside {3, 5, [9:15], z, [y*2:y]};}
有一点我们必须注意:inside操作符是双向的,因此,上述代码中的{a inside {b, c};}等价于{a==b || a==c}。
dist分布操作:
除了指定取值范围在某个集合内,约束也指定集合内不同的权重分布。关键字dist具有两个特性:对指定集合的范围进行关系测试;若为真,则为其指定统计分布权重。
expression dist {dist_list | value_range [:= | :/ dis_weight]};
如果表达式的值包含在集合中,那么分布操作符dist的计算结果为"真";否则计算结果为"假"。分布集合为以逗号分隔的整型表达式和范围的列表。列表中的每一项都可以带有一个可选的权重,这个权重使用“:=”或":/"操作符说明。如果没有为某一条目指定权重,那么缺省权重是:=1.权重可以是任意的整型表达式。
当采用:=操作符为一个条目指定一个权重时,如果条目是一个范围,它为范围中的每一个值指定权重。当采用:/操作符为一个条目指定权重时,如果条目是一个范围,它会将范围作为一个整体指定权重;如果范围中有n个值,那么范围中每个值的权重为range_weight/n。
采用dist为表达式或者变量指定分布的时候要注意下列限制:
1) dist操作不可以应用到randc变量;
2) dist表达式要求表达式至少包含一个rand变量。
条件操作:
约束提供了两种结构来声明条件操作:->(蕴涵操作)和if...else。
foreach迭代操作:
迭代约束让我们能够使用循环变量和索引表达式以一种参数化的方式约束数组变量。foreach结构制定了数组元素上的迭代。它的参数是一个标识符跟着一个包围在方括号中的循环变量,这个标识符说明了任意类型的数组(固定尺寸、动态数组、联合数组或队列)。每一个循环变量均对应于数组的某一维。
solve...before操作
解算器必须确保选择的值在有效的值组合中具有均匀的值分布(也就是说,所有的有效值组合在求解过程中具有相等的概率)。这个重要的特定能够保证所有的值组合的可能性是相等的,这也就使得随机化能够更好地探索整个设计空间。然后又是我们希望强制某种组合出现的频率更高一些。SystemVerilog提供了solve...before关键字来解决。如:
constraint order { solve s before d; }
在这个例子中,order约束块向解算器指示s在d被求解之前求解。变量排序可以被用来强制某些边界条件出现的机会更频繁一些,但solve不会改变求解空间,并且不会引起解算器失败。
注意:如果一个派生类(子类)中的约束与其基类(父类)中的约束具有相同的名字,那么它会改写基类约束。randomize()任务是虚方法,因此,它以一个虚方法的属性来对待类的约束。当一个命名的约束在扩展类中被重新定义的时候,基类中的定义会被子类中的改写。
c. 随机方法
对象中的变量使用randomize()方法进行随机化,每个类都有一个内建的randomize()虚方法,它的声明原型如下:
virtual function int randomize();
randomize()方法是一个虚函数,它为对象中的所有激活的随机变量产生随机值,产生的随机值应该符合激活的约束。如果randomize()方法成功地设置了所有的随机变量和对象的有效值,那么它返回1;否则返回0。检查返回状态时很有必要的,因为状态变量或继承类的约束的增加都会导致表面上简单的约束不再满足。
每一个类都有内置的pre_randomize()和post_randomize()函数,它们在计算新的随机值之前和之后被randomize()自动调用。用户可以在任何类中重写pre_randomize()方法以便执行对象被随机化之前的初始化和预处理。用户可以在任何类中重写post_randomize()方法以便执行对象被随机化之后的清除、打印诊断以及后处理。如果这些方法被重写,那么它们必须先调用对应的父类方法,否则它们在随机化之前和之后的处理步骤会被忽略。pre_randomize()和post_randomize()方法不是虚拟的。然而它们会被虚拟的randomize()方法自动调用,所以它们看上去像是虚拟的。
使用随机化方法randomize()需要注意如下事项:

被声明成static的随机变量被随机变量在其中声明的类的所有实例所共享,每次调用rando()方法的时候,在每一个类实例中的变量都会被改变;

  • 如果randomize()失败,那么约束不可实行且变量保持为原来的值;
  • 如果randomize()失败,post_randomize()方法不会被调用;
  • randomize()方法是内置并且不能被重写;
  • randomize()方法实现了对象随机稳定性,一个对象可以通过调用它的srandomize()方法来设置种子;
  • pre_randomize()和post_randomize()内置方法是函数并且不能被阻塞。
    d. 随机使能控制
    一般情况下,所有定义的随机变量和约束默认都是被激活。但是,在运行的过程中我们可以通过使用内置方法rand_mode()和constraint_mode()对随机变量的状态和约束进行控制、关闭或者激活。
    随机变量使能控制:
    rand_mode()可以用来控制激活或关闭一个随机变量。当一个随机变量处于未激活状态的时候,这个随机变量就好像没有使用rand或randc声明一样。未激活的随机变量不会被randomize()方法随机化,并且它们会被解算器当做一般的状态变量。所有的随机变量最初都是激活的。rand_mode()方法的语法如下:
    task object [.random_variable]::rand_mode(bit on_off);
    或者:function int object.random_variable::rand_mode();

      object是对象句柄,并且随机变量在其中定义。random_variable是操作被应用的随机变量的名字。如果没有指定随机变量的名字(仅在作为任务调用时才允许出现这种情况),那么这个动作会被应用到对象内所有随机变量。

    当作为任务调用时,rand_mode方法的参数确定了执行的操作,rand_mode参数等于0时:将指定的随机变量设置为未激活状态;rand_mode等于1时:将指定的随机变量设置为激活状态。对于非压缩数组变量,random_variable可以使用索引来指定数组中的单个成员。省略索引会导致数组中的所有成员都被调用影响。如果随机变量是一个对象句柄,那么仅仅该变量的模式被改变,而该句柄对象内的随机变量的模式不会改变。如果指定的变量在类层次中不存在或者它虽然存在但没有使用rand或randc声明,那么编译器应该发布一个错误信息。
    如果作为函数调用,那么rand_mode()返回指定变量当前的激活状态。如果随机变量处于激活状态(ON),则返回1;如果随机变量处于未激活状态(OFF),则返回0.rand_mode()方法的函数形式仅能接收单一变量,因此,如果指定的变量是一个非压缩数组,那么必须使用索引来选择单一的元素。
    rand_mode()方法是内置的,并且不能被重写。

随机约束使能控制:
默认情况下,所有的约束都是激活的。constraint_mode()方法可以用来控制激活或关闭一个约束。当约束处于未激活(关闭)状态时,它不会被randomize()方法采用。constraint_mode()方法的语法如下:
task object[.constraint_identifier]::constraint_mode(bit on_off);
或者:function int object.constraint_identifier::constraint_mode();
当作为任务调用的时候,constraint_mode方法的参数确定了需要执行的操作,constraint_mode参数为0时,将指定的约束块设置成未激活状态,这样它就不会被后来对randomize()方法的调用所强制;constraint_mode参数为1时,将指定的约束块设置成激活状态,这样它会被后来对randomize()方法的调用所考虑。如果指定的约束块在该类中不存在,那么编译器会发布一条错误信息。
当作为函数调用的时候,constraint_mode()方法返回指定约束块当前的激活状态。如果约束处于激活状态(ON),那么它返回1;如果约束处于未激活状态(OFF),那么它返回0。
constraint_mode()方法是内建的,并且不能被重写。
约束的动态修改:
SystemVerilog中提供了如下几种方法来动态地修改随机化方法:

  • 蕴涵( -> )以及if...else允许声明具有判决条件的约束;
  • 可以使用constraint_mode()内建方法来激活或关闭约束块。初始情况下,所有的约束块均处于激活状态。未激活的约束会被randomize()函数忽略;
  • 可以使用rand_mode()内建方法来激活或关闭随机变量。初始情况下,所有的rand和randc变量都处于激活状态。未激活的随机变量会被randomize()函数忽略;
  • 在dist约束中的权重可以被改变,从而影响了被选定集合中特定值出现的概率。
    另外,我们可以在调用randomize的时候通过with加以约束。

通过使用randomize()...with结构,用户可以在randomize()方法的调用点上声明内联约束。这些额外的约束与对象约束共同起作用。
4. 标准随机函数
标准随机函数(std::randomize())使得用户能够随机化当前范围内的数据,而无需定义一个或实例化一个对象。标准随机函数的语法如下:
scope_randomize::= [stb::] randomzie([variable_identifier_list]) [with constranit_block]
标准随机函数与类的随机方法的作用相同,只是它仅限于操作当前范围内的变量而不是类成员变量。函数的参数指定了哪些需要赋值为随机值的变量,也就是随机变量。

八、继承与多态

从基类(父类)做扩展并创建新的派生类(子类)的过程,就是类的派生。当一个类被扩展并创建了派生类(子类)之后,该派生类继承了其基类的数据成员、属性和方法,这就是类的继承。所谓多态,也就是可以呈现多个“形状”(功能)的能力。在面向对象语言中,一个新的派生类被创建后,其中基类中的某些方法可以通过重写(override)被重新定义,这个过程就是重写。当一个对象调用了一个被重写的方法,对象的类型将决定调用方法的实现方式。通常,这是一个动态的过程,动态的选择方法的实现方式就是多态。

8.1 类的继承与重写

当一个类从另外一个类继承其属性,原有的类就称为基类或者父类,而扩展引申出来的类称为派生类或者子类。在继承的过程中,存在如下的一些基本规则:

  • 子类继承父类的所有数据成员和方法;
  • 子列可以添加新的数据成员或者方法;
  • 子类可以重写基类中的数据成员和方法,也就是重写;
  • 如果一个方法被子类重写,其必须保持和基类的原定义有一致的参数;
  • 子类可以通过super操作符引用父类中的方法和成员;
  • 被声明为local的数据成员或方法只能对自身可见,而对外部和子类不可见;
  • 被声明为protected的数据成员或方法,对外部不可见,对于自身和子类可见;

      SystemVerilog通过extends关键字来扩展类,语法如下:  class class_name extends base_class_name

    ...
    endclass
    一个类的对象被定义的时候是一个空句柄,当其构造函数被调用的时候才分配空间,其句柄也指向该片空间的入口地址。同样,当一个派生类的对象被定义的时候,它也是一个空句柄,当其构造函数被调用的时候,分配的空间不仅有来自父类继承而来的数据和方法,还有自己添加的部分;其中新添加的数据成员和方法存储在独立的空间中,保持与继承成员和方法的独立性。派生类中,重写和添加的数据成员和方法都保持独立的空间。

8.2 子类对象和父类对象的赋值

子类对象是它们父类对象的有效表示。子类对象可以赋值给父类对象,在这个过程中需要遵循以下规则:

  • 子类对象是父类的有效表示,可以赋值给父类对象;

8 父类对象可以通过$cast的方式尝试给子类对象赋值,并判定是否赋值合法;

   在访问对象的过程中遵循以下规则:通过父类的对象去引用在子类中重写的属性或方法,结果只会调用父类的属性和方法;通过子类对象可以直接访问重写的属性或方法;在子类扩展过程中新增的属性和方法对于父类对象不可见;子类可以通过super操作符访问父类的属性和方法,以区分于本身重写的属性和方法。
8.3 构造函数调用

一个子类在实例化的时候会调用其构造函数new()为其分配空间。在该函数中定义的任何代码执行之前,new()执行的第一个默认动作是调用其父类的构造函数new(),并且会沿着继承关系按这种方式一直向上回溯调用。因此,所有的构造函数会按正确的顺序调用执行,它们都是起始于根基类并结束于当前的类。
如果父类的构造函数需要参数,那么可以有两种方法做参数传递:一种是在扩展的时候提供初始化参数,另一种是在派生类中直接使用super关键字,调用父类的构造函数。如果参数总是相同的,那么它们可以在被扩展的时候指定:
class EtherPacket extends Packet(5); //将5传递到与Packet中的new方法中
一个更加通用的方法是直接使用super关键字来调用父类的构造函数:
function new();
super.new(5);
endfunction
应该注意的是,super.new(5)必须是new函数中执行的第一条语句。

8.4 虚方法与多态

默认情况下子类中重写的方法对于父类是不可见的。如果我们希望重写的方法能够被父类看得到,也就是父类能够调用子类的重写方法,可以使用虚方法和多态。
1) 虚方法

类中的方法可以在定义的时候通过添加virtual关键字来声明一个虚方法,虚方法是一个基本的多态性结构。虚方法为具体的实现提供了一个原型,也就是说派生类中,重写该方法的时候必须采用一致的参数和返回值。虚方法可以重写其所有基类中的方法,然而普通的方法被重写后只能在本身及其派生类中有效。从另一个角度解释,每个类的继承关系只有一个虚方法的实现,而且是在最后一个派生类中。
一个方法在被重写的时候可以声明为虚方法;一旦方法被声明为虚方法,它在后续的继承过程中就永远是一个虚方法,不管重写的时候是否使用virtual。也就是说,虚方法在其子类扩展、重写的时候virtual是可选的而不是强制的。
2) 多态

   封装可以隐藏实现细节,使得代码模块化,继承可以扩展已存在的代码模块,它们的目的都是为了代码重用,而多态则是为了实现另外一个目的:接口重用。虚方法是类中声明的非静态方法,在类中该方法声明带有virtual,我们称带有虚方法的类为多态类。多态类提供了相同的操作接口,但能适应于不同的应用要求。当基于父类所设计的程序在应用的时候需要适应子类操作的变化时,就需要使用虚方法通知编译器这种可能的变化,使编译器为虚方法调用生成特别代码,以便运行时对虚方法调用采用动态绑定。为此,我们说虚方法与重写的实现就是多态。

   实现多态需要以下三个步骤:
  • 在父类中定义虚方法。如basepacket类中的虚方法printb;
  • 在子类中重写父类中的虚方法。如my_packet中对printb的重写;
  • 申明父类对象的变量(如P1),该变量既可以指向父类对象(如P1自身),也可以执行子类对象(如P2)。当变量指向父类对象时,调用的是父类方法;当变量指向子类对象时,调用的是子类同名方法。为此,当父类的对象指向不同的子类对象的时候,虚方法(如printb)可以表现出不同的实现方法,而它们都可以通过统一的父类对象实现访问。
8.5 虚类和参数化类

为了实现代码重用,SystemVerilog还引入了虚类(抽象类)和参数化类两个特殊的类。虚类主要作为基类模板,不可以实例化;参数化类就如C++中的模板(template),可以根据需要修改其中参数的类型。

8.5.1 虚类

一个基类定义了其子类的原型,共有基类可采用virtual将其变得抽象化,为此,虚类也被称为抽象类。虚类的对象是不可以被直接构造使用的,也就是虚类一般不用来定义对象。虚类的构造函数只能在其派生类的构造函数的调用过程中使用。虚类内也可以定义虚方法,虚方法是一个基本的多态性结构。虚类中的虚方法可以只提供原型而无需具体实现,也就是成为纯虚方法,只要virtual前面添加一个pure的关键字就可以,其扩展过程中可以被重写为纯虚方法或者普通需方法,并提供一个具体的实现。虚类也可以被进一步扩展为另外一个虚类,但是若要扩展为非虚类(普遍类),所有的纯虚方法必须被重写,并且提供一个具体的实现,以便在后续过程中使用。

8.5.2 参数化类

为了避免为每个特定的类编写类似的代码,SystemVerilog也支持参数化类,SystemVerilog参数化机制可以用来参数化一个类,如:
class vector #(int size = 1);
bit [size-1 : 0] a;
endclass
接下来,这个类的对象就可以像模块或接口一样例化:
vector #(10) vten; //size为10的vector对象
vector #(.size(2)) vtwo; //size为2的vector对象
typedef vector #(4) Vfour; //size为4的类
任何类型都可以用作参数,包括用户定义的类型,例如类或结构体。通用类和真实参数值的一个组合被称作一个特例。每个特例又具有一组单独的静态成员变量。为了在多个特例间共享静态成员变量,这些变量必须放置在一个非参数化的基类中。

8.6 约束重写

类在继承的过程中除了可以重写数据成员和方法外,其约束也可以被重写。约束与类数据成员和方法遵从相同的规则,如果一个继承类中的约束与它父类中的约束具有相同的名字,那么它会重写基类约束。
在约束的定义过程中,我们也可以把约束分为两个类型:硬约束和软约束。所谓硬约束是把约束限制在一个确定的范围之内,例如:以太包的包长大于64字节小于1522字节;软约束就是通过变量来限制范围,这样我们通过变量来动态地改变需要的约束,例如x<y,而y是一个变量,所以我们可以通过控制y进而动态地对x进行控制。

8.7 数据的隐藏与封装

在SystemVerilog中,未被限定的类属性和方法默认是公共的(public),它们对访问对象的任何人都是可见的。被标识成local的成员仅对类内的方法可见,而且这些本地成员在派生类内是不可见的,即是无法访问的。当然,访问本地类的属性或方法的非本地非本地方法可以被继承,并且作为子类的方法它可以正常工作。
除了可以被继承以及对子类可见外,被标识成protected的类属性或方法具有本地成员的所有特性。需要注意的是,在类的内部,本地方法或类属性可以被引用,即时它属于一个不同的实例对象。
类成员可以被标识成local或protected;类属性可以进一步定义成const,方法可以进一步定义成virtual。在指定这些修饰符的时候没有预定义的顺序;然而,对于每一个成员它们只能出现一次,也就是不可能将成员同时定义为local或protected。

九、功能覆盖率

基于约束的随机激励测试,在测试整个设计的各种状态时,如何才能知道哪个状态或功能点已经被验证过了,这就需要一个衡量标准,功能覆盖率就因此产生,是供验证工程师分析并回答一个终极问题:验证是否收敛?
一般来说,覆盖率就是指设计中的哪些部分被测试过了。其中,有验证计划目标覆盖率、代码覆盖率和功能覆盖率三类覆盖率。

9.1 目标覆盖率

目标覆盖率是指在验证计划中规定的需要验证点的目标值。在验证计划中,当验证点实际覆盖率没有达到100%的时候,说明验证工作还未还未完成目标方案。没有达到100%的项目需要通过添加测试用例或者修改约束等来对其进行充分的验证。验证计划中列出的项目都要一一被测试,当然这需要一个比较全面和完整的验证计划。为此,在验证环境搭建的前期,制定验证计划,明确验证点并确定目标覆盖率是一项艰巨而且细致的工作。
制定验证计划中的功能点的时候,需要考虑如下三个问题:

  • 哪些功能点需要检查?
  • 这个功能点的哪些数据需要检查?
  • 如何对这些数据进行采样?
    哪些功能点需要检查呢?这样根据设计的具体情况而定,一般情况下,以下几类是参考的对象:功能要求、接口要求、系统规范、协议规范等。具体验证计划中可能表现为:FIFO是否溢出和空读、外部接口是否遵从以太网物理层的传输协议、是否满足系统规范要求的支持发送超长包、内部的AMBA总线是否符合协议要求等。
9.2 代码覆盖率

代码覆盖率是从软件编程的角度来分析设计代码是否被充分的验证。代码覆盖率可以通过仿真器在运行的过程中自动统计数据并生成报告。代码覆盖率只是一个统计报告,并不能指出设计行为是否正确,它只是表明设计代码中的哪些部分已经测试或者没有被测试过,验证工程师或者设计工程师通过分析报告,了解是否存在冗余的代码,是否需要增加测试激励以保证代码能够被充分的测试。代码覆盖率可以通过如下方式来统计:

  • 行覆盖率:检查某行代码是否被执行过;
  • 分支覆盖率:检查条件分支是否都被执行过;
  • 条件覆盖率,表达式覆盖率:通过真值表分析表达式各种逻辑组合;
  • 有限状态机覆盖率:检查每个状态是否被覆盖,状态之间的跳转是否被执行;
    代码覆盖率一般是在测试回归的时候,在所有的测试用例仿真结束后做一个总结报告,然后进行分析。
9.3 功能覆盖率

一般情况下,不论直接测试还是随机测试,验证计划中都会规定哪些功能点是需要测试的。在直接测试中,可能一个用例就对应测试某一个功能点,所以很多情况下,回归测试所有用例全部通过就可以表示达到了验证计划的要求;而在随机激励测试中,一个测试用例可能会测试到不同的功能点,或者是这些功能点的局部,为此需要有一个具体的衡量标准。
SystemVerilog中提供了如下两种类型的功能覆盖率表达形式:
1) 面向控制的功能覆盖率。通过cover对断言中的sequence或者property做统计(在第十章我们再详细讨论)。
2) 面向数据的功能覆盖率。在特定的时间点对某些数据值使用covergroup做采样统计分析。下面我们来详细介绍该种功能覆盖率;
SystemVerilog功能覆盖率结构可以实现如下功能:

  • 定义和统计变量和表达式的覆盖率及其交叉覆盖率;
  • 支持自动化创建覆盖点计数器和用户自定义覆盖率点计数器;
9.3.1 覆盖组(covergroup)

SystemVerilog中提供了covergroup来定义一个覆盖组。每个功能覆盖组内可以定义以下信息:

  • 该覆盖组采样的时钟事件;
  • 一系列的覆盖点;
  • 覆盖点的交叉覆盖;
  • 可指定形式参数;
  • 覆盖组的属性参数;
    covergroup是一个用户自定义的类型,其类似一个class,需要通过new来构造而且可以多次例化;可以在module/program/interface/class/package等结构体内定义覆盖组,其语法如下:

covergroup name [(<list_of_args>)] [clocking_event];
<cover_option>;
<cover_type_option>;
<cover_point>;
<cover_cross>;

endgroup [: identifier]
覆盖组可以定义参数,在其例化时需传递实参,实参在new的过程中采样;时钟事件定义了功能覆盖组的采样条件,若不定义时钟采样条件,则需要通过通过内置的采样方法sample()。其中,最重要的是通过coverpoint和cross的定义覆盖点和交叉覆盖点,其能够指定采样变量的对象。
covergroup也可以在class内定义使用,可以对该class的属性定义对应的功能覆盖率模型,无论该属性是否是protected或者是local;在类中可以不使用例化,但仍需在类的构造函数中调用new对覆盖组进行分配初始化。覆盖组也可以定义一些属性参数去控制和管理其数据统计和计算的过程。

9.3.2 覆盖点(coverpoint)

覆盖组内可以通过关键字coverpoint定义一个或者多个覆盖点。覆盖点可以针对一个整型的变量或者表达式,每个覆盖点内会有对应的一组分组柜(bin),计数器可以是用户自定义的或者是自动创建的。coverpoint语法如下:

[label: ] coverpoint <expr> [iff(<expr>)]

{

[<(coverage_option)>]

bins name [[ ]] = {valude_set} [iff(<expr>)];

bins name [[ ]] = (transitions)[iff(<expr>)];

bins name [[ ]] = {value_set};

ignore_bins name = {value_set};

ignore_bins name = (transitions);

illegal_bins name = {value_set};

illegal_bins name = (transitions);

}
label是可选的覆盖点的名称,用户可指定具体的名字以便通过层次化引用来访问该覆盖点;若不指定,则由仿真器自动分配。该覆盖点在采样事件激发或者内置sample被调用时采样统计。iff结构可以指定采样条件,若指定条件不成立时,则不作覆盖率统计。在显示指定分组柜时,可以通过关键字default将其他未分配到特定分组柜中的数值进行分配。

9.3.3 交叉覆盖点(cross)

覆盖组内可以通过cross关键字对两个或者多个覆盖点指定交叉覆盖点。若cross指定的交叉覆盖点中的成员是一个变量(未通过coverpoint指定的覆盖点),则系统会自动为该变量创建对应的默认的覆盖点;但是成员中不能是未通过coverpoint指定为覆盖点的表达式,这种情况必须事先为其定义覆盖点。交叉覆盖点的语法结构如下:

[label:] cross <coverpoint list> [iff(<expr>)]

{

bins name=binsof(binname) op binsof(binname) op ... [iff(expr)];

bins name=binsof(binname) intersect {value | [range]} [iff(expr)];

ignore_bins name=binsof(binname)...;

illegal_bins name=binsof(binname)...;

}
和coverpoint类似,label可以指定交叉覆盖点的名字,iff可以定义采样保护条件,bins可以定义普通的分组柜,ignore_bins可以定义忽略的交叉覆盖点,该类交叉覆盖点出现的时候不列入统计的范围,计数器不做自加;illegal_bins可以定义覆盖点,一旦出现该类交叉覆盖点,系统会报告错误。
关键字binsof可以为cross指定的表达式(覆盖点或者变量)生成对应的分组柜,其结果可以通过和其他分组柜做进一步的选择操作,从而生成期望的交叉分组柜。如下所示:
binsof(x) intersect {y}
表示覆盖点x中和给定y这个表达式的交集合,其反向表达式是:
!binsof(x) intersect {y}
表示覆盖点x中和给定y这个表达式交集以外的范围。

十、断言

断言描述的是设计的属性,可以表达设计意图。断言可以采用VHDL、Verilog、PSL、SVA等多种语言来描述,可以应用在动态仿真或静态分析中。断言的具体定义和使用在我的另一篇文章有详细描述,这里就不赘述了。

一个多月断断续续把《SystemVerilog与功能验证》这本书看完了,还是有很多收获的,对于扩展验证工程师的知识面挺有帮助的,方便今后深入了解各个方向的知识点。

更多相关阅读

UVM中uvm_component之间的transaction传输
UVM如何debug objection

作者:谷公子
首发博客:https://blog.csdn.net/W1Z1Q/article/details/102316012
更多IC设计相关的文章请关注IC设计极术专栏,每日更新。
推荐阅读
关注数
19597
内容数
1303
主要交流IC以及SoC设计流程相关的技术和知识
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息