终于进入了systemverilog的学习,接续加油!!
基础
语法-数据类型
SV中引入logic类型,和传统verilog中的reg和net的区别和联系:
- verilog作为描述语言,倾向于设计人员懂得寄存器和线网类型变量。利于后端综合工具
- SV侧重验证的语言,对logic对应的硬件并不十分关切,只作为单纯的变量进行赋值操作,这些变量也属于软件环境构建。
注意:
logic不能有多个结构性的驱动,也就是logic类型只能有一个驱动,如果存在多个驱动,那么编译时就会出现错误。 !!!
当然,有些信号你本来就希望它有多个驱动,例如双向总线,这些信号就需要被定义成线网类型,例如wire。
logic相对应的是bit类型,他们均可以构建矢量类型(vector),而他们的区别在于
logic为4值逻辑,即可以表示0,1,x,z
bit为2值逻辑,只有0,1
为什么有了四值了,还搞个2值的?将硬件世界与软件世界分离
按四值逻辑类型和二值逻辑类型划分:
- 四值:integer、logic、reg、net-type(wire,wand。。。)
- 二值:byte、shortint、int、longint、bit
按有符号和无符号划分:
- 有符号类型:byte、shortint、int、longint、integer
- 无符号类型:bit、logic、reg、net-type(逻辑信号)
图中画圈部分,从有符号数到无符号数的转化,即为静态转化,在编译的时候完成检查
对应的动态转换$cast(tgt, src),和静态转化均为显式转化。对应上面的隐式转化
所以在不同数据类型进行操作时应注意变量的:
- 逻辑数值类型
- 符号类型
- 矢量位宽
使用typedef创建新的类型
创建数据自己的数据类型,可以用来声明变量
1 | typedef bit [31:0] uint; // 32比特双状态无符号数 |
可以用来定义一个struct
1 | typedef struct packed { |
类型转换
静态转换:如整形和实数的转换
1 | inti; |
动态转换: $cast
枚举类型
定义常量的另 一种方法是使用参数。 但参数需要对每个数值进行单独的定义,而枚举类型却能够自动为列表中的每个名称分配不同的数值
例如:enum {RED, BLUE, GREEN} color;
创建署名的枚举类型有利于声明更多新变量,尤其是当这些变量被用作子程序参数或模块端口时。 你需要首先创建枚举类型,然后再创建相应的变量。 使用内建的name()函数,你可以得到枚举变量值对应的字符串
1 | //创建代表0,1,2的数据类型 |
还可以指定枚举值
1 | typedef enum {INIT, DECODE= 2, IDLE} fsmtype _e; |
注意:第一个值只能默认缺省或指定为0值
枚举类型的方法
(1) first()返回第一个枚举常量。
(2) last()返回最后一个枚举常量。
(3) next()返回下一个枚举常量。
(4) next (N)返回以后第N个枚举常量。
(5) prev()返回前一个枚举变量。
(6) prev (N)返回以前第N个枚举变量。
操作符
数组
定宽数组
声明:
1 | int ascend[4]; |
合并数组
例如:
bit[2:0] [7:0] array5;
在存储时是连续的:此时内存占用为1个word(32位操作系统)
非合并数组
eg:
bit[7:0] array4[2:0] 或 bit[7:0] array4[3]
在存储时是非连续地址:此时内存占用为4个word
注:合并数组和非合并数组可以混用,例如:可以理解为连续的一维数组4*8bit进行了二维扩展
1 | bit[3:0] [7:0] barray[3]; //合并:3x32比特 |
常量数组
1 | int ascend [ 4] ='{ 0, 1, 2, 3} ; //对4个元素进行初始化 |
数组遍历(for、foreach)
注意多维数组中foreach的使用:
1 | int md[2][3] ='{'{ 0, 1, 2},'{ 3, 4, 5}}; |
打印结果
1 | Initial value: |
动态数组(new)
- new[ ] ——> allocates the storage.
- size() ——> returns the current size of a dynamic array.
- delete() ——> empties the array, resulting in a zero-sized array.
类似于列表,可以动态扩展
只要基本数据类型相同,例如都是int,定宽数组和动态数组之间就可以相互赋值。
在元素数目相同的情况下,可以把动态数组的值复制到定宽数组。
当你把一个定宽数组复制给一个动态数组时,SystemVerilog会调用构造函数new[]来分配空间并复制数值。
1 | int dyn[], d2[]; //声明动态数组 |
队列
队列与链表相似,可以在一个队列中的任何地方增加或删除元素,这类操作在性能上的损失比动态数组小很多,因为动态数组需要分配新的数组并复制所有元素的值。
队列定义:
- 第一种是bounded queue,定义了队列的上界;
- 第二种是unbounded queue,没有定义上界。
1 | bit queue_1[$]; // queue of bits (unbound queue) |
队列方法:
1 | int j = 1; |
关联数组
类似于哈希表,是键值对应关系进行存储
1 | int a_array1[*] ; // associative array of integer (unspecified index) |
数组方法
缩减方法
sum(和),product(积),and(与),or(或),和xor(异或)
1 | bit on[lO]; |
数组定位方法
数组定位方法:min、max、unique
1 |
|
数组定位方法:find
1 | int d[] ='{ 9, 1, 8, 3, 4, 4}, tq[$]; |
数组的排序
1 | int d[]='{9,1,8,3,4,4}; |
变量生命周期
对于automic方法,内部所有声明的变量默认是局部变量,伴随automic方法产生和销毁
对于static方法,内部所有声明的变量默认全局变量
可通过automic或static关键字显示声明变量
static声明的全局变量在编译时被创建加载,在整个运行期间存在
不同位置变量的默认生命周期
- module,program,interface,function,task之外声明的变量,默认为static变量,存在于整个仿真阶段
- module,program,interface之内,function,task之外声明的,默认为static变量,作用域在该块中
- module,program,interface中定义的function和task默认都是static类型
- function和task可以通过显示的姓名automic来改变其内部变量作用域!!
过程语句和子程序
initial块:类似于c的main方法,是启动程序的入口,可以有多个initial块并行,除class外,别的块都可以有
always:硬件行为,只能用于硬件块中!
硬件世界:module,interface;
软件世界:class,programe;
过程语句
SystemVerilog的新增
for循环:continue,break
begin-end:可省略()
1
2
3
4task multiple_lines;
$display ("First line");
$display ("second line");
endtask : multiple_linesreturn 语句:显式返回;function会返回一个与方法名同名的变量,可以直接对该变量进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13typedef int fixed_array5[5];
function fixed_array5 init (int start);
foreach (f[i])
init[i]= i + start; //直接对返回值进行修改
endfunction
fixed_arrayS f5;
initial begin
f5= init(5);
foreach (f5[i])
$display("...") ;
end当然,针对数组的操作函数,最好使用ref进行
function和task
区别:verilog中function不能消耗时间,不能调用task,function必须有返回值
Systemverilog中放宽限制,function可以调用task,但只能在fork-join_none块中
返回值问题:
声明无返回值的方法:void 修饰
忽略有返回值方法的返回值:void’ ($fscanf (file,”%d II, i))
传入参数:
Verilog对参数的处理方式很简单:在子程序的开头把input和inout的值复制给本地变量,在子程序退出时则复制output和inout此的值。 除了标量以外,没有任何把存储器传递给Verilog子程序的办法。
可以在参数列表中指定输入参数(input),输出参数(output),输入输出参数(inout),引用类型(ref),这些参数形式区别于软件的参数
input说明参数流入,在参数未表明传输方向时是默认的
output定义输出方向,方法结束后该参数其实是一个输出值
1
2
3
4
5
6
7
8
9
10
11
12class trans;
endclass
initial begin
trans t;
trans s;
t = new();
s = new();
copyof(t,s);
end
function void copyof(trans t, trans s);
t=s;
endfunction默认为input参数,和软件的形参一样,input类型的参数在方法内部就是一个动态变量,和此时方法内的t和外面的t没有任何关系!!!
1
2
3function void copyof(output trans t, input trans s);
t=s;
endfunction通过output修饰,则对变量的修改可以传出方法
inout:双向端口
ref:类似于inout,传入变量的引用,实际也就是内存地址或者说指针,方法内对变量的修改实际是对内存的修改,是立即发生的!!!
对应变量,可以理解为传入的是一个指针
对应句柄,可以理解为传入的是一个句柄!!
1
2
3function void initial(ref trans t);
t=new();
endfunction
ref专题
在SystemVerilog中,参数的传递方式可以指定为引用而不是复制。 这种ref参数类型比input、output或inout更好用。
为什么要使用ref?
- System Verilog允许不带ref进行数组参数的传递,这时数组会被复制到堆栈区里。这种操作的代价很高,除非是对特别小的数组。
- ref参数的第二个好处是在任务里可以修改变量而且修改结果对调用它的函数随时可见。 当你有若干并发执行的线程时,这可以给你提供一种简单的信息传递方式。
const ref声明参数又有何作用?
编译器会进行检查,以确保数组不被子程序修改,子程序存在修改操作,编译器会报错~
实例:
一旦bus.enable有效,初始化块中的thread2块马上就可以获取来自存储器的数据,而不用等到bus_read任务完成总线上的数据处理后返回,这可能需要若干个时钟周期。 由于参数data是以ref方式传递的,所以只要任务里的data有变化,@data语句就会触发。 如果你把data声明为output,则@data语句就要等到总线处理完成后才能触发。
1 | task bus_ read ( |
参数默认值
实例:
1 | 七ask many (input int a= 1, b= 2, c= 3, d= 4); |
接口interface
通过module端口进行连接:信号名映射的连接方法存在许多缺点:
- 增加了代码输入量
- 信号多时更加复杂
- 增加新的信号,需要对模块端口和顶层连接都进行修改
接口就是解决这种层次化的配置,将所有连线提取出来封装到一起。在使用时对接口进行例化,依此来简化连接
modport
接口中使用了点对点的无信号方向的连接方式。在使用该端口的原始网单里包含了方向信息,编译器依此检查连线错误。
接口中使用modport结构能够将信号分组并指定方向。
如在test和dut的接口中引入monitor模块的接口:
1 | interface arb_if(input clk); |
此时接口例化时需要指明modport
1 | module arb(arb_if.DUT aif); |
激励时序
对应时钟周期级的测试平台,需要在相对于时钟信号的合适时间点驱动和接收同步信号。驱动的太晚或采样的太早,测试平台就会错过一个时钟周期。即使在同一个时间片内,设计和测试平台的时间也会引起竞争状态,即在时钟的有效边沿处进行信号的驱动或采样是很容易出现竞争状态的。
对于测试平台来说:
驱动:时钟有效沿时,dut对连接线上的数据进行锁存和处理,测试平台应将本次驱动延时,以保证数据的保持时间
采样:时钟有效沿时,测试平台想要采到当前连接线上的数据,因此需要在dut驱动数据之前,即提前时钟有效沿采样,否侧新的信号被dut驱动到连接线,本次数据丢失
时钟块
竞争问题实例
在45ns时,clk1在上升沿处打印结果d1,clk2在45ns处采样的d1应该是多少?
在一般手绘仿真波形的时候,都会将d1的数值变化较时钟上升沿延后,但是在仿真软件中无法直接看出
如何打开delta-cycle模式,捕捉时序关系
将detla-cycle展开,看一下实际的时序关系
竞争问题的理解
由于各种可能性,clk与被采样数据之间如果只存在若干个delta-cycle的延迟,假如出现了时钟的复用,那么采样可能会存在问题,也就如上面例子中所示,相同时钟在同样的时刻中得到的是不同的采样结果,因此采样数据中的竞争问题会成为潜在困扰仿真采样准确性的问题。
如上面的例子所示,45ns处clk1和clk2(实际是45+delta-cycle)的打印结果不同,这显然是不符合逻辑的!!!clk2采样到了新更新的数值,这是不对的
不一定是测试环境,就算是在设计环境中,clk2对通过clk1上升沿时驱动的数据进行采样也都是有问题的
为了避免在RTL仿真行为中发生的信号竞争问题,建议通过非阻塞赋值或特定的信号延迟来解决同步的问题。
默认情况下,时钟对于组合电路的驱动会添加一个无限最小时间(delta-cycle)的延迟,而该延迟无法用绝对时间单位衡量,它要比最小时间单位精度还小。
在一个时间片(time-slot)中可以发生很多事情,例如在仿真器中敲命令“run 0”,即是让仿真器运行一个无限最小时间,一个时间片包含无限多个delta-cycle。 s >
ms > ns > ps > fs > delta-cycle
查看delta-cycle可以观察在特定时间点,时序逻辑或组合逻辑中,参与硬件模拟仿真的硬件变量之间的准确时序前后关系。
竞争问题的解决
在驱动时,添加相应的人为延迟。模拟真实的延迟行为(模拟保持时间的驱动要求,即将输入保持一定时间),同时加大clk与变量之间的延迟,以此提高DUT使用信号时的准确性和TB采样信号时的可靠性。
比如说在clk1上升沿到达后,给1个1ps的延时后再做信号变更,此时仿真里面也就不存在delta-cycle的竞争问题
对于一些设计中的没有进行上述延迟处理的信号,也就是采样时依然存在的delta-cycle延迟的信号,我们还可以依靠在采样事件前的某段时刻进行采样,来模拟建立时间的采样要求(即信号在时钟到来前已经建立一段时间),确保采样的可靠性。
时钟块怎么用
实例:
- 硬件世界和软件世界的连接可以通过灵活的interface实现,也可以通过modport来进一步限定信号传输的方向,避免端口的连接错误。
- 也可以在接口中声明clocking(时序块)和采样的时钟信号,用来做信号的同步和采样。
- clocking块基于时钟周期对信号进行驱动或采样的方式,使得testbench不再苦恼于如何准确及时地对信号驱动或者采样,消除了信号竞争的问题。
- clocking块不但可以定义在interface中,也可以定义在module和program中。
- clocking中列举的信号不是自己定义的,而是应该由interface或其它声明clocking的模块定义的。
- clocking在声明完名字之后,应该伴随着定义默认的采样事件,即“default input/output event”。如果没有定义,则会默认地在clocking采样事件前的1step对输入进行采样,在采样事件后的# 0 对输出进行驱动。
结论
在我看来interface中对于时间块的定义,实际上就是软件系统(tb)对硬件系统(dut)的一个模仿,可以把tb的时钟理解为clk1,dut的时钟理解为clk2,此时在clk1的上升沿,通过驱动的延时,就给了dut相对于clk2的一个保持时间;
另外tb采样的过程和驱动过程是独立起来看的,如果把dut的时钟理解为clk1,tb的时钟理解为clk2,则此时tb的采样结果和dut的输出结果
为了避免可能的采样竞争问题,应该在验证环境的驱动环节就添加固定延迟,使得在仿真波形中更容易体现出时钟与被驱动信号之间的时序前后关系,同时也便于DUT的准确处理和TB的准确采样。
如果TB在采样从DUT送出的数据,在时钟与被驱动信号之间在存在delta-cycle时,应该考虑时钟采样沿的更早时间段去模拟建立时间要求采样,这种方法也可以避免由于delta-cycle问题带来的采样竞争问题。
当我们把clocking运用到interface中,用来声明各个接口与时钟的采样和驱动关系后,可以大大提高数据驱动和采样的准确性,从根本上消除采样竞争的可能性。
类和包的使用
面向对象
包的使用
包的意义
1
2SV语言提供了一种在多个module、interface和program之中共享parameter、data、type、task、function、class等的方法,即利用package的方式来实现。通常将不同模块的类定义归整到不同的package中。
package的好处是将一簇相关的类组织在了单一的命名空间namespace下,使得分属于不同模块验证环境的类来自于不同的package,这样可以通过package来解决类的归属问题。包的定义
1
2
3
4
5
6
7
8
9
10
11
12package regs_pkg;
'include "stimulator.sv"
'include "monitor.sv"
'include "chker.sv"
'include "env.sv"
endpackage
package arb_pkg;
'include "stimulator.sv"
'include "monitor.sv"
'include "chker.sv"
'include "env.sv"
endpackage
注意:
两个package中都定义了4个与模块验证相关的类,而这两个package中同名的类,它们的内容是不相同的,实现的也是不同的功能。将这些重名的类归属到不同的package中编译,不会发生重名的编译冲突,因为package是将命名空间分隔开来的。使用时需要注明使用哪一个package中的类。
1 | module mcdf_tb; |
包与库的区分
尽管regs_pkg和arb_pkg中都存在着一个名字为monitor的类,我们可以在引用类名的时候通过域名索引::操作符的方式来显式支出所引用的monitor类具体来自于哪一个package,这样能很好地通过不同名的package来管理同名的类。package这个容器可以对类名做一个隔离的作用。
package更多的意义在于将软件(类、类型、方法等)封装在不同的命名空间中,一次来与全局的命名空间进行隔离。package需要额外定义,容纳各种数据、方法、类。
library是编译的产物,在没有介绍软件之前,硬件(module、interface、program)都会编译到库中,如果不指定编译库的话,会被编译进入默认的库中。从容纳的类型来看,库既可以容纳硬件类型,也可以容纳软件类型,例如类和方法,也包括package。包的命名规则
在创建package的时候,已经在指定包名称的时候隐含地指定了包的默认路径,即包文件所在的路径。如果有其他要被包含在包内的文件在默认路径之外,需要在编译包的时候加上额外指定的搜寻路径选项“+incdir+PATH”。
如果遵循package的命名习惯,不但要求定义的package名称独一无二,其内部定义的类应该也尽可能的独一无二。
如果不同package中定义的类名也不相同时,在顶层的引用也可以通过 import pkg_name::*的形式,来表示在module mcdf_tb中引用的类,如果在当前域中没有定义的话,会搜寻regs_pkg和arb_pkg中定义的类,又由于它们各自包含的类名不相同,因此不用担心搜寻中会遇到同名类冲突问题。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package regs_pkg;
'include "regs_stm.sv"
'include "regs_mon.sv"
'include "regs_chk.sv"
'include "regs_env.sv"
endpackage
package arb_pkg;
'include "arb_stm.sv"
'include "arb_mon.sv"
'include "arb_chk.sv"
'include "arb_env.sv"
endpackage
module mcdf_tb;
import regs_pkg::*;
import arb_pkg::*;
regs_mon mon1 = new();
arb_mon mon2 = new();
endmodule使用包的注意事项
在包中可以定义类、静态方法和静态变量。
如果将类封装在某一个包中,那么它就不应该在其它地方编译,这么做的好处在于之后对类的引用更加方便。
包是类的归宿,类是包的子民。
一个完整模块的验证环境组件类,应该由一个定义的模块包来封装。
使用’include的关键字完成类再包中的封装,要注意编译的前后顺序来放置各个’include的类文件。
编译一个包的背后实际是将各个类文件“平铺”在包中,按照顺序完成包和各个类的有序编译。
使用类可以通过import完成包中所有类或者某一个类的导入,使得新的环境可以识别出该类,否则类会躺在包这个盒子里不被外部识别。
类型转换
1.静态转换
在需要转换的表达式前面加单引号
这种方式不会对转换值做检查。如果转换失败,我们也无从得知
2.动态转换
动态转换需要调用系统函数$cast(tgt,src)做转换,把src转换成tgt的类型。
比如声明了父类句柄的子类src,转化为子类句柄的tgt
==son tgt = (son)src;
类句柄的向下转换
父类句柄转换为子类句柄时,需要使用$cast()函数进行转换,否则会出现编译错误;
$cast()会检查句柄所指向的对象的类型,而不是检查句柄本身;
子类句柄赋值给父类句柄(也就是将子类句柄拷贝成父类句柄),编译器认为合法;
父类句柄拷贝给子类对象,需要使用$cast检查句柄所指向的对象类型,一旦源对象跟目的对象是同一类型,就可以从父类句柄拷贝子类对象的地址给子类句柄。
【注意】:
当$cast作为任务来使用时(直接调用,不需要返回值时),如果转换失败会给出一个错误报告
当$cast作为函数使用时(需要返回值),转换失败返回0,不给出错误报告
3.显式和隐式转换
动态转换和静态转换都需要操作符或者系统函数的介入,称为显式转换
不需要转换的操作,称为隐式转换;
如:右侧是4位的矢量,左侧是5位的矢量,赋值时会隐式的做位扩展,然后再赋值
虚函数(virtual)
看一下”virtual”关键字有哪些使用场景:
- 主要应用场景在virtual class,virtual interface 以及 virtual task/function。
- OOP三大特性(封装,继承,多态)中的 多态 在SystemVerilog中一般通过 “virtual” 关键字实现。
1. virtual interface
- 在interface定义时,如果不使用关键字 “virtual” 那么在多次调用该接口时,在其中的一个实例中对接口中某一信号的修改会影响其他实例接口;如果使用了 “virtual” 关键字,那么每个实例是独立的。
- 习惯上在声明interface时均添加 “virtual”关键字。
2. virtual task/function
子类在实现方法的继承时可以发生方法的重写,此时子类只有通过调用super.fun()才能访问父类方法
- 多态的元素之一,没有声明virtual的方法,父类句柄=子类对象时,父类句柄无法访问到子类,只能访问到父类本身的function()。一旦声明virtual,则该方法实现动态绑定,父类句柄则可以访问到子类的function()!!!
- virtual方法只需要定义在最顶层的类中,其所有子类及子类的子类中的重写方法都实现了动态绑定,通过
最顶层的类的句柄=子类实例
都可以实现调用子类的该方法
3. virtual class
即抽象类
虚类一般用来定义类的格式,、类的成员、类的参数等,虚类不能被实例化,只能被扩展(重载)后实例化,用于在项目中定义一些标准的类。
虚类中的方法通常使用关键字 “ pure virtual “ 纯虚方法。同时OOP规定,只要class中存在一个没有被实现的pure function,就不允许例化这个class
pure virtual function(纯虚方法):没有实体的方法原型,相当于一个声明,只能在抽象类中定义。
对象拷贝
当需要赋值一个对象,以防止对象的方法修改原始对象的值,或者在一个发生器中保留约束时,可以对对象做拷贝
1. 浅拷贝
只拷贝成员变量:String,int,句柄等
1 | packet p = p1;packet p = p2;p1 = new();p2 = new() p1; |
此时p1和p2是两个对象,区别于直接句柄赋值:p1=p2
2. 自定义拷贝
拷贝函数和新对象生成函数分开写:
- 父类和子类成员均可以完成拷贝,拷贝方法声明为virtual,遵循只考虑该类的域成员的原则,父类的成员拷贝调用父类的拷贝函数
- copy_data()需要注意句柄的类型转换,保证转换后的句柄可以访问类的成员变量
实验进化2
要从实验1完成哪些进化:
interface的提取
test和dut的连接关系(initiator):
initiator的目的更像是创建虚拟的硬件模型,通过该module的实例化创建具有驱动能力的数据发送模块(给data就能按照需要进行发送,task chnl_write())
实验1:通过module进行例化
实验2:通过interface实例化再传入module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67//实验1的initiator_nodule
module chnl_initiator(
//创建模型的第一部分,创建需要的输入输出接口
input clk,
input rstn,
output logic [31:0] ch_data,
output logic ch_valid,
input ch_ready,
input [ 5:0] ch_margin
);
string name;
//给个名字作为标识
function void set_name(string s);
name = s;
endfunction
//赋予该模型驱动能力,即给MCDT写入的能力
task chnl_write(input logic[31:0] data);
// USER TODO
// drive valid data
// ...
@(posedge clk);
ch_valid <= 1;
ch_data <= data;
@(negedge clk);
wait(ch_ready === 'b1);
$display("%t channel initial [%s] sent data %x", $time, name, data);
chnl_idle();
endtask
//赋予该模型等待的能力
task chnl_idle();
// USER TODO
// drive idle data
// ...
@(posedge clk);
ch_valid <= 0;
ch_data <= 0;
endtask
endmodule
//实例化为3个channel
chnl_initiator chnl0_init(
.clk (clk),
.rstn (rstn),
.ch_data (ch0_data),
.ch_valid (ch0_valid),
.ch_ready (ch0_ready),
.ch_margin(ch0_margin)
);
chnl_initiator chnl1_init(
.clk (clk),
.rstn (rstn),
.ch_data (ch1_data),
.ch_valid (ch1_valid),
.ch_ready (ch1_ready),
.ch_margin(ch1_margin)
);
chnl_initiator chnl2_init(
.clk (clk),
.rstn (rstn),
.ch_data (ch2_data),
.ch_valid (ch2_valid),
.ch_ready (ch2_ready),
.ch_margin(ch2_margin)
);1
//实验2的initiator_interface(添加了clock块,对采样和驱动数据进行过滤)interface chnl_intf(input clk, input rstn); logic [31:0] ch_data; logic ch_valid; logic ch_ready; logic [ 5:0] ch_margin; clocking drv_ck @(posedge clk); default input #1ns output #1ns; output ch_data, ch_valid; input ch_ready, ch_margin; endclockingendinterface//用interface把接口摘出去,再建立初始化模型module chnl_initiator(chnl_intf intf); string name; int idle_cycles = 1; //增加模块功能,动态调整idle(等待)周期 function automatic void set_idle_cycles(int n); idle_cycles = n; endfunction function automatic void set_name(string s); name = s; endfunction //添加赋予驱动的功能 task automatic chnl_write(input logic[31:0] data); @(posedge intf.clk); //时钟上升时设置驱动 intf.drv_ck.ch_valid <= 1; intf.drv_ck.ch_data <= data; @(negedge intf.clk); //等待半拍,接收信号reday为高电平则 wait(intf.ch_ready === 'b1); $display("%t channel initiator [%s] sent data %x", $time, name, data); //这一步就决定了idle的执行次数 repeat(idle_cycles) chnl_idle(); endtask //添加空闲等待功能 task automatic chnl_idle(); @(posedge intf.clk); intf.drv_ck.ch_valid <= 0; intf.drv_ck.ch_data <= 0; endtaskendmodule
generator的提取
驱动数据data的生成:
实验1:在test功能module中进行创建
1
initial begin chnl0_arr = new[100]; chnl1_arr = new[100]; chnl2_arr = new[100]; foreach(chnl0_arr[i]) begin chnl0_arr[i] = 'h00C0_00000 + i; chnl1_arr[i] = 'h00C1_00000 + i; chnl2_arr[i] = 'h00C2_00000 + i; endend
实验2:抽象出generator模块,通过interface实例化再传入module,此时结构明显更清晰了
1
module chnl_generator; int chnl_arr[$]; int num; int id; function automatic void initialize(int n); id = n; num = 0; endfunction function automatic int get_data(); int data; data = 'h00C0_0000 + (id<<16) + num; num++; chnl_arr.push_back(data); return data; endfunctionendmodule
将interface和generator的module按照需要进行实例化
并对实例化结果赋想要的初值
可以看到因为把接口提取出去,module的实例化不需要再对
1 | chnl_intf chnl0_if(.*); chnl_intf chnl1_if(.*); chnl_intf chnl2_if(.*); chnl_initiator chnl0_init(chnl0_if); chnl_initiator chnl1_init(chnl1_if); chnl_initiator chnl2_init(chnl2_if); chnl_generator chnl0_gen(); chnl_generator chnl1_gen(); chnl_generator chnl2_gen(); initial begin // verification component initializationi chnl0_gen.initialize(0); chnl1_gen.initialize(1); chnl2_gen.initialize(2); chnl0_init.set_name("chnl0_init"); chnl1_init.set_name("chnl1_init"); chnl2_init.set_name("chnl2_init"); chnl0_init.set_idle_cycles(0); chnl1_init.set_idle_cycles(0); chnl2_init.set_idle_cycles(0); end |
test的module
将generator的值装配到initiator中,然后执行
随机
1. 随机约束和分布
1 | class Packet; |
rand:以扑克牌为例,每次随机都是抽回一张牌,然后再放回去
randc:每次随机抽回的牌都不放回
随机值必须是二值逻辑,4值逻辑中的x,z随机不出来
assert:==if
p.randomize():生成随机数,返回是否成功(0则为失败)
约束
constraint:约束求解器
父类约束被子类继承
dist:权重分布关键词(主要涉及:=表示赋值和:/表示平均概念的理解)
1 | rand int src, dst; |
inside表示约束中变量是某一值的集合,除非还存在其他约束,否则随机变量在集合中取值概率相同。
1 | rand int c; //随机变量 |
使用$指定最大值和最小值。
1 | rand bit [6:0] b; //0 <= b <= 127rand |
对于条件约束,可以通过->或者if-else让约束表达式在特定时刻有效
1 | class BusOp; ... |
对于约束而言,是双向约束,是一种声明性质的代码,并行的,不是自上而下的,所有约束表达式同时有效
一旦发生约束冲突,则会同时失败。
1 | rand logic [15:0] r, s, t;constraint c_bidir { r < t; s == r; t < 30; s > 25;} |
2. 约束块控制
在一个对象中,可能会有很多的约束块,在实际使用时,我们希望一些随机块起作用,一些随机块不起作用,这就需要控制开关来控制约束块。在system verilog中提供了constrian_mode()函数来打开或关闭约束,同时也提供了随机变量的控制函数rand_mode()来控制变量的随机性,当随机变量的随机属性被关闭时,它就不在是一个随机变量,randomize()函数不会对其赋值。
constraint_mode()
可以被类调用,也可以被约束块调用,让该作用域下的约束打开或关闭
1 | class Packet; |
rand_mode()
当rand_mode()作为task调用时,控制随机变量的随机属性开和关;
和constraint_mode的使用方法相同,只是一个是直接屏蔽constraint,一个是不对随机变量进行随机赋值
1 | class Packet; |
randomize().with可以在外部增加约束,每次生成随机的时候只在当前有效,多次randomize的结果不会互相影响!!!
问:t.randomize().wiht({addr inside [200,300]; date inside [0,10];});
这次随机化能不能实现?
答,可以,soft关键字表示在发生约束冲突时,soft修饰的约束失效!!
3. 随机函数
对于上面提到的randomize()函数的注意点:
- 可以传入参数,rand声明的变量没传入就不会随机化,没被rand声明的变量传入也会被随机化
- 无论是否传入参数,约束条件都生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Rising;
byte low; //未被随机约束变量
rand byte med, hi; //随机化的变量,8位有符号值
constraint up
{ low < med; med < hi;}
endclass
initial begin
Rising r;
r = new();
r.randomize(); // 随机化hi,但是不改变low
r.randomize(med); // 只随机化med
r.randomize(low); // 只随机化low
end问:上述代码中,例化了r之后,先调用r.randomize(low),那么low,med和hi的组合值可能是下面哪一组?
1
(A)low = -1,med=0, hi=0(B)low = -1,med=1, hi=2(C)low = 报错,med=0, hi=0(D)low = 报错,med=null, hi=报错
答:c,因为约束条件不满足,随机化失败
4. 数组的约束
可以加约束的两个层面:size()和content
动态数组分别可以对其长度和内容做随机化处理。此外,还可以通过在约束中结合数组的其它方法sum(), product(), and(), or()和xor()。
1 | class good_sum5; |
randc并不能让数组的每个元素随机化时不重复
可采用以下方法:
1.数组的每个元素遍历
```verilog
class UniqueSlow;rand bit [7:0] ua[64]; constraint c { foreach (ua[i]) //对数组中每一个元素操作 foreach (ua[j]) if(i != j) //除了元素自己 ua[i] != ua[j]; //和其它元素比较 }
endclass
1
2
3
4
5
- 2.创建randc变量,在`pre_randomize()`方法中给数组赋值
- ```verilog
class randc8; randc bit [7:0] val; //随机变量的值的范围为0~255,每一次randmize的256次值都不相同endclass class LittleUniqueArray; bit [7:0] ua [64]; //定义一个含有64个元素的数组 function void pre_randomize(); randc8 rc8; rc8 = new(); foreach (ua[i]) begin assert(rc8.randomize()); //从256个元素中随机化64次,每次随机化的值都不同 ua[i] = rc8.val; //之后将随机化后的值赋值给数组 end endfunctionendclass
案例1:
1 | class packet; rand bit[3:0] da[]; //rand修饰动态数组 constraint int da { da.size() inside {[3:5]}; //约束数组的个数为3-5 foreach(da[i]) da[i] <= da[i+1]; }endclasspackat p;initial begin p = new(); p.randomize() with {da.size() inside {3,5};}; //约束da数组的个数或者3或者5end |
本题中考查动态数组的范围,边界范围da[4] < da[5]中超出了范围。
随机化句柄数组
这里需要特别注意的:随机函数会随机每个句柄的对象
案例2:
1 | parameter MAX_SIZE = 10; |
问:ra.randomize() with {array.size == 2}的合法求解是多少呢?
答:array[0].value = 1,array[1].value = 1
因为句柄对象没有rand修饰,随意randomize()进不到对象里面
问:RandArray类中bit[1:0] value加上rand修饰会怎么样?
答:此时随机化方法会向句柄对象中继续寻找,然后给value变量进行随机化
5. 随机控制
生成随机序列
(1)产生随机事务序列的另一种方法就是使用SV的 randsequence结构。这对于随机安排组织原子(atomic)测试序列很有帮助。
1 | initial begin |
(2)randcase来建立随机决策树,但是它带来的 问题就是没有变量 可以提供追踪 调试。
1 | initial begin int len; randcase 1:len = $urandom_range(0,2); //10%:0,1,or2 8:len = $urandom_range(3,5); //80%:3,4,or5 1:len = $urandom_range(6,7); //10%:6or7 endcase $dsiplay("len = %0d", len);end |
总结:
(1)randsequence和randcase都是针对轻量级的随机控制应用。而我们可以 通过定义随机类 取代上述随机控制并且由于类的继承 性使得在后期维护代码的时候更加方便。
(2)randsequence的相关功能我们在协调激励组件和测试用例的时候可能会用到。
(3)randcase对应着随机约束中dist权重约束if-else条件约束组合。
线程
核心:
1 | fork … join |
三者区别:执行顺序
- fork-join:执行完该并行块才继续执行
- fork-join_none:先执行后面的,这个块和后面的语句属于并行
- fork-join_any:最快的一句执行完,就和后面的语句并行
一旦initial块执行结束,线程中未完成的语句也结束
线程控制
如何确保fork-join和fork-none在退出前执行完呢
在end之前调用wait fork
停止多个线程:disable fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14initial begin
check_trans(tr0); //线程0
//创建一个线程来限制disable fork的作用范围
fork //线程1
begin
check_trans(tr1); //线程2
fork //线程3
check_tans(tr2); //线程4
join
//停止线程1-4,单独保留线程0
#(TIME_OUT/2) disable fork
end
join
enddisable线程1,以及其衍生出来的线程
停止多次 调用的任务
如果你给某一个任务或者线程指明标号,那么当这个线程被调用多次后,如果通过disable去禁止这个线程标号,所有衍生的同名线程都将被禁止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17task wait_for_time_out (int id);
if(id == 0)
fork
begin
#2;
$display("@%0t: disable wait_for_time_out", $time);
disable wait_for_time_out;
end
join_none
fork: just_a_little
begin
$display("@%0t: %m: %0d enteriing thread", $time,id);
#TIME_OUT;
$display("@%0t; %m: %0d done", $time,id);
end
join_none
endtask可以看到该task占用的线程只是点火启动
1
2
3
4
5
6initial begin
wait_for_time_out(0); //Spawn thread 0
wait_for_time_out(1); //Spawn thread 1
wait_for_time_out(2); //Spawn thread 2
#(TIME_OUT*2) $display("@%0t: All done", $time);
end若果执行上述语句,则在执行完id=0的task后,所用其他由该task开辟的线程被全部截止
线程通信
event
event关键字:声明即实例
看一下线程间的相互阻塞
1 | event e1,e2; |
e1和e2在同一时刻被触发,但是由于delta cycle的时间 差是的两个 初始化块可能无法等到e1或者e2
因为存在date-cycle,e1和e2总是有一个先触发,触发e1时等待e2,触发e2时等待e1。打印结果为:
1 | @0:1:before trigger @0:2:before trigger @0:1:after trigger |
可以看到@e1导致的阻塞没有办法解决
triggered()
此时就可以对阻塞方式进行一下修改:
更加安全的方式使用event的方法triggered(),将边沿触发改为电平触发
1 | event e1,e2; |
此时只有e1,e2执行就代表电平触发已经完成,不会再造成阻塞
@和triggered()的使用时机
1 | module road; |
为了保证发动方法在前,通过bit位控制move()方法阻塞
此时只要执行了launch(),move()方法随便执行
此时可以直接换成event和triggered
1 | class car; event e_start; task launch(); -> e_start; $display("car is launched"); endtask task move(); wait(e_start.triggered()); $display("car is moving"); endtasktask drive(); fork this.launch(); this.move(); joinendtaskendclass |
那如果再增加一个显示速度的方法,要求每次增加速度都会显示,此时就要使用边沿触发@,因为电平触发只会发生一次,后续wait(triggered())
的返回值会一直为1,阻塞检测激励将不复存在
1 | lass car; |
旗语(semaphore)
也就是锁
1 | program automatic test(bus_ifc.TB.bus); |
semaphore 钥匙数是傻瓜式的增减:
new() :不传参就没钥匙
get():调用就少一把
put():调用就多一把
上面这种调用方式可能出现的问题,钥匙越来越多
1 | class car; |
如果没注意还了一把钥匙,就会导致钥匙数在意料之外变多,如何避免这种情况
1 | class carkeep; |
增加一个用户队列,此时用户没有资格获取钥匙,由keep_car()统一分发!!!用户只可以还钥匙,并将该用户删除
这就保证了最多只能有1把钥匙!!!
- 调用还钥匙方法:user是锁死的,只有同一个user才可以还,还完别的user无法访问,钥匙数最多只能为1
- 调用取钥匙方法:一旦队列有人,并且有钥匙,马上分配!!此时key为0,队列进入等待,直到取钥匙的user还钥匙
mailbox
线程同步只需要event来保证,只有线程之间传递消息时使用mailbox
- try_get(),try_put(),try_peek()是非阻塞
- 限定mailbox可以传入的参数类型:
mailbox #(int)
- new() :表示变长,new(N)表示定长
1 | program automatic bounded; |
1 | program automatic bounded; |
总结
覆盖率
代码覆盖率:工具会自动搜集已经编写好的代码,常见的代码覆盖率如下:
- 行覆盖率(line coverage):记录程序的各行代码被执行的情况。
- 条件覆盖率(condition coverage):记录各个条件中的逻辑操作数被覆盖的情况。
- 跳转覆盖率(toggle coverage):记录单bit信号变量的值为0/1跳转情况,如从0到1,或者从1到0的跳转。
- 分支覆盖率(branch coverage):又称路径覆盖率(path coverage),指在if,case,for,forever,while等语句中各个分支的执行情况。
- 状态机覆盖率(FSM coverage):用来记录状态机的各种状态被进入的次数以及状态之间的跳转情况
功能覆盖率:是一种用户定义的度量,主要是衡量设计所实现的各项功能,是否按预想的行为执行,即是否符合设计说明书的功能点要求,功能覆盖率主要有两种如下所示:
- 面向数据的覆盖率(Data-oriented Coverage)-对已进行的数据组合检查.我们可以通过编写覆盖组(coverage groups)、覆盖点(coverage points)和交叉覆盖(cross coverage)获得面向数据的覆盖率.
- 面向控制的覆盖率(Control-oriented Coverage)-检查行为序列(sequences of behaviors)是否已经发生.通过编写SVA来获得断言覆盖率(assertion coverage).
需要指出的是: 代码覆盖率达到要求并不意味着功能覆盖率也达到要求,二者无必然的联系。而为了保证验证的完备性,在收集覆盖率时,要求代码覆盖率和功能覆盖率同时达到要求。
功能覆盖率建模
功能覆盖率主要关注设计的输入、输出和内部状态
,通常以如下方式描述信号的采样要求;
对于输入,它检测数据端的输入和命令组合类型,以及控制信号与数据传输的组合情况。
对于输出,它检测是否有完整的数据传输类别,以及各种情况的反馈时序。
对于内部设计,需要检查的信号与验证计划中需要覆盖的功能点相对应。通过对信号的单一覆盖、交叉覆盖或时序覆盖来检查功能是否被触发,以及执行是否正确。
覆盖组——covergroup
使用覆盖组结构(covergroup)定义覆盖模型,覆盖组结构(covergroup construct)是一种用户自定义的结构类型,一旦被定义就可以创建多个实例就像类(class)一样,也是通过new()来创建实例的。覆盖组可以定义在module、program、interface以及class中。
每一个覆盖组(covergroup)都必须明确一下内容:
1 | covergroup cov_grp @(posedge clk); //用时钟明确了覆盖点的采样时间,上升沿采样覆盖点,也可省略clk,在收集覆盖率时在根据情况注明 |
上述例子用时钟明确了覆盖点的采样时间,上升沿采样覆盖点,也可省略clk,在收集覆盖率时在根据情况注明,如下示例:
1 | covergroup cov_grp; |
上面的例子通过内建的sample()方法来触发覆盖点的采样.
覆盖组中允许带形式参数,外部在引用覆盖组时可以通过传递参数,从而对该覆盖组进行复用。
1 | logic [7:0] address; |
覆盖点——coverpoint
一个覆盖组可以包含多个覆盖点,每个覆盖点有一组显式bins值,bins值可由用户自己定义,每个bins值与采样的变量或者变量的转换有关
。一个覆盖点可以是一个整型变量也可以是一个整型表达式。覆盖点为整形表达式的示例如下:注意覆盖点表达式写法。
1 | class Transaction(); |
当进行仿真后,len16的覆盖点覆盖率最高可达100%,而覆盖点len32的覆盖率最高只能达到23/32=71.87%。由于总的bins数量为32个,而实际最多只能产生产生len_0,len_1,len2,…,len22共23个bins,所以覆盖率永远不可能达到100%
如果要使覆盖点len32达到100%的覆盖率,可以手动添加自定义bins,代码如下:
1 | covergroup Cov; |
盖点元素——隐式bin与显式bins
隐式或自动bin:覆盖点变量,其取值范围内的每一个值都会有一个对应的bin,这种称为自动或隐式的bin。例如,对于一个位宽为nbit的覆盖点变量,若不指定bin个数,2^n个bin将会由系统自动创建,需要注意的是自动创建bin的最大数目由auto_bin_max内置参数决定,默认值64。
1 | program automatic test(busifc.TB ifc); //接口例化 |
显式bins:”bins”关键字被用来显示定义一个变量的bin,用户自定义bin可以增加覆盖的准确度,它属于功能覆盖率的一个衡量单位。在每次采样结束时,生成的数据库中会包含采样后的所有bins,显示其收集到的具体覆盖率值。最终的覆盖率等于采样的bins值除以总的bins值。
针对某一变量,我们关心的可能只是某些区域的值或者跳转点,因此我们可以在显示定义的bins中指定一组数值(如3,5,6)或者跳转序列(如3->5->6)。 显示定义bins时,可通过关键字default将未分配到的数值进行分配。
1 | covergroup Cov; |
覆盖点的状态跳转——=> 与 ?通配符
除了在bins中定义数值,还可以定义数值之间的跳转,操作符(=>),如下所示:
1 | bit[2:0] v; |
除操作符外,还可使用关键词wildcard和通配符?来表示状态和状态跳转
1 |
|
覆盖点之间的交叉覆盖率——cross
交叉覆盖是在覆盖点或变量之间指定的,必须先指定覆盖点,然后通过关键字cross定义覆盖点之间的交叉覆盖。
1 | //通过覆盖点来定义交叉覆盖 |
由于上面每个覆盖点都有16个bin,所以它们的交叉覆盖总共有256(16*16)个交叉积(cross product),也就对应256个bin。
代码code——约束与覆盖率的运用
void sample() : Triggers the sampling of covergroup 触发覆盖组的采样
real get_coverage() : Calculate coverage number, return value will be 0 to 100 返回覆盖组覆盖率
real get_inst_coverage() :Calculate coverage number for given instance, return value will be 0 to 100 返回覆盖组实例的覆盖率
void set_inst_name(string) :Set name of the instance with given string 设置实例名
void start() :Start collecting coverage 开启覆盖率收集
void stop() :Stop collecting coverage 结束收集覆盖率
如何提高覆盖率
1 | module test(); |
通过修改随机化次数——提高覆盖率(覆盖点变量取值范围小)
或者说改变激励的生成和测试次数
通过添加约束constraint、自定义bins——提高覆盖率(覆盖点变量取值范围大)
在自定义bins,而不添加约束constraint
的情况下:
1 | module cov_demo(); |
在自定义bins,并缩小约束constraint
范围的情况下:
1 | module cov_demo(); |
通过权重dist——调整hit次数分布
将代码中的constraint约束调整为权重dist处理后,其各个bins的hit次数分布更加均匀,如下所示:
1 | constraint data_c1{ |
断言
https://www.cnblogs.com/pcc-uvm/p/14771685.html
断言的理解:断言更像是对信号的一个要求,不满足就不行!
断言被用来:
(1)检查特定条件或事件序列的发生;
(2)提供功能覆盖(functional coverage),使用 cover 关键字 ;
存在两种断言:立即断言和并发断言;
1. 立即断言=~if
立即断言在当前仿真时间检查条件,类似于if…else语句,但是断言带有控制.立即断言必须放在过程块定义中.
当断言带了else 语句,则失败后进入else;没带else语句,失败后直接执行$error
失败后的错误信息:
1 | “test.sv”, 7: top.t1.a1: started at 55ns failed at 55ns |
下面展示一个简单的断言:
断言格式如下:
1 | assert(condition) |
如果断言失败,并且没有指定任何其他else子句,则默认情况下,该工具将调用$error
2. 定制断言
1 | a1: assert(bus.cb.grant==2'b01) |
4个在断言中输出消息的函数:
- $info
- $warning
- $error
- $fatal
3. 并发断言 Concurrent Assertions(edge采样)
与时序相关,常用
并发断言可以认为是一个连续运行的模块,为整个仿真过程检查信号,所以需要在并发断言内指定一个采样时钟。
需要指出的是:
1 | 并发断言只有在时钟沿才会执行; |
1 | c_assert: assert property(@(posedge clk) not(a && b)); |
实例:request信号除了在复位期间,其他任何时候都不能是X或者Z
1 | interface arc_if(input bit clk); |
通用格式:
1 | c_assert: assert property(@(posedge clk) not(a && b)); |
4. iff的使用
屏蔽不定态:
当信号被断言时,如果信号是未复位的不定态,不管怎么断言,都会报告:“断言失败”,为了在不定态不报告问题,在断言时可以屏蔽。
如:
@(posedge clk) (q == $past(d))
,当未复位时报错,屏蔽方法是将该句改写为:
@(posedge clk) disable iff (!rst_n) (q == $past(d))
//rst是低电平有效