目录
- Q:logic和wire、reg的区别
- Q:数组、队列的常用方法
- Q:program 和 module 的区别
- Q:为什么program中不允许使用always块?
- Q:子程序参数
- Q:局部数据存储
- Q:fork...join创建线程
- Q:线程间通信
- 事件——实现线程的同步
- 旗语——实现对同一资源的访问控制
- 信箱——实现线程间信息的传递
- Q:DPI传入类型?
Q:logic和wire、reg的区别
Verilog中 wire 和 reg 的区别:
- wire表示导线结构,reg表示存储结构。
- wire使用assign赋值,reg赋值定义在always、initial、task或function代码块中。
- wire赋值综合成组合逻辑,reg可能综合成时序逻辑,也可能综合成组合逻辑。
SystemVerilog的 logic 类型
- logic是reg类型的改进,它既可被过程赋值也能被连续赋值,编译器可自动推断logic是reg还是wire。
- 唯一的限制是logic只允许一个输入,不能有多个结构性的驱动,例如inout类型端口不能定义为logic。所以单驱动时用logic,多驱动时用wire。
- 不过这个限制也带来了一个好处,由于大部分电路结构本就是单驱动,如果误接了多个驱动,使用logic在编译时会报错,帮助发现bug。
Q:数组、队列的常用方法
常量数组:使用一个单引号加大括号来初始化数组
int array[4] = '{1,2,3,4};
foreach循环:在foreach循环中,只需要指定数组名并在其后的方括号中给出索引变量,SV便会自动遍历数组中的元素,索引变量将自动声明,并只在循环内有效。多维数组的循环使用foreach(a[i,j])
。
合并数组:既可以用作数组,也可以当作单独的数据,声明合并数组时,数组大小和合并的元素个数作为数据类型的一部分必须在变量名前面指定,数组大小的定义格式必须是[msb : lsb]。如下例所示
bit[3:0][7:0] bytes;
是一个有四个字节的合并数组,引用bytes则为全部的32bit,引用bytes[3]则为最高字节,引用bytes[3][7]
则为最高bit位。
动态数组:声明时使用空的下标 [ ],数组的宽度不在编译时给出,而是在程序运行时指定。数组在最开始时是空的,需要调用new[]操作符来分配空间,同时在方括号中传递数组宽度,可以把数组名传给new[]构造符,把已有数组的值复制到新数组里
array = new[20](array);
array最终指向了一个有20个元素的数组,里面包含原有数组的值。
动态数组有一些内建的子程序,例如delete()和size()。
队列:声明时使用美元符号为下标[$],可以在队列的任何地方增加和删除元素,这类操作在性能上的损失比动态数组小的多,因为动态数组需要分配新的数组,并复制所有的元素值。
注意不要对队列使用构造函数new[]
。
队列的常量不需要使用单引号
q[$] = {1,2}; //队列的常量不需要使用单引号'
q.insert(1,0); //在第1位插入0{0,1,2,}
q.delete(1); //删除第一位{1,2}
//下面的操作执行速度很快
q.push_front(6); //最前面插入6 {6,1,2}
j=q.pop_back; //取出最后的数 j=2 {6,1}
q.push_back(8); //在最后插入8 {6,1,2,8}
j=q.pop_front; //取出最前面的数 j=6 {1,2,8}
foreach(q[i])
$display("%0d",q[i]); //打印整个队列
q.delete(); //等价于命令q={};删除整个队列
在队头或队尾存取数据非常方便,在队列中间增加或删除元素需要对已加存在的数据进行搬移以便腾出空间
关联数组:采用在方括号中放置数据类型的形式来进行声明。
bit[63:0] assoc[bit[63:0]],
数组缩减方法:基本的数组缩减方法就是把一个数组缩减成一个值。最常用的方法就是求和sum,除此之外还有product(乘)and(与)or(或)xor(异或)等。
在进行数组压缩的时候,应该特别重要的一点需要注意,那就是位宽的问题。缺省情况下,如果你把一个单比特数组的所有元素相加,其和也是单比特的,但如果你使用32比特的表达式,或者把结果保存在32比特的变量里,或者和一个32比特的变量进行比较,或者使用适当的with表达式,SV都会在数组求和的过程中使用32比特位宽。
$display("a.sum=%0d",a.sum); //a.sum是单比特无符号的数 打印出单比特和 a.sum=1
$display("a.sum=%0d",a.sum+32'd0); //a.sum是32比特数 打印出32比特和 a.sum=5
数组定位方法:包括min()、max()、unique()、find(),这些方法的返回值通常是一个队列。
重点说一下find,find还有find_first,find_last用来寻找元素,find_index,find_first_index,find_last_index用来寻找索引。
通常会使用到条件语句with,item被称为重复参数,它代表了数组中一个单独的元素,item是缺省的名字,你也可以指定别的名字,只要在数组方法的参数列表中列出来就可以
tq=d.find_first with (item= =4); //等价
tq=d.find_first() with (item= =4);
tq=d.find_first(item) with (item= =4);
tq=d.find_first(x) with (x==4);
当数组缩减方法与条件语句with结合使用时,如下例,如果with的表达式是条件表达式,则sum的结果是条件表达式为真的次数;如果with的表达式是具体的数或者元素时,sum的结果是对其求和
d[]='{9,1,8,3,4,4};
count=d.sum with (item>7); // 2: {9,8}
total=d.sum with ((item>7)*item); //17=9+8
count=d.sum with (item==4); // 2: {4,4}
数组排序方法
int d[]='{9,1,8,3,4,4};
d.reverse(); //反向'{4,4,3,8,1,9}
d.sort(); //正序{1,3,4,4,8,9}
d.rsort(); //逆序'{9,8,4,4,3,1}
d.shuffle(); //随机'{9,4,3,8,1,4}
Q:program 和 module 的区别
- 相似之处:
1、和module相同,program也可以定义0个或多个输入、输出、双向端口。
2、一个program块内部可以包含0个或多个initial块、generate块、specparam语句、连续赋值语句、并发断言、timeunit声明。
3、在program块中数据类型、数据声明、函数和任务的定义均与module块类似。
4、一个设计中可以包含多个program块,这些program块既可以通过端口交互,也可以相互独立,这一点与module块也是相似的。 - 不同之处:
1、program块内部不能包含任何其他的always块、用户自定义原语( UDP)、module块、接口(interface)、或者program块。
2、module块中可以定义program块,但program块中却不能定义module块
3、program块可以调用其他module块或者program块中定义的函数或任务,但是module块却不能调用其他program块中定义的任务或函数。
Q:为什么program中不允许使用always块?
program中可以使用initial块,但是不能使用always块。
在一个设计中,一个always块可能从仿真的开始就会在每一个时钟的上升沿触发执行,但是一个测试平台的执行过程是经过初始化、驱动和响应设计行为等步骤后结束仿真的,在这里一个连续的always模块不能正常工作。
当program中最后一个initial块结束的时候,仿真实际也默认结束了,就像执行了$finish
一样,如果加入一个always块,它将永远不会结束,这样就不得不明确的调用$exit
来发出程序块结束的信号。
如果你确实需要一个always块,你可以使用"initial forever"来完成相同的事情。
Q:子程序参数
Verilog任务中要求对一些参数进行两次声明,一次是方向声明,一次是类型声明。
task mytask;
output[31:0] x;
reg[31:0] x;
input y;
...
endtask
在SV中,可以采用简明的C语言风格,
task mytask(output logic[31:0] x,
input logic y);
...
endtask
高级的参数类型
Q:局部数据存储
静态存储
Verilog中,所有的对象都是静态分配的,也就是说子程序参数和局部变量是被存放在固定位置的,而不像其他语言那样存放在堆栈区,如果你试图在程序里的多个地方调用同一个任务,由于任务里的局部变量会使用共享的静态存储区,所以不同的线程之间会窜用这些局部变量,导致错误。
自动存储
可以指定任务,函数和模块使用自动存储,从而迫使仿真器使用堆栈区存储局部数据。
在SystemVerilog中,模块(module)和program块中的子程序缺省情况下仍然使用静态存储,如果要使用自动存储,则必须在程序语句中加入automatic关键字。
Q:fork…join创建线程
begin…end中的语句以顺序方式执行,而fork…join中的语句则以并发方式执行,后者的不足是必须等待fork…join内的所有语句都执行完后才能继续后续的处理,SV中引入了两种新的创建线程的方法fork…join_none和fork…join_any
fork…join_none块在调度其内语句时,父线程继续执行
fork…join_any块对块内语句进行调度,当第一个语句完成后,父线程才继续执行,其他停顿的线程也得以继续
- 当你使用循环来创建线程时,如果在进入下一轮循环之前没有保存变量值,便会碰到一个常见却又难以被发现的漏洞。如果线程使用的是静态存储,那么每个线程将会共享相同的变量,这样会导致后面的调用覆盖前面的调用的值。通常在声明变量时使用自动变量(automatic)来避免这个问题。
如果代码所处的程序或者模块是自动存储的,那么变量声明时可以不使用关键词automatic。 - 当程序块内的initial块到达end语句时,仿真将在该时刻终止,不管子线程是否还有未执行的语句。可以在end前面使用wait fork语句来等待所有子线程结束。
- 停止单个线程:通过disable一个标签可以精确的指定需要停止的块
- 停止多个线程:disable fork可以停止从当前线程衍生出来的所有子线程,包括当前线程。
- 禁止被多次调用的任务:如果在某个任务内部禁止该任务,会停止所有由该任务启动的线程。如果该任务已被多个线程调用,禁止其中的一个将导致它们全部被禁止。
Q:线程间通信
数据交换和控制的同步被称为线程间的通信(IPC)。在System Verilog中,可使用事件、旗语和信箱来完成。
事件——实现线程的同步
-
->
用来触发事件; -
@
或者wait
用来等待事件触发。
那么@
和wait
有什么区别呢?
- @操作符是边沿敏感的,它总是阻塞着,等待着事件的变化。当一个线程在一个事件上发生阻塞的同时,正好另一个线程触发了这个事件,则竞争就出现了。如果触发线程先于阻塞线程,则触发无效(触发是一个零宽度的脉冲)。
- wait是电平敏感的,SystemVerilog引入了triggered()函数,可用于查询某个事件是否已经被触发,包括在当前时刻。线程可以等待这个函数的结果,而不用在@操作符上阻塞。
如果在循环中使用wait(e1.triggered())
,一定要确保在下次等待之前程序的时间可以向前推进,否则你的代码将进入一个零时延循环,原因是wait会在单个事件触发器上反复执行。这种情况可以考虑使用@来等待。
旗语——实现对同一资源的访问控制
旗语有以下操作。
- 使用new方法可以创建一个带单个或多个钥匙的旗语
- 使用get可以获取一个或多个钥匙
- 而put则可以返回一个或多个钥匙
- 如果你试图获取一个旗语而希望不被阻塞,可以使用try_get(),返回1表示有足够多的钥匙,返回0则表示钥匙不够。
semaphore sem; //创建一个旗语
sem = new(1); //分配一把钥匙
sem.get(1); //获取一把钥匙
sem.put(1); //返回一把钥匙
try_get(); //希望获得钥匙而不被阻塞,返回1表示有足够多的钥匙,返回0则表示钥匙不够。
- 可以将旗语看成是一把钥匙,谁拥有钥匙谁就对资源具有使用的权利。旗语可以被视为一个互斥体,用于实现对同一资源的访问控制。
- 一个线程如果请求钥匙而得不到的话,就会一直阻塞。多个阻塞的线程会以先进先出(FIFO)的方式进行排队。
- 线程在请求钥匙时,会先依次取,若不够取则先跳过,让后面够取的取,等待钥匙够了时,再让前面跳过的线程取。先入先出的规则在这里会被忽略掉。
例如有4个钥匙,四个线程分别取2,3,2,1个,则线程1和3先执行,执行完后,线程2和4再执行。 - 你返回的钥匙可以比你取出来的多
信箱——实现线程间信息的传递
- SystemVerilog中的信箱,解决两个线程间的传递。从硬件角度理解,可以看成一个具有源端和收端的先进先出FIFO。源端把数据放进信箱,收端把数据取出信箱。
- 信箱是一种对象,必须调用new函数进行实例化。例化时有一个可选的参数size,用以限制信箱中的条目。如果size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。
- 使用put任务可以把数据放入信箱,使用get可以移除数据。如果信箱为满,则put会阻塞;而信箱为空,则get会阻塞。peek任务可以获取对信箱里数据的拷贝而不移除它。
使用信箱的一个典型漏洞就是在循环外面构造一个对象,然后使用循环对对象进行随机化并把它们放入信箱,因为实际上只有一个对象,它被一次又一次地随机化,信箱内存放的所有句柄都指向同一个对象。
Q:DPI传入类型?