目录
3. import pkg::*是将pkg的所有都导入进来吗?
8. fork join/fork join_any/fork join_none的用法差异
11.在TB中使用interface和clocking blocking的好处
Class代表软件世界,Module代表硬件世界,Interface是连接软件世界和硬件世界的桥梁,但是更加偏硬件一些。
Package也是软件世界的,相当于是SV中用于定义namescope的东西。Interface和Module(由于含有硬件世界的成分)不能包含在(属于软件世界的)Package中。
类中的成员(数据、函数、任务)缺省地都是automatic类型。如果需要static类型成员的话,需要用static显式地指定。Module中的所有变量、函数、任务都缺省为static。如果需要automatic类型成员的话,需要用automatic显式地指定。
Module的例化都是静态的。Class的对象的创建是动态的。这里所谓的静态是指在编译后就已经生成了。而动态则是指要到仿真实际开始后执行new()创建。
Module一经例化,模块的实例自始自终一直存在。而SV中关于对象生命期管理是采用自动回收机制,如果一个对象已经没有任何一个handle指向它,它就被自动回收了。
由于Module和Interface是硬件世界的,它们的例化是静态的,所以在编译后它们就已经被创建了。但是对象的创建是动态的,必须在仿真真正开始后才会被创建。以QuestaSim仿真为例,即便在执行了vsim命令后,对象其实还没有被创建,直到执行了“run 0ns”以后(那些应该在一开始就创建的)对象才真正被创建。
Ref: SystemVerilog Package (chipverify.com)
SystemVerilog中的package提供了保存和共享数据、参数和方法的机制,可以在多个module、class、program和interface中重用。
package中声明的内容都属于这个package作用域(scope)。
如果要想使用package中的东西,就需要先import这个package,然后通过package引用。SV中的import与python中的import是相同的功能。
- package my_pkg;
- typedefenumbit [1:0] { RED,YELLOW, GREEN, RSVD } e_signal;
- typedefstruct { bit [3:0]signal_id;
- bit active;
- bit [1:0] timeout;
- } e_sig_param;
-
- function my_func();
- $display ("mypkg::my_func is called...");
- endfunction
- endpackage
import的时候可以逐个import想要使用的东西,或者一锅端将package中的所有东西都import进来,如以下例子所示:
- //import my_pkg::* ; // Import all things defined in my_pkg
- import my_pkg::my_func ;// Import my_func only from my_pkg
-
- module tb_top;
-
- initial begin
- my_func();
- end
如果在package中定义的一个变量或者函数名与外部其它定义的变量或函数名冲突的时候,需要用pkg_name::var的形式进行引用以示区分。
- import my_pkg::* ; // Import all things defined in my_pkg
- typedef enum bit [1:0] { RED,YELLOW,GREEN} e_pixel;
-
- module tb_top;
- initial begin
- e_signal signal = RED;
- e_pixel pixel = RED;
- my_func();
- end
- endmodule
以上例子中,用到了两个RED,会发生冲突。这是需要加package名前缀用于区分,如下所示
- ...
- e_signal signal = my_pkg::RED;
- ...
一个常见的误解是使用“import pkg::*”会将一个package的所有的东西一股脑儿都导入进来。因此有人会担心这样做会不会使得编译产生的库会不适当地变得巨大因而导致仿真内存紧张?
其实不会。
“import pkg::*”这条语句的效果是告诉仿真器,当在当前namescope中没有找到某个类、变量或者函数、任务等的声明时可以到该pkg中去找。而且仿真器在寻找某个类、变量、函数、任务等时是遵循所谓的“就近原则”。
当有多个packge中都包含某个待搜索的{类、变量、函数、任务}名,而且它们都以“import pkg::*”的形式被导入时,此时会发生name collision,最明智的做法是如上节所述一样以pkg::name的显式的形式进行引用。
如上所述,import是用于package的导入。
`include将文件中所有文本原样插入包含的文件中。这是一个预处理语句,`include在import之前执行。
import不会复制文本内容。但是import可package中内容引入import语句所在的作用域。
To put it simply, include is equivalent to copy/paste or insertion of whatever is present in that file to wherever you include; import is used to make all/select variables from a package visible.
从根本上来说,在数字前端(RTL)仿真中,只有一种语句会产生耗时(或者说导致仿真时间前进)效果,即延时语句,比如说#10ns。同步电路是基于时钟沿进行动作的,而时钟信号的产生是基于延时语句的,比如说以下语句即生成了一个周期为4ns(250MHz)的时钟信号。
- initial begin
- clk = 0;
- forever clk = #2ns ~clk;
- end
除了显式地基于延时语句产生时间前进效果,更常见的(尤其是在同步数字电路中)是基于事件。不管是基于对基本的时钟沿事件,还是别的更复杂的事件,其根源都可以追溯到延时语句。
- always @(posedge clk) begin
- ...
- end
除了以上所示的@(posedge clk)之类的@(*)外,还有wait(*)等。这些等待事件的操作,如果所等待的事件需要时间流逝才会触发的话,那么等待该事件的发生就隐含了时间消耗。
在使用forever语句的一个常见错误是在forever循环中没有包含导致时间流逝(或者说时间前进)的因素,因此导致仿真陷入(trapped in)在这个循环中,仿真时间无法向前推进。这种循环被称为无限死循环。如下例所示:
- i = 0;
- forever begin
- i = i + 1;
- end
如上所示,在时刻0时执行forever的第一次循环:i_new = i_old + 1 = 1,然后回到forever处,由于没有时间前进的因素发生作用,所以时间仍然处于t=0。仿真器继续执行下一次i_new = i_old + 1。。。这样仿真器将永远停留在0时刻无限地执行“i_new = i_old + 1”处理。要解消这个死循环需要引入会导致时间前进的因素,如以下所示:
- i = 0;
- // example1
- initial begin
- forever begin
- # 10ns;
- i = i + 1;
- end
- end
- // example2
- initial begin
- forever begin
- @(posedge clk); // assuming clk is generated elsewhere
- i = i + 1;
- end
- end
- // example3
- initial begin
- forever begin
- wait(cnt[1:0]=2'b00); // assuming cnt is a free-running counter based on one clk
- i = i + 1;
- end
- end
SystemVerilog 为@事件控制加入了一个 iff 限定符。一般的写法是这样的:
- @(posedge clk iff(vld));
- do_something;
-
在没有iff,以上语句表示在每个clk上升沿都会发生事件触发,并进而唤醒“do_something”进程。但是有了iff后,只有在时钟上升沿并且同时满足vld有效的情况下,才会真正触发事件并向下执行“do_something”进程。它产生的效果和下面的代码一样。
- forever begin
- @(posedge clk);
- if(vld)
- break;
- end
- do_something;
另外一种等价的写法是:
- forever begin
- @(posedge clk);
- if(vld)begin
- do_something;
- end
- end
以上使用 iff 和 if 的区别以及使用iff的优点是:
使用iff时,事件表达式仅仅在 iff 之后的表达式为真时才会被评估并触发,在上面的例子中就是 vld 等于 1 的情况。注意:这个表达式只有在 vld 发生变化时计算,而不是 clk 发生变化的时候。这样会使得 iff 比 if 效率更高,因为它降低许多无谓的作为一个线程被唤醒的概率,避免了许多无谓的事件触发。所以更推荐使用 iff 。
此外,虽然也可以在always 和 always_ff语句使用iff,如下所示。但是由于iff并不能综合,而验证平台中一般不会使用always和always_ff(至少SV风格一般不会用,更谈不上UVM风格),所以在always 和 always_ff语句使用iff一般来说没有什么实用意义。
- always @(posedge clk iff(vld)) begin
- do_something;
- end
-
- always_ff @(posedge clk iff(vld)) begin
- do_something;
- end
队列:队列结合了链表和数组的优点,可以在一个队列的任何位置进行增加或者删除元素;
定宽数组:属于静态数组,编译时便已经确定大小。其可以分为压缩定宽数组和非压缩定宽数组:压缩数组是定义在类型后面,名字前面;非压缩数组定义在名字后面。Bit [7:0][3:0] name; bit[7:0] name [3:0];
动态数组:其内存空间在运行时才能够确定,使用前需要用new[]进行空间分配。
关联数组:其主要针对需要超大空间但又不是全部需要所有数据的时候使用,类似于hash,通过一个索引值和一个数据组成,索引值必须是唯一的。
Fork join:内部 begin end块并行运行,直到所有线程运行完毕才会进入下一个阶段。
Fork join_any:内部 begin end块并行运行,任意一个begin end块运行结束就可以进入下一个阶段。
Fork join_none:内部 begin end块并行运行,无需等待可以直接进入下一个阶段。
wait fork:会引起调用进程阻塞,直到它的所有子进程结束,一般用来确保所有子进程(调用进程产生的进程,也即一级子进程)执行都已经结束。
disable fork:用来终止调用进程 的所有活跃进程, 以及进程的所有子进程。
在SV中多线程之间同步主要有mailbox、event、 semaphore三种方法。
mailbox邮箱:主要用于两个线程之间的数据通信,通过put函数和 get 函数还有peek函数进行数据的发送和获取。put/get/peek为阻塞性方法,与之相对的还有非阻塞性的一组方法try_put/try_get/try_peek()。
Event:事件主要用于两个线程之间的一个同步运行,通过事件触发和事件等待进行两个线程间的运行同步。使用@(event)或者 wait(event.trigger)进行等待,->进行触发。
Semaphore:旗语主要是用于对资源访问的一个交互,通过key的获取和返回实现一个线程对资源的一个访问。使用put和 get函数获取返回key。一次可以多个。
函数能调用另一个函数,但不能调用任务,任务能调用另一个任务,也能调用另一个函数
函数总是在仿真时刻0就开始执行,任务可以在非零时刻执行
函数一定不能包含任何延迟、事件或者时序控制声明语句,任务可以包含延迟、事件或者时序控制声明语句
函数至少有一个输入变量,可以有多个输入变量,任务可以没有或者一个或者多个输入(input)、输出(output)和双向(inout)变量
函数只能返回一个值,函数不能有输出(output)或者双向(inout)变量;任务不返回任何值,任务可以通过输出(output)或者双向(inout)变量传递多个值
Interface是一组接口,用于对信号进行一个封装,捆扎起来。如果像verilog中对各个信号进行连接,每一层我们都需要对接口信号进行定义,若信号过多,很容易出现人为错误,而且后期的可重用性不高。因此使用interface接口进行连接,不仅可以简化代码,而且提高可重用性,除此之外,interface内部提供了其他一些功能,用于测试平台与DUT之间的同步和避免竞争。
Clocking block:在interface内部我们可以定义clocking块,可以使得信号保持同步,对于接口的采样vrbg和驱动有详细的设置操作,从而避免TB与 DUT的接口竞争,减少我们由于信号竞争导致的错误。采样提前,驱动落后,保证信号不会出现竞争。
需要注意的是,clocking block的input/output的定义。接口中的信号可以在clocking block中被任意定义为input、output,甚至同时被定义为input和output。定义为input的表示是需要对该信号进行采样观测,定义为output的则表示需要对该信号进行驱动。
进一步,在同一个interface可以定义多个clocking block,同一个信号在不同的clocking block中可以定义为不同的input或output方向,如以下示例代码中,定义了一个用于驱动的时钟块drv_ck和一个用于观测的时钟块mon_ck。在drv_ck既有input/output,因为需要根据当前接口信号状态进行驱动,所以需要进行采样;而mon_ck则时专门的观测用的时钟块,因此仅定义了input方向的信号。
- interface example_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;
- endclocking
- clocking mon_ck @(posedge clk);
- default input #1ns output #1ns;
- input ch_data, ch_valid, ch_ready, ch_margin;
- endclocking
- endinterface
封装:通过将一些数据和使用这些数据的方法封装在一个集合里,成为一个类。
继承:允许通过现有类去得到一个新的类,且其可以共享现有类的属性和方法。现有类叫做基类,新类叫做派生类或扩展类。
多态:得到扩展类后,有时我们会使用基类句柄去调用扩展类对象,这时候调用的方法如何准确去判断是想要调用的方法呢?通过对类中方法进行virtual声明,这样当调用基类句柄指向扩展类时,方法会根据对象去识别,调用扩展类的方法,而不是基类中的。而基类和扩展类中方法有着同样的名字,但能够准确调用,叫做多态。
即SystemVerilog是否支持多个侦听者侦听同一个事件?直接上一段代码仿真确认一下最直接了当。
- module tb;
- event e;
-
- initial begin
- #10ns;
- $display("event is triggered at %0t",$time);
- ->e;
- end
-
- initial begin : event_capture_a
- @e;
- $display("%m: event is capture at %0t(ns)",$time);
-
- #1ns;
- $finish;
- end
-
- initial begin : event_capture_b
- wait(e.triggered);
- $display("%m: event is capture at %0t(ns)",$time);
- end
-
- endmodule
运行结果如下所示:
# event is triggered at 10
# tb.event_capture_b: event is capture at 10(ns)
# tb.event_capture_a: event is capture at 10(ns)
# ** Note: $finish : event_example.sv(15)
所以,结论是,多人侦听同一个事件,只要侦听条件满足,都能侦测到同一个事件。
相关文章: