注:平台需机构邮箱注册,还支持Perl、python等脚本语言以及UVM验证。
1.数据类型
- VerilogHDL中有2种变量类型:
wire
和reg
,这两种变量是4值类型的(即有四种状态)。 - SystemVerilog在此基础上拓展了一种变量类型:
logic
类型,该变量类型可以取代wire
型变量和reg
型变量。但需要注意的是,logic
型的变量不能够有多个结构性的驱动,所以在对inout
端口变量进行声明的时候,需要使用wire
变量类型。 - 其余的变量类型如下表所示:
- 定宽数组。Verilog要求在数组的声明中必须给出明确的上下界,例如:
int array[8]
。需要注意的是,当越界访问数组的时候,若数据是四状态类型,那么将返回X
值;若数据是双状态类型,那么将直接返回0
。 - 数组的初始化。数组在定义的时候即可进行初始化,例如:
int array[8] = '{1,2,3,4,,default=7}
,又例如:int array[8] = '{8{7}}
。 - 合并数组。对于某些数据类型,用户可能希望它能够作为数组访问,也希望有时它可以作为一个整体访问,这就是合并数组的意义所在,例如:
bit [3:0][7:0] array
,这个合并数组既可以当作4个byte
分别访问,也可以作为一个32bit
的数据进行整体访问。 - 动态数组。定宽数组在使用之前必须规定好数组的长度,有时这样的操作带有局限性,而动态数组在定义的时候不需要指明数组的长度,在使用的时候再对数组长度进行
new[]
操作即可。例如,定义一个动态数组:int array[]
,在使用的时候,对array
进行new[]
操作并指明数组长度即可:array = new[5]
。 - 队列。队列是SystemVerilog新引进的数据类型,它结合了链表和数组的优点,可以随时增删数组里的元素,也可以做到快速的数组操作。在对队列进行声明的时候,需要使用
[$]
。 - 关联数组。类似于Python里的字典。关联数组的声明有些复杂,例如:
bit [63:0] assoc[bit[63:0]]
。另外一个简单的栗子:
1 program automatic assoc_test();
2 int assoc[string];
3
4 initial begin
5 assoc["super"] = 1;
6 assoc["special"] = 2;
7 assoc["offer"] = 3;
8
9 foreach(assoc[i]) begin
10 $display("assoc[%s] = %0d", i, assoc[i]);
11 end
12 end
13 endprogram
14 //result:assoc[offer]=3, assoc[special]=2, assoc[super]=1
- 数组的方法。数组拥有各类方便用户的方法,但是这些方法不能够使用在合并数组上。
- 流操作符。(bit-stream)表示方式为{>>{}} 和 {<<{}}。前者会把数据块按照从左到右的形式流(stream),后者则是从右到左。流操作符常用于把其后面的数据打包成一个比特流。
实际项目中,经常需要打包(pack)和解包(unpack)数据。其实这些操做可以通过流操作符简单的实现。
1 module test;
2 function void pack_array_int(const ref bit[7:0] array[4], output int a);
3 a = {<<byte{array}};
4 endfunction
5
6 bit[7:0] array[4] = '{ 8'h8C, 8'h00, 8'hA4, 8'hFF };
7 int pack_result;
8
9 initial begin
10 pack_array_int(array, pack_result);
11
12 $display("The result is 0x%h", pack_result);
13 end
14 endmodule
注意,此处的 const ref是为了避免子程序对数组的改变。ref是对数组直接的引用,避免子程序需要拷贝数组,从而释放仿真器的压力。
另一个实例是,项目中会涉及的大小端(big endian,little endian)的转换,且需要将数据解包。
1 module test;
2 function void unpack(bit[127:0] big_endian_data, output bit[63:0] little_endian_array[2]);
3 bit[127:0] temp;
4 temp = {<<byte{big_endian_data}};
5 for(int i=0; i<2; i++) begin
6 little_endian_array[i] = temp[63:0];
7 temp = temp >> 64;
8 end
9 endfunction
10
11 bit[127:0] big_endian_data = 128'h1122334455667788_aabbccddeeff1122;
12 bit[63:0] little_endian_array[2];
13
14 initial begin
15 unpack(big_endian_data, little_endian_array);
16
17 foreach(little_endian_array[i]) begin
18 $display("The result is 0x%h",little_endian_array[i]);
19 end
20
21 end
22 endmodule
该实例实现了将一个16byte的大端数据(big endian)转换为小端数据后,unpack成为2个64bit的数组:output bit[63:0] result[2]。
注:使用bit而不是byte是因为byte是有符号数,可能会引起产生负数。所以实际验证中一般使用bit。
- 枚举类型。枚举类型用
enum
和一个中括号{}
定义,常用于状态机中的状态定义,常用的枚举类型方法如下所示。
2.过程语句和子程序
- 任务和函数之间有着很大的差别,最大的差别在于:任务内部可以消耗仿真时间,而函数是表达式,不能够消耗仿真时间,例如:不能有
wait
或者posedge clk
这样的语句存在。并且对于函数来说,必须要有返回值,而且返回值必须要立即使用,不带返回值的函数需要用void
进行声明。 - 参数的方向:
input
、output
、inout
、ref
,ref
类型是参考类型,直接传递参数的指针,所以在对ref
型的数据进行修改时,是直接对原始数据进行修改,类似于C语言的“实参”。 - VerilogHDL中的对象是静态分配的,不像其它的编程语言那样将对象放在堆栈里面,所以在多个地方同时调用同一个子程序(函数或者任务),可能会使用到同一个局部变量。解决这一问题的方法在于声明任务或者函数的时候,添加
automatic
自动存储关键词,这样会迫使仿真器使用堆栈进行仿真。 - 仿真时间值。SystemVerilog提供多种指明仿真时间的方法:
timescale 1ns/1ps
、timeunit
、timeprecision
、$timeformat
(时间标度,小数点后的时间精度,后缀字母(单位),显示数值的最小宽度)。使用timescale
可以对全局的文件进行统一的仿真时间设置,而后面的方法可以单独对某个模块进行设置。
3.连接设计和测试平台
- SystemVerilog使用接口作为验证平台的通信管道,接口包含了连接、同步、多个模块之间的通信,是连接测试平台与DUT的关键桥梁。一个接口的示例如下所示:
interface my_if(input logic clk, input logic rst_n);
logic valid, ready;
endinterface
- 在接口
interface
中,可以使用modport
关键词定义一组信号并规定其方向,这类似于将interface
中的某些信号用线捆好。在使用modport
信号组的时候,需要指明信号组的名称。
interface my_if(input logic clk, input logic rst_n);
logic valid, ready;
modport DRIVER(input clk, rst_n, ready, output ready);
modport MONITOR(input clk, rst_n, valid, ready);
endinterface
//using interface.MONITOR
module my_dut(my_if.MONITOR vif);
...
endmodule
- 接口
inerface
可以使用时钟块clock blocking
对接口内部的信号进行同步采样和驱动,这样可以保证测试平台在正确时间点的信号交互。另外,在时钟块里可以用default
语句指定时钟偏移,但是在默认情况下输入信号仅在设计执行之前被采样。
interface my_if(input logic clk);
logic valid, rst_b, ready;
clocking drv_cb@(posedge clk);
input ready;
output valid;
endclocking
endinterface
- SystemVerilog是以事件调度来进行仿真的语言,它将程序运行时发生的事情按一定的顺序进行调度并执行,在一段特定时间内会发生所有的事件调度,这段时间叫做time slot(时间片),某时刻仿真进入一个time slot,执行完其中的所有事件后,便进入下一个time slot。仿真时间(而非现实时间)是按照time slot向前推进的,如果程序中某一段代码反复调度回该time slot(无延时的死循环),仿真会因为无法进入下一个time slot而卡死,此时虽然现实时间在流逝,但仿真时间不再向前推进了。
- 对于时间片而言,主要有以下几个执行区域,其中
Acitive
、Inactive
、NBA
区域属于Active Region
,主要执行的是module(即RTL)中的代码;而Reacitive
、Re-inactive
、Re-NBA
区域属于Reactive Region
,主要执行的是program(即testbench)中的代码:
- SystemVerilog中提供了断言,用户可以使用断言对时序进行判断。断言分为两类:立即断言和并发断言。
4.面向对象编程基础
- 面向对象编程的三大特点:封装、继承、多态。
- 在SystemVerilog中要分清楚句柄和对象的区别。
class transaction;
logic data[100];
function new();
foreach(data[i]) begin
data[i] = 2*i;
end
endfunction
endclass
transaction tr;//声明句柄
tr = new();//为对象开辟地址空间
- OOP中的静态变量和全局变量。每个类的内部都有自己的成员变量,当这些成员变量没有被
static
关键词进行声明的时候,这些变量是局部的,而被static
关键词修饰的成员变量则是共享的。除此之外,静态函数只能读写静态变量,不能够读写非静态变量,例如下面的例子中静态函数只能读写count的值,而不能对id的值进行访问。
class transaction;
static int count=0;
int id;
function new();
id = count++;
endfunction
endclass
transaction tr1, tr2;
tr1=new();//id=0,count=1
tr2=new();//id=1,count=2
this
指代类一级的变量。当使用一个变量名的时候,SystemVerilog首先会在当前作用域进行寻找,假若找不到则会在上一级作用域进行寻找,这样循环往复直到找到为止。而若在某一局部作用域中想使用class
一级的成员变量,可以使用this
进行指代。- 对象的复制。对象有2种不同的复制方式:简易复制和深层次复制。简易复制方式就像是对原对象的简单粗暴赋值,不会考虑对象内部的类;深层次复制可以解决这个问题,但用户需要自己维护复制函数。
1 class transaction;
2 static int count=0;
3 int id;
4 int addr, data;
5 statistics stats;//另一个类,内有成员变量int starttime
6
7 function new();
8 stats = new();
9 id = count++;
10 endfunction
11 endclass
12
13 //shallow copy
14 transaction src, dst;
15 initial begin
16 src = new();
17 src.stats.starttime = 30;
18 dst = new src;
19 dst.stats.starttime = 90;//同时src.stats.starttime也变成了90
20 end
21
22 //deep copy
23 class transaction;
24 static int count=0;
25 int id;
26 int addr, data;
27 statistics stats;//另一个类,内有成员变量int starttime
28
29 function new();
30 stats = new();
31 id = count++;
32 endfunction
33
34 function transaction copy();
35 copy = new();
36 copy.addr = addr;
37 copy.data = data;
38 copy.stats = stats.copy();//类statistics内部也需要维护一个复制函数
39 id = count++;
40 endfunction
41 endclass
- 类的公有和私有。在SystemVerilog中,所有的成员都是公有的,若在声明时加
local
和protected
关键词则可以将其变为私有成员或保护成员。网页、游戏等代码需要的是长时间的稳定性,为此需要将所用到的类都声明为私有类型,但是测试平台不同,有时我们不仅需要注入正确的激励,还需要注入错误的激励,这时候公有的类可以使我们的操作更加方便。
5.随机化
- 带有约束的随机是SV的灵魂,我们不可能指望用一个接着一个的定向激励去覆盖所有的DUT功能点,也不可能完全让激励放任自由地随机化,最好的设想就是利用带有约束的随机产生某一个方向上的随机。下面的代码展示了一个简单的带有随机的类:
1 class packet;
2 rand bit [7:0] data;
3 randc int count;
4 constraint count_cons {count>10;count<15;}//约束是声明性语句,需要用到{}
5 endclass
6
7 //创建一个对象并对其进行随机化
8 packet pkt;
9 initial begin
10 pkt = new();
11 assert(pkt.randomize())
12 else $fatal(0, "packet::randomize failed!");
13 end
- 随机的权重。使用
dist
关键词可以改变值的随机概率,即让某些值更加频繁地出现,或者更加不容易被随机出来。通常情况下,dist
关键词的后面会跟着一个值及其相应的权重,中间用:=
或者:/
分开。
rand int src, dst;
constraint src_dst_cons{
src dist{0:=40,[1:3]:=60};
//src=0,weight=40/220
//src=1,weight=60/220
//src=2,weight=60/220
//src=3,weight=60/220
dst dist{0:/40,[1:3]:/60};
//dst=0,weight=40/100
//dst=1,weight=20/100
//dst=2,weight=20/100
//dst=3,weight=20/100
}
- 随机中的集合和
inside
操作符。可以使用inside
操作符产生一个集合,使得随机变量在这个集合中随机取值。另外可以使用$
运算符代指最小值或最大值。
1 rand int a, low, high;
2 const int array[5]='{1,3,5,7,9};
3 rand bit [6:0] b;
4 rand bit [5:0] c;
5 rand int d;
6 constraint a_cons {a inside [low:high];}//low<=a<=high
7 //constraint a_cons {!(a inside [low:high]);}//a<low||a>high
8 constraint b_cons {b inside {[$,4],[20:$]};}//(0<=b<=4)||(20<=b<=127)
9 constraint c_cons {c inside {[$,2],[20,$]};}//(0<=c<=2)||(20<=c<=63)
10 constraint d_cons {d inside array;}//d==1||d==3||d==5||d==7
- 条件约束。使用条件约束,可以使得约束条件在某些情况下才起作用,条件约束可以使用
->
符号,也可以利用关键词if()...else()
。
1 class transaction;
2 bit workmode;
3 int crc;
4 constraint cons_crc {if(workmode) crc==0; else crc=$random();}
5 endclass
6
7 class transaction;
8 bit workmode;
9 int crc;
10 constraint cons_crc {workmode -> crc==0;}
11 endclass
- 解的概率。SystemVerilog并不保证随机约束器能够给出精确的解,但是用户可以控制概率的分布。另外需要注意的是,SystemVerilog中随机的约束是双向的。
1 //x有2种解:0和1,y有4种解:0、1、2、3,每一种随机结果的概率是一样的
2 class imp;
3 rand bit x;
4 rand bit [1:0] y;
5 endclass
6
7 //共有5种解:x=0,y=0;x=1,y=0;x=1,y=1;x=1,y=2;x=1,y=3,但是每种解的概率不一定相同
8 class imp;
9 rand bit x;
10 rand bit [1:0] y;
11 constraint cons_xy {(~x)->(y==0);}//当x=0的时候,y只能为0
12 endclass
13
14 //共有3种解:x=1,y=1;x=1,y=2;x=1,y=3,且每种解的概率一样,均为1/3
15 class imp;
16 rand bit x;
17 rand bit [1:0] y;
18 constraint cons_xy {y>0;(~x)->(y==0);}//当x=0的时候,y只能为0
19 endclass
20
21 //共有5种解:x=0,y=0(1/2);x=1,y=0(1/8);x=1,y=1(1/8);x=1,y=2(1/8);x=1,y=3(1/8),但是每种解的概率不一定相同
22 class imp;
23 rand bit x;
24 rand bit [1:0] y;
25 constraint cons_xy {solve x before y;(~x)->(y==0);}//当x=0的时候,y只能为0
26 endclass
27
28 //共有5种解:x=0,y=0(1/8);x=1,y=0(1/8);x=1,y=1(1/4);x=1,y=2(1/4);x=1,y=3(1/4),但是每种解的概率不一定相同
29 class imp;
30 rand bit x;
31 rand bit [1:0] y;
32 constraint cons_xy {solve y before x;(~x)->(y==0);}//当x=0的时候,y只能为0
33 endclass
- 对于
class
内的约束模块constraint
,可以通过constraint_mode()
开启或者关闭约束模块;而对于class
内的成员变量,可以通过rand_mode()
开启或者关闭随机。 - SystemVerilog允许使用
randomize() with
来增加额外的约束,with
后面的{}
里,SystemVerilog使用的是类的作用域。 - SystemVerilog提供了许多常用的系统函数,具体的函数列在下表:
6.线程的使用
- SystemVerilog提供了创建多线程的方法,即
fork...join
方法。
//启动完之后,所有线程结束才继续执行后面的程序
fork
...
join
//线程启动之后,只要有一个线程结束就继续执行后面的程序
fork
...
join_any
//线程启动之后,继续执行后面的程序
fork
...
join_none
- 线程中的自动变量。当使用循环来创建线程的时候,如果在进入下一个循环之前没有保存当前的变量,那么该变量会在下一个线程当中被覆盖,这是一个很难发现的漏洞。
1 //最终结果会显示3个3
2 program no_auto;
3 initial begin
4 for(int i=0; i<3; i++) begin
5 fork
6 $write(i);
7 join_none
8 end
9 #0 $display("\n");
10 end
11 endprogram
12
13 //最终结果:1 2 3
14 program no_auto;
15 initial begin
16 for(int i=0; i<3; i++) begin
17 fork
18 automatic int j = i;
19 $write(j);
20 join_none
21 end
22 #0 $display("\n");
23 end
24 endprogram
25
26 //最终结果:1 2 3
27 program automatic no_auto;
28 initial begin
29 for(int i=0; i<3; i++) begin
30 fork
31 int j = i;
32 $write(j);
33 join_none
34 end
35 #0 $display("\n");
36 end
37 endprogram
- SystemVerilog中可以利用事件
event
进行线程间的通信,->
符号表示触发一个事件,@
符号表示等待一个事件的触发,该符号是一个边沿敏感型符号,是阻塞的;另外,可以通过wait(event.triggered())
这样的电平敏感型表达式替换@
这样的边沿敏感型符号,但这个表达式是非阻塞的,在使用的时候需要保证仿真时间的向前推进。
//仿真卡死
forever begin
wait(handshake.triggered());//non-blocking
$display("received next event!");
process_next_item_function()//a function defined by user
end
- SystemVerilog提供旗语实现多个线程对同一个资源的访问控制。例如,DUT中有多个线程请求同一个资源,这就可以使用旗语来完成控制权的仲裁。旗语通过
semaphore
关键词来声明,旗语有3个基本的操作:new
、get
和put
。 - SystemVerilog提供信箱供线程之间进行信息交换,可以将信箱看作是一个连接了收端和发端的FIFO,当信箱为空的时候,读取信息(
mailbox.get()
)会被阻塞;当信箱为满的时候,放入信息(mailbox.put()
)这个动作会发生阻塞。信箱用mailbox
关键词进行声明,mailbox
是一个类,所以在使用之前需要new()
操作,在创建对象的时候可以传入参数,这个参数是可容纳数据的上限,如果不传或者传入数据为0,则视为信箱的容量无限大。
1 program automatic synchronized;
2 class producer;
3 mailbox mbx;
4
5 function new(mailbox mbx);
6 this.mbx = mbx;
7 endfunction
8
9 task run();
10 for(int i=1; i<4; i++) begin
11 $display("producer:before put information(%d)", i);
12 mbx.put(i);
13 end
14 endtask
15 endclass
16
17 class consumer;
18 mailbox mbx;
19
20 function new(mailbox mbx);
21 this.mbx = mbx;
22 endfunction
23
24 task run();
25 repeat(3) begin
26 int i;
27 mbx.get(i)
28 $display("consumer:after get information(d%)", i);
29 end
30 endtask
31 endclass
32
33 producer p;
34 comsumer c;
35 initial begin
36 maibox mbx;
37 mbx = new();
38 p = new(mbx);
39 c = new(mbx);
40 fork
41 p.run();
42 c.run();
43 join
44 end
45 endprogram
46
47 //result
48 producer:before put information(1)
49 producer:before put information(2)
50 consumer:after get information(1)
51 consumer:after get information(2)
52 producer:before put information(3)
53 consumer:after get information(3)
View Code
- 假若在用信箱
mailbox
的同时使用事件event
的话,就可以实现线程间的完全同步了。
1 program automatic synchronized;
2 class producer;
3 mailbox mbx;
4 event handshake;
5
6 function new(mailbox mbx, event handshake);
7 this.mbx = mbx;
8 this.handshake = handshake;
9 endfunction
10
11 task run();
12 for(int i=1; i<4; i++) begin
13 $display("producer:before put information(%d)", i);
14 mbx.put(i);
15 @handshake;//等待事件handshake的触发
16 end
17 endtask
18 endclass
19
20 class consumer;
21 mailbox mbx;
22 event handshake;
23
24 function new(mailbox mbx, event handshake);
25 this.mbx = mbx;
26 this.handshake = handshake;
27 endfunction
28
29 task run();
30 repeat(3) begin
31 int i;
32 mbx.get(i)
33 $display("consumer:after get information(d%)", i);
34 ->handshake;//触发事件handshake
35 end
36 endtask
37 endclass
38
39 producer p;
40 comsumer c;
41 initial begin
42 maibox mbx;
43 mbx = new();
44 event handshake;
45 p = new(mbx, handshake);
46 c = new(mbx, handshake);
47 fork
48 p.run();
49 c.run();
50 join
51 end
52 endprogram
53
54 //producer和consumer两个线程之间实现了完全同步
55 producer:before put information(1)
56 consumer:after get information(1)
57 producer:before put information(2)
58 consumer:after get information(2)
59 producer:before put information(3)
60 consumer:after get information(3)
View Code
7.面向对象编程的高级技巧
- OOP中类的变量称为属性(property),类中的任务或者函数称为方法(method),子程序的原型(prototype)是指明了参数列表和返回类型的第一行。
- 类在使用
extends
关键词进行继承的时候,假若构造函数eg.function new(int value)
中有参数传递,那么扩展类中的构造函数的第一行也必须调用基类的构造函数super.new(value)
。 - 可以将一个派生类的句柄赋值给一个基类的句柄,并且不需要任何特殊的代码;但是相反的,假若将一个扩展类的句柄赋值给一个基类句柄时,最好使用
$cast()
进行类型匹配,类型不匹配时会返回0值。
1 //将子类的句柄赋值给父类
2 program automatic test;
3 class father;
4 function void call;
5 $display("***I am father!!!***");
6 endfunction
7 endclass
8
9 class son extends father;
10 function new();
11 super.new();
12 endfunction
13
14 function void call;
15 $display("***I am son!!!***");
16 endfunction
17 endclass
18
19 father fa;
20 son so;
21 initial begin
22 so = new();
23 so.call;//输出是:I am son!!!。假若在function的前面加上virtual关键字,那么SV会根据对象的类型进行方法的调用
24 fa = so;
25 fa.call;//输出是:I am father!!!。假若在function的前面加上virtual关键字,那么输出为:I am son!!!
26 end
27 endprogram
View Code
1 //将父类的句柄赋值给子类
2 program automatic test;
3 class father;
4 function void call;
5 $display("***I am father!!!***");
6 endfunction
7 endclass
8
9 class son extends father;
10 function new();
11 super.new();
12 endfunction
13
14 function void call;
15 $display("***I am son!!!***");
16 endfunction
17 endclass
18
19 father fa;
20 son so;
21 initial begin
22 fa = new();
23 fa.call;//输出应为:I am father!!!
24 so = fa;//此行为禁止,编译不通过
25 so.call;
26 end
27 endprogram
View Code
- 虚方法。虚方法需要在最前面添加
virtual
关键词进行声明,类在调用虚方法的时候,会根据对象的类型进行调用而不是句柄的类型。假若没有对方法进行virtual
虚方法的声明,则SystemVerilog会根据句柄的类型而非对象的类型调用方法。 - 抽象类和纯虚方法。SystemVerilog提供了2种构造方法来创建一个可以共享的基类:第一种是抽象类,即可以被扩展但是不能被直接实例化的类,使用
virtual
关键词进行修饰;第二种是纯虚方法,这是一种没有实体的方法原型,用关键词pure
修饰,并且纯虚方法只能够在抽象类中定义。
1 virtual class base_transaction;
2 static int count = 1;
3 int id;
4 function new();
5 id = count++;
6 endfunction
7 pure virtual function bit compare(input base_transaction to);
8 pure virtual function base_transaction copy(input base_transaction to=null);
9 pure virtual function void display(input string prefix="");
10 endclass
View Code
- 回调。回调功能其实是在类的方法中预设了一些虚方法,这些虚方法的内部是没有代码实现的,所以在使用回调函数的时候需要对虚方法进行重写。SystemVerilog中的自建方法
randomize()
即是一种回调方法,其前后还包括了pre_randomize()
和post_randomize()
。
1 //代码来自数字IC小站:SystemVerilog中的callback(回调)
2 class abc_transactor;
3 virtual task pre_send(); endtask
4 virtual task post_send(); endtask
5
6 task xyz();
7 // Some code here
8 this.pre_send();
9 // Some more code here
10 this.post_send();
11 // And some more code here
12 endtask : xyz
13 endclass : abc_transactor
14
15 class my_abc_transactor extend abc_transactor;
16 virtual task pre_send();
17 ... // This function is implemented here
18 endtask
19
20 virtual task post_send();
21 ... // This function is implemented here
22 endtask
23 ...
24 endclass : my_abc_transactor
View Code