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
-
-
-
二值/四值的区别
-
Z和X态是什么?
-
Z是高阻态,悬空未驱动,电阻相当于无穷大;
-
X是不定态,需要自行check逻辑确保一下;
-
-
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
-
作用
-
起别名:
可以给任何已有的数据类型、联合体等,起新名字(别名)。
-
自定义新的变量类型
-
提前声明 (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
一样。 -
定义与赋值语法
-
直接声明变量;
-
用
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(映射计算hierarchy 与 parameter的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
、'b1
、1
三个的区别!
- 注意:
-
-
字符串
-
语法:用
""
包裹. -
多行字符串:行尾 用
\
来换行. (新行空白格也将被算作字符) -
末尾间隔符:似乎会插入
\0
作为间隔符(和C一样,ASCII值为0).我很怀疑有没有…因为打印不出此间隔符…
-
-
时间表示
- 时间文本被解释为 realtime类型,将根据当前的时间精度四舍五入。
- 可选的时间单位有:s、ms、us、ns、ps、fs.
注释
- 同C,有行注释
//
和块注释\*, *\
.
2.2 赋值语句
变量间赋值
-
四态值赋给两态值
这是合法的;所有
X
和Z
都会变成0
.-
但关键是要检查是否有“未知态
X
传播”:Solution: 用
$isunknown()
,若表达式内有X
或Z
,则返回1.
-
参数传递与设置
SV提供了四种参数设置/传递的方式:
-
顺序赋值
类型 # (值1, 值2, ..., 值i);
-
显示引用形参名赋值
类型 # (.形参名1(值1), .形参名2(值2), ..., .形参名i(值i));
-
defparam
语句(不推荐)
类型强制转换
-
语法
// 取一个极端的例子,强转成自定义类型 typedef enum{red, green, blue}color; initial begin color tmp; tmp = color'(2); // 把2转成color类型. end
模块例化的信号传递 ⭐️
Verilog/SV提供了三种模块例化的信号传递方式:
-
自动的默认连接:同名信号自动连接
模块名 实例名(.*);
-
位置关联
按定义顺序传递给module端口形参.
模块名 实例名(实参1, 实参2, ...);
-
名称关联
可不按定义顺序传递给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的
==
、===
、==?
的区别?都是判断“不等”,但判断的数据范围不同。
===
能处理x
和z
态数据,==
不能。-
==
和``!=是两值运算符,但<font color=red>若有
x和
z态数据,返回结果是
x`。不管是不是两边的
x
一样,返回结果都是x
。 -
===
和!==
是四值运算符,要求两边的数据的x
和z
态也一毛一样才行!不管是不是两边的
x
一样,如果希望===
返回的结果是1,两边数据的x
和z
必须一样。 -
==?
和!=?
会忽略两边的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:
case
(普通case);casex
(表达式中的x
,都不参与比较);casez
(表达式中的x
和z
,都不参与比较).
case(表达式) // value可以用 'x' 和 'z' 进行多选. value1, value2: begin end value3: begin end value4: begin end default: begin end endcase
-
SV的
case
与 C的switch
的区别-
C里,若没有
break
,就会执行多个case
; -
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 数组与队列
-
概述
数组有三种:
-
静态数组(static array);
大小在声明时,确定。
-
动态数组(dynamic array);
可以多维或一维。但必然有一维,大小未指定,在使用前才分配。
-
关联数组(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是
x
或0
,取决于是四态还是二态——不会报错.
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
- 无返回值;
- 可以执行时序等“消耗仿真时间”的相关逻辑:如
@
、#5ns
等; - 可以调用function;
-
function
- 可以有(不必须)返回值;
- 不可以执行时序等“消耗仿真时间”的相关逻辑:如
@
、#5ns
等; - 不可以调用task;
function 返回类型 函数名(形参表); // 注意分号 //... return ...; endfunction
形参表
形参定义
同Verilog,有两种形参定义方式:括号内定义、任务/函数内定义;
-
括号内定义
【详见Verilog】
-
任务/函数内定义
【详见Verilog】
形参的端口类型
-
input (默认类型)
输入类型,值传递.
-
output
输出类型,值传递.
-
inout
输入输出时,各进行值传递.
-
ref
引用(指针/句柄)传递.
-
默认类型:
-
function/task的默认变量类型是
logic
; -
[经验]function/task传递对象时,除非加了 input/output/inout关键字,对象形参都默认是引用ref类型.
-
定义类型后,后方形参的端口/变量类型将都将是此类型,例如:
// 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
关键字-
作用:将本地变量/局部变量定义为“堆栈变量”。
-
使用方式
-
加在变量上——堆栈的局部变量;
-
加在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
-
大概作用
- 是整个dv的入口,分隔design与dv平台;
- 可封装整个dv的程序、函数、任务;
- 提供了语法,以调度Reactive区域内的执行逻辑,避免竞争;
-
语法
program 名称(信号, interface xx_if); //... endprogram
-
program
语法特点- 不能使用
always
、UDP,只能用initial
、final
; - 当
program
内的initial
全部结束时,会自动调用$finish
结束仿真; - 不能例化
module
、interface
、嵌套program
; - 必须用非阻塞赋值
<=
对clocking
block进行驱动; - 因为代码全运行在Reactive region set中,故可看见当拍中所有module的信号变化结果;
- 养成好习惯,
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;
-
注意:
- function/task是不是automatic、static,只影响其内部定义的变量,影响不到形参。
- 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;
可以给块语句加关键字
automatic
或static
来显式定义其生命周期,会同时改变其内部变量生命周期。 -
不考虑OOP,所有变量/function/task,默认为
static
;故一般function/task内的局部变量,都是被多个进程、方法的调用所共享的。
Verilog是为硬件服务的,和C/C++不一样;SV首先得兼容Verilog,因此也是默认static,后来才推出了automatic关键字。
-
考虑OOP,class内的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特有的特点。
-
核心区别:
- 大多数编程语言内(C/C++),局部变量就已经是动态创建与回收(对应SV里的automatic类型)了,局部变量与动态回收是绑定的;
- 只有Verilog,出发点是描述电路,因此独立于OOP的概念外引入了static概念,且将局部变量默认定义为static类型。
- 由于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
解答:
Case 1
是很正常是单进程串行执行的结果;Case 2
和Case 3
理由差不多。默认的initial块,是static的,其内定义的变量int j
自然也是static变量。但for
循环内临时定义的循环变量int i
是automatic的。导致类型匹配不上。编译器可能是出于保护的目的,进行了报错。Case 3
理由同上,j
是static变量,故只会初始化一次,即j=0
。后续for循环,每次j
都是0.Case 4
是本来是为了表明,task的默认类型也是automatic. 但这个代码不能实现这一点,因为task/function定义为static或automatic,是影响不到形参和实参的。Case 5
把变量定义为automatic,就获得了预期的结果。Case 6
和Case 2
的function几乎一样,为什么结果不一样?因为SV的class内的所有成员函数,默认也必须,都是automatic类。所以就不会有static的烦恼了。
3.2 调度机制与program
-
Ref
SystemVerilog调度机制与一些现象的思考 ——CSDN ⭐️ ⭐️
SV的仿真调度机制 ——CSDN
SystemVerilog Scheduling Semantics ⭐️⭐️ 言简意赅.
SystemVerilog LRM 学习笔记 – SV Scheduler仿真调度 CSDN ⭐️ 补充了“确定性(Determinism)和不确定性(Nondeterminism)”这些名词解释.
时间片与调度机制
-
time slot、Verilog/SV的宏观调度flow
-
Verilog与SV仿真的底层实现中,将仿真时间以多个离散的“时间片(time slot)”单位进行具体的调度与执行。
SV是离散事件执行模型,即不是每个time slot都执行,只会对基于事件、离散的、有效time slot进行执行。
-
单个time slot内,将划分为多个Event Region(e.g. Active、Inactive、NBA…),它们分别对应不同代码行为的“具体落实”,自上向下进行。
-
而不同的块语句(
always
、initial
、assertion
、primitive
、assign
等)都将分别创建1个进程(不是基于语句创建进程,而是基于块),在各自对应的region上等待被执行。在语法上,Verilog/SV毕竟是不检测竞争冒险的。因此早期RTL编程不规范,1个块语句内混用阻塞赋值与非阻塞赋值也不会报错。
-
当time slot内所有Area被执行完毕,当前time slot的信号值就确定了;同时进入下一time slot的更新。
-
-
timescale、time step、time slot的区别
时间片(time slot) 与 timescale 是不一样的。
- timescale:
- timescale规定了仿真的局部“最小单位”与“仿真精度(time precision)”;
- 可以在不同文件、不同module中定义不同的timescale的“精度部分”,实现差异化精度;
- time step:
- time step是工程中,全局最小的timescale的“仿真精度”的值;
- RTL中的
#1
,延后的是当前timescale的局部仿真精度的单位;RTL中的#1step
,延后的是全局timescale的最小仿真精度的单位;
- time slot:
- time slot是Verilog/SV底层实现的调度的最小单位,它的重点是内涵盖的多个region的概念,重点不在时间间隔本身;
- 两个time slot间隔1 time step.
- timescale:
Verilog的调度机制 (4 regions)
-
Active Region
核心是负责有关组合逻辑的更新:(以下执行顺序随机)
- 进行阻塞赋值、连续赋值、primitive UDP的赋值;
- 计算非阻塞赋值的右侧表达式(RHS, right-hand-side),进程顺滑入NBA区;
- 调用
$display
;
-
inactive Region
执行
#0
延迟下的阻塞赋值。- 理论意义:因为多个进程的阻塞赋值间顺序随机,于是Verilog提供了
#0
,可以至少实现“这条阻塞语句是最后执行”的效果。 - 现实意义:没人用。因为,规范的RTL综合规范中,本就不应该 在多个块中对同个变量进行阻塞赋值。
- 理论意义:因为多个进程的阻塞赋值间顺序随机,于是Verilog提供了
-
NBA Region
-
赋值非阻塞赋值的左侧表达式(LHS, left-hand-side);
若此赋值更改了其他组合逻辑进程的监听信号,则其他进程需要重新被迁入Active Region.
-
-
Postponed Region
- 所有信号确定后的值,准备进入下个time slot.
- 调用
$monitor
与$strobe
;
SV的调度机制
不考虑PLI (9 regions)
-
Preponed Region
是上个time slot内Postponed Region的值,为Observed服务.
-
Observed Region
用于断言检查,计算 (evaluate) 断言内property的事件是否发生、表达式等.
-
Pre-Active Region
- 执行断言,确定断言的pass/fail结果;
- 执行
program
包裹的验证程序中,Active的行为的赋值;
-
Pre-Inactive
- 执行
program
包裹的验证程序中,Inactive的行为的赋值;- 意义:这里Pre-Inactive中的
#0
还是有点用的——TB的fork-join中,在父进程中加入#0,可以实现“让子进程先于父进程执行”。
- 意义:这里Pre-Inactive中的
- 执行
-
Pre-NBA
- 执行
program
包裹的验证程序中,NBA的行为的赋值;
- 执行
-
#1step
for inputSV中,新引入了
#1step
的概念:即有关的输入信号的初值采样,提前1个time step,在上个time slot的postponed region进行采样,等价于在当前time slot的prephoned region进行采样。
注:这个概念,在
clocking
block的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,用
program
、module
两种来进行包裹,都行。其中:
-
UVM是基于
uvm_pkg::run_test()
来启动的,其运行region可自定义。此方法的调用在
program
的initial
块中,还是在module
的initial
中,将直接决定UVM的各phase的代码,是运行在Active区域还是Re-active区域.Cadence推荐在module中调用
run_test()
;Synopsys中推荐在program中调用,从而实现design、dv的区分。 -
“clocking块” 之前没提
不论它定义在
program
还是module
中,它的input都是在preponed region进行sample的、其ouput都是在reactive region进行driven的
-
编程规范
经过上述思考,为避免竞争冒险,也是为了更好的顺利综合,RTL的编程规范是需要记住的。
-
Guideline
-
时序逻辑(sequential logic) ,要用非阻塞赋值(nonblocking assignment);
-
Latches,要用非阻塞赋值;
-
用
always
块实现的组合逻辑,要用阻塞赋值; -
不要在同个
always
块中,混合写时序逻辑与组合逻辑; -
不要再多个
always
块中,对同个变量进行赋值(仅用1个always
完成对1个变量的写操作);注:在
always_comb
、always_latch
、always_ff
中,这已经是强制性要求了. -
不要使用
#0
的任何过程性赋值(<=
、=
都是过程性赋值);Design中不允许,TB中自己根据需要。
-
需要打印非阻塞赋值的变量时,用
$strobe
而不是$display
。-
$display
是在此time slot的active region打印,对应在代码中就是“写在哪,就在哪打印”; -
$write
和$display
一样,但末尾不自动加换行符'\n'
; -
==
$strobe
是在此time slot的prephoned region打印,==不管调用在何时,结果都是一样的; -
$monitor
,只要加一句,它会在要打印的变量一发生变化,就打印一次。但只能存在1条,否则打印1次后只有最后一条monitor才会驻留发挥效果。
-
-
-
其他收集的编程好习惯
-
组合逻辑多用
assign
,少用always(*)
;SV LRM中写着,
assign
赋值将在0时刻生效,但always(*)
没说。
-
4 并发编程与进程同步
fork-join
语法与功效
-
作用
SV可以用fork-join系列语句,启动多个进程进行并发执行;
begin-end
语句块内是单个线程、顺序执行。- 注:并非并行,故
fork-join
的并发和多个initial
、always
块并发、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
:-
作用:终止当前进程的所有子进程,递归所有子孙后代进程
-
语法:
-
disable fork
:终结当前父进程以及其子孙进程;和
fork-join any
等父进程非阻塞的fork联用。 -
disable fork_name
:终结某个子进程(得先定义fork_name); -
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.
-
触发事件
-
立即(阻塞赋值形式)触发此事件:
->事件名
此触发行为,类似边沿,是不可用表达式去观测的,即,无法用
if(posedge clk)
的形式去检测. -
无阻塞赋值形式触发此事件:
->>事件名
我很怀疑是否有这个语法,网上都搜不到…
试了下真的有…确实是无阻塞赋值的形式…虽然感觉有点鸡肋,不如
wait(event.trigger)
-
-
等待事件被触发
-
检测“当下被立即触发”:
@ 事件
缺陷:事件被触发是边沿性质的,一旦“事件等待”行为,没精确在“触发行为”之前,就会永远阻塞。
-
检测“此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; // 默认:浅拷贝
上述区别是:
-
C++里,对象的等于号
=
的赋值默认是 “深拷贝”,对象a和b是分别两个内存空间,但值相等; -
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是指针.
-
对象调用成员函数与成员方法
.
运算符
构造/析构函数
-
构造函数
-
特点
- 用于分配空间;
- 同C++/Java,无返回类型与返回值;
- 对象变量未调用
new()
时,默认为null
. - 构造函数内未初始化的变量,用系统默认值(不是随机数).
-
语法
见上面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.
-
静态方法不能是virtual 方法、不可访问非静态变量、不可用
this
,否则编译报错. -
静态方法 ≠ \neq = 静态生命周期方法.
这个之前章节论述过。主要是
static
位置不同.
class Node; static int num=0; static task fun1(); //静态方法,默认是动态生命周期 endtask task static fun2(); //静态生命周期 endtask endclass
-
类外定义方法
-
类外定义function/task
- 类内声明function/task,加上
extern
关键字以及其他需要的关键字; - 类外定义function/task,不加任何
extern
等关键字、但追加类作用域::
; - 类内类外的声明定义,仅需要且必须形参表一致,virtual等关键字可略.
class A; extern protected virtual function void fun(); endclass function void A::fun(); endfunction
- 类内声明function/task,加上
-
与C++的异同
- C++不用
extern
关键字,直接类名::函数名()
即可; - C++内
extern
不是“类外定义”意思,而是“文件外定义”的意思; - C++内
extern "C"
更是另一个意思:此段代码按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.父类方法;
不允许嵌套super:
super.super.成员变量/方法;
❌ -
注意事点
-
父指针指向儿子,只能访问到父亲内的成员变量与成员函数。
重点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
-
覆写意义与访问特性
-
子类可以覆写父类的同名成员变量、成员方法、constraint;
-
父类的成员变量/成员方法依然存在,只是访问不到;
-
constraint享受多态的效果;
故父类指针指向子类,调用
.randomize()
,生效的子类的constraint.
-
多态
overload/override/overwrite
- SV这仨支持性上与C++/Java区别
- SV不支持子类函数的重载(overload);
- SV支持子类函数的覆写(overwrite/override);
- SV支持动态多态的virtual function.
虚函数
-
作用:略
-
虚函数
作用与语法,同C++/Java.
-
父类定义时,function/task上加
virtual
关键字; -
子类实现时,可省略
virtual
,但加上方便识别,是好习惯.子类即使不加,孙子继承过来的也是虚函数!
-
-
虚函数与普通函数的嵌套/混合调用,触发的核心要义:
-
爹class 间接调用的 virtual 函数,也会触发多态!
只要是按多态的形式调用,几经辗转调用了虚函数,那么触发结果即为多态的运行结果。
-
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函数在满足条件就可行:爹=son1;
$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的意义
-
interface可用于将所有可能要用到的端口信号进行封装;
-
但interface内部可通过
modport
,进一步划分成可重叠的端口子集,按不同module类别进行划分。e.g. 一个interface类,用modport分为master信号集合、slave信号集合。(AXI协议的端口)
-
不同modport,其内的端口信号可以重复出现,但direction注意别矛盾、冲突.
-
-
interface内引入clocking块的意义
接口 interface
-
什么是interface?
Verilog/SV内允许将端口信号作为“通道”进行专门、标准化的“封装”,一方面,能类似class或module一样,便于例化与复用;另一方面,可用于module与SV的OOP进行通信。
- 注意:
-
- interface不是“类”,作用类似,和module、class平级;但毕竟是电路逻辑.
- 能定义module的代码位置,就能定义interface;
- 能定义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、其他端口类型的嵌套;
-
关系介绍
-
SV引入了interface,用于封装端口;
-
引入了virtual interface,是为了SV的OOP;
-
同时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:
- 类外定义interface;
- top_tb内定义interface对象,传入DUT;
- OOP类内定义virtual interface的成员变量,指向top_tb的interface对象;
- 按指针进行赋值即可——秉持着“virtual interface是interface的指针”理念.
-
备注
virtual interface是SV内多出来的语法,是SV为了兼容verilog的interface、又兼容OOP的操作。
端口模式
-
意义
见【6.1 各关系介绍】;一句话总结:信号分组,并规定方向。
-
定义语法
- 端口的方向direction,可挪动到modport内进行规定;
- 不同modport的端口可以有交集;
- 不同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
- input/output skew的可以是ps、ns等单位的仿真时间,也可以是
1step
(全局最小time precision),其意义在**【3.2 SV的调度机制】**中有说明. - default input skew是
1step
:表示输入信号,将在event发生之前采样,微观上是在上一个time slot的postphone region进行采样. - default output skew是
0
:表示输出信号,将在event发生时进行采样,微观上是在当前time slot的re-NBA region进行driven. - 可以设置 input skew #0:表示输入信号,将在event发生时进行采样,微观上是在当前time slot的observed region进行采样.
- input/output skew的可以是ps、ns等单位的仿真时间,也可以是
-
特点
-
interface内可以有多个clocking block,且不同clocking block内的信号可以重复出现。但一次只能指定运行1个clocking block.
-
可以在interface的模块中,指定默认的clocking block.
加了
default
关键字后,##1、##N的时钟就默认是此clocking block了.
-
-
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(变量)
——获取变量类型