SystemVerilog: 仿真验证知识点点滴滴

目录

1. 软件世界和硬件世界

2. 什么是package

3. import pkg::*是将pkg的所有都导入进来吗?

4. import vs include

5. 仿真时间和无限死循环

6. iff: 对事件控制的增强

7. 定宽数组、动态数组、关联数组、队列各自特点和使用

8. fork join/fork join_any/fork join_none

8.1 什么是线程?

8.2 线程结构

8.3 fork...join

8.4 fork...join family 

8.5 基本代码示例 

8.6 其它

8.7 一图览尽SV线程

9. 线程间同步和通信

9.1 事件event

9.2 旗语(semaphore)

9.3 信箱mailbox

9.4 信箱和队列区别

9.5 小结

10. Task和function的区别

11.Interface和clocking blocking的好处

12. OOP(面向对象)的优势及其基本特性:封装、继承和多态

12.1 面向对象编程的优势

12.2 面向对象编程的三大基本特性

12.3 类的public、protected和local的区别

13. event支持一对多通信吗?

14. 接口参数中ref类型与inout类型的异同点

15. 深拷贝和浅拷贝的异同点

16. Equality Operators (===, !==, ==, !=)

17. 关于覆盖率

17.1 三种覆盖率

17.2 如何看待覆盖率与验证完备性?

18. 类型转换

18.1 基本概念

18.2 类的类型转换

19. force/release

20.  assign/deassign

21. $test$plusargs和$value$plusargs

$test$plusargs

$value$plusargs


1. 软件世界和硬件世界

        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”以后(那些应该在一开始就创建的)对象才真正被创建

2. 什么是package

        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;
...

3. import pkg::*是将pkg的所有都导入进来吗?

        一个常见的误解是使用“import pkg::*”会将一个package的所有的东西一股脑儿都导入进来。因此有人会担心这样做会不会使得编译产生的库会不适当地变得巨大因而导致仿真内存紧张?

        其实不会。

        “import pkg::*”这条语句的效果是告诉仿真器,当在当前namescope中没有找到某个类、变量或者函数、任务等的声明时可以到该pkg中去找。而且仿真器在寻找某个类、变量、函数、任务等时是遵循所谓的“就近原则”。

        当有多个packge中都包含某个待搜索的{类、变量、函数、任务}名,而且它们都以“import pkg::*”的形式被导入时,此时会发生name collision,最明智的做法是如上节所述一样以pkg::name的显式的形式进行引用。

4. import vs include

        如上所述,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.

5. 仿真时间和无限死循环

        从根本上来说,在数字前端(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

6. iff: 对事件控制的增强

        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

7. 定宽数组、动态数组、关联数组、队列各自特点和使用

  • 队列:队列结合了链表和数组的优点,可以在一个队列的任何位置进行增加或者删除元素;

  • 定宽数组:属于静态数组,编译时便已经确定大小。其可以分为压缩定宽数组和非压缩定宽数组:压缩数组是定义在类型后面,名字前面;非压缩数组定义在名字后面。Bit [7:0][3:0] name; bit[7:0] name [3:0];

  • 动态数组:其内存空间在运行时才能够确定,使用前需要用new[]进行空间分配。

  • 关联数组:其主要针对需要超大空间但又不是全部需要所有数据的时候使用,类似于hash,通过一个索引值和一个数据组成,索引值必须是唯一的。关联数组可以用来保存稀疏矩阵的元素。当你对一个非常大的地址空间寻址时,该数组只为实际写入的元素分配空间,这种实现方法所需要的空间要小得多。此外,关联数组有其它灵活的应用,在其它软件语言也有类似的数据存储结构,被称为哈希(Hash)或者词典(Dictionary),可以灵活赋予键值(key)和数值(value) 。

8. fork join/fork join_any/fork join_none

8.1 什么是线程?

        在SystemVerilog语境中,线程(thread)和进程(process)是表达同一个意思,可以互换使用。线程就是任何一段可以独立运行的一段代码。

        比如说,在verilog中,module的实例就代表一个线程,其生命期是从仿真开始到仿真结束。而module中的每个initial或者always则对应着该module的子线程,同样是从零时刻开始独立运行(always块的生命期也是从仿真开始到仿真结束,而initial则有可能提前结束),在verilog中这些线程是天然地并发执行的。

8.2 线程结构

        线程的执行轨迹是呈树状结构的,任何线程都有且只有一个父线程,除了唯一的一个根线程以外。父线程掌握着子线程的生杀予夺大权,父线程可以生成若干个子线程,父线程也可以暂停或终止子线程。子线程终止后,父线程可以继续执行。父线程终止时,其子线程相应地也被终止。

8.3 fork...join

        module, initial和always这些天然的线程并发性对于硬件建模是足够的,但是对于验证环境来说显得缺少灵活性。为了提高线程管理的灵活性,在verilog中导入了fork...join用于产生并发线程,它必须用于initial块中,在initial块中,对其中语句的组织方式有两种基本类型:

  1. begin...end
  2. fork...join

        在begin...end中语句以顺序方式执行,类似于软件世界的程序执行。在fork...join等中的语句块(比如说,begin...end可以看作是一个语句块)则是以并发方式执行。

        在fork...join的基础上,SystemVerilog进一步导入了它的两个兄弟fork...join_none和fork...join_any,进一步提高了线程并发管理的灵活性。从名字上来看,可以把原来的fork_joint理解为fork...join_all。以下叙述中为了简便起见有时候会分别以ALL, ANY和NONE来指代它们。

8.4 fork...join family 

三者之间的差别如下所示:

fork...join内部 子线程并行运行,等其中所有的子线程都运行结束了,才前往执行fork...join后面的代码。类似于“阻塞”的含义,可以理解为完全阻塞
fork...join_any等fork...any中任何某个子线程运行结束了就前往继续执行fork...join_any后面的代码。但是fork...any内的其它子线程继续执行。可以认为是部分阻塞(blocking)
fork...join_none

生成fork...join_none中的各子进程就立即前往执行fork...any后面的代码,fork...join_none中的各子线程和外面的线程并发执行。

相对于fork...join/any,可以理解为非阻塞的

8.5 基本代码示例 

module fork_join;
    initial begin
    
        #5 $display ("[%0t] Start fork_join ...", $time);
        
        // Main Process: Fork these processes in parallel and wait untill all 
        // of them finish
        fork
            // Thread1 : Print this statement after 5ns from start of fork
            #10 $display ("[%0t] Thread1: start and then stop immediately", $time);
         
            // Thread2 : Print these two statements after the given delay from start of fork
            begin                                                
                #5  $display ("[%0t] Thread2: start...", $time);      
                #10 $display ("[%0t] Thread2: stop...", $time);      
            end                           
         
            // Thread3 : Print this statement after 10ns from start of fork
            #13 $display ("[%0t] Thread3: start and then stop immediately", $time);  
        join
        
        // Main Process: Continue with rest of statements once fork-join is over
        $display ("[%0t] After Fork-Join", $time);
    end
endmodule
module fork_join_any;
    initial begin
  
        #5 $display ("[%0t] Start fork_join_any ...", $time);
        
        // Main Process: Fork these processes in parallel and wait untill all 
        // of them finish
        fork
            // Thread1 : Print this statement after 5ns from start of fork
            #10 $display ("[%0t] Thread1: start and then stop immediately", $time);
         
            // Thread2 : Print these two statements after the given delay from start of fork
            begin                                                
                #5  $display ("[%0t] Thread2: start...", $time);      
                #10 $display ("[%0t] Thread2: stop...", $time);      
            end                           
         
            // Thread3 : Print this statement after 10ns from start of fork
            #13 $display ("[%0t] Thread3: start and then stop immediately", $time);  
        join_any
        
        // Main Process: Continue with rest of statements once fork-join is over
        $display ("[%0t] After Fork-Join", $time);
    end
endmodule
module fork_join_none;
   initial begin
 
        #5 $display ("[%0t] Start fork_join_none...", $time);
        
        // Main Process: Fork these processes in parallel and wait untill all 
        // of them finish
        fork
            // Thread1 : Print this statement after 5ns from start of fork
            #10 $display ("[%0t] Thread1: start and then stop immediately", $time);
         
            // Thread2 : Print these two statements after the given delay from start of fork
            begin                                                
                #5  $display ("[%0t] Thread2: start...", $time);      
                #10 $display ("[%0t] Thread2: stop...", $time);      
            end                           
         
            // Thread3 : Print this statement after 10ns from start of fork
            #13 $display ("[%0t] Thread3: start and then stop immediately", $time);  
        join_none
        
        // Main Process: Continue with rest of statements once fork-join is over
        $display ("[%0t] After Fork-Join", $time);
    end
endmodule

  

        以上三个代码段分别是采用fork...join/join_any/join_none,其运行结果的差异也就体现在"After Fork-Join"打印语句出现的时间。如上所示,使用fork...join时,这条语句在所有三个子线程全部结束并从fork...join退出来后才执行;而使用fork...join_any时,这条语句在三个子线程中最早结束的Thread1结束时就执行了;而使用fork...join_none时,这条语句没有受到fork...join_none的任何阻塞,(在仿真器启动fork...join_none之后)立刻就被执行了,比三个子线程都要早!

8.6 其它

  • wait fork:会引起调用进程阻塞,直到它的所有子进程结束,一般用来确保所有子进程(调用进程产生的进程,也即一级子进程)执行都已经结束。

  • disable fork:用来终止调用进程 的所有活跃进程, 以及进程的所有子进程。

8.7 一图览尽SV线程

9. 线程间同步和通信

        在SV中多线程之间同步和通信主要有mailbox、event、 semaphore三种方法。

9.1 事件event

  • 通过event声明一个event变量;
  • event变量可以由一端去触发,再由另一端(一个或者多个)完成阻塞式的等待,即可实现多个线程之间的同步;
  • 通过->来触发事件;
  • 其他等待该事件的线程可以通过@(event)或者 wait(event.trigger)来等待事件触发或检查事件的触发状态;

9.2 旗语(semaphore)

        旗语主要是用于对共享资源访问的控制,通过key的获取和返回实现多个线程对公共资源的互斥式访问。使用put和 get函数获取返回key。一次可以多个。

        在创建旗语时,会为其分配固定的钥匙数量;

        使用旗语的进程必须先获得钥匙,才可访问资源;

        旗语的钥匙数量可以有多个,等待旗语的进程也可以有多个;

        旗语的等待队列是先进先出(FIFO),即先排队等待旗语的将优先得到钥匙;

        【旗语操作】

        semaphore sm;//声明一个旗语

        sm=new(1); //创建带一个钥匙的旗语

        从旗语获取一个或多个钥匙(阻塞型):get()

        返回一个或多个钥匙:put()

        获取一个或多个钥匙而不被阻塞:try_get()

task send;

    sem.get(1) ///获取钥匙

    ……

    sem.put(1); //处理完成时把钥匙返回

endtask

9.3 信箱mailbox

        mailbox主要用于两个线程之间的数据通信,通过put函数和 get 函数还有peek函数进行数据的发送和获取。put/get/peek()为阻塞性方法,与之相对的还有非阻塞性的一组方法try_put/try_get/try_peek()。

        SV信箱可以存放任何数据类型,也可以从信箱中读取这些数据,

        创建信箱:new()

        将信息写入信箱:put()

        写入信箱但不会阻塞:try_put()

        获取信息:get() 获取信息并取出数据:

        peek()获取信息不会取出数据

        从信箱获取数据但不会阻塞:try_get()/try_peek()

        获取信箱信息数目:num()

9.4 信箱和队列区别

  • 信箱必须通过new()例化,队列只需要声明
  • 信箱的存取方法put()和get()是阻塞方法,不一定会立即返回;队列的存取方法push_back和pop_front()是非阻塞方法,会立即返回;
  • 传递形式参数时,如果是input方向,则信箱类型传递的是句柄,队列类型完成的是队列内容的拷贝;
  • 队列可以用作FIFO(先进先出),也可以用作LIFO(后进先出);邮箱只能以FIFO的形式工作

9.5 小结

        Event:最小信息量的触发,即单一的通知单元,用来做事件的触发,也可多个事件组合起来作线程之间的同步;

        Semaphore:共享资源安全,用于多线程间需要对某一公共资源做访问;

        Mailbox:SV类似FIFO,在线程之间做数据通信或者内部数据缓存;

10. Task和function的区别

  • 函数能调用另一个函数,但不能调用任务,任务能调用另一个任务,也能调用另一个函数

  • 函数总是在仿真时刻0就开始执行,任务可以在非零时刻执行

  • 函数一定不能包含任何延迟、事件或者时序控制声明语句,任务可以包含延迟、事件或者时序控制声明语句

  • 函数至少有一个输入变量,可以有多个输入变量,任务可以没有或者一个或者多个输入(input)、输出(output)和双向(inout)变量

  • 函数只能返回一个值,函数不能有输出(output)或者双向(inout)变量;任务不返回任何值,任务可以通过输出(output)或者双向(inout)变量传递多个值

11.Interface和clocking blocking的好处

  • 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

12. OOP(面向对象)的优势及其基本特性:封装、继承和多态

12.1 面向对象编程的优势

  1. 易维护:采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。

  2. 质量高:在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。

  3. 效率高:在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。

  4. 易扩展:由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。

12.2 面向对象编程的三大基本特性

  • 封装:通过将一些数据和使用这些数据的方法封装在一个集合里,成为一个类。

  • 继承:允许通过现有类去得到一个新的类,且其可以共享现有类的属性和方法。现有类叫做基类,新类叫做派生类或扩展类

  • 多态:得到扩展类后,有时我们会使用基类句柄去调用扩展类对象,这时候调用的方法如何准确去判断是想要调用的方法呢?通过对类中方法进行virtual声明,这样当调用基类句柄指向扩展类时,方法会根据对象去识别,调用扩展类的方法,而不是基类中的。而基类和扩展类中方法有着同样的名字,但能够准确调用,叫做多态。

12.3 类的public、protected和local的区别

  1. 如果没有指明访问类型,那么成员的默认类型是public,子类和外部均可以访问成员。

  2. 如果指明了访问类型是protected,那么只有该类或者子类可以访问成员,而外部无法访问。

  3. 如果指明了访问类型是local,那么只有该类可以访问成员,子类和外部均无法访问。

13. event支持一对多通信吗?

        即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)

        所以,结论是,多人侦听同一个事件,只要侦听条件满足,都能侦测到同一个事件。

14. 接口参数中ref类型与inout类型的异同点

        最主要的区别在于:任务内外可以通过ref类型变量实时传递该参数的变化情况。而inout类型参数进行进入任务以及从任务中退出才实现task内外的值的同步,无法实现参数值变化的实时传递。详细可参考:SystemVerilog: What is and why to use pass-by-reference?

        此外向子程序传递数组时应尽量使用ref获取最佳性能,如果不希望子程序改变数组的值,可以使用const ref类型。

15. 深拷贝和浅拷贝的异同点

        SystemVerilog deep copy copies all the class members and its nested class members. unlike in shallow copy, only nested class handles will be copied. In shallow copy, Objects will not be copied, only their handles will be copied. to perform a full or deep copy, the custom method needs to be added.

        如果类的成员中只有普通的成员,没有指向别的对象的句柄成员,那浅拷贝和深拷贝没有差别,都会将原有对象复制一份,产生一个新对象,对新对象里的值进行修改不会影响原有对象,新对象和原对象完全分离开。

        如果类的成员中有指向别的对象的句柄成员(即上面说的嵌套类成员,nested class members),浅拷贝会对句柄进行拷贝,但是该句柄所指向的对象不会发生拷贝。新的对象和旧的对象中的该句柄成员指向同一个对象。通常来说,这都不是所希望看到。要想将新的对象和旧的对象完全隔离开来,就需要深拷贝(嵌套式拷贝)。深拷贝方法通常需要开发者自己定义。

16. Equality Operators (===, !==, ==, !=)

        logical equality: ==, !=

        case-equality: 

OperatorDescription
a === b判断a与b是否相等,X和Z也在比较范围以内。比如说('h010x === 'h010x)为True,('hz10x === 'hz10x)为True,('h010x === 'h0100)为False,等等
a !== b判断a与b是否不相等,X和Z也在比较范围以内。
a == b判断a与b是否相等,如果a或者b中包含值为X或Z的比特的话,结果为unknown(X)
a != b判断a与b是否不相等,如果a或者b中包含值为X或Z的比特的话,结果为unknown(X)

17. 关于覆盖率

17.1 三种覆盖率

        代码覆盖率,功能覆盖率,断言覆盖率。

  • 代码覆盖率主要是针对RTL设计代码的运行完备度的体现。主要有行覆盖率、条件覆盖率、fsm覆盖率、跳转覆盖率、分支覆盖率,他们是否都是运行到的,比如 fsm,是否各个状态都运行到了,然后不同状态之间的跳转是否也都运行到了。代码覆盖率不需要verifier去额外地编写代码,只要在仿真工具命令行执行覆盖率收集选项即可自动进行代码覆盖率收集和统计。

  • 功能覆盖率主要是针对spec文档中功能点的覆盖检测。需要verifier自己编写covergroupcoverpoint去覆盖我们想要覆盖的功能点。 

  • 断言覆盖率:

17.2 如何看待覆盖率与验证完备性?

  • 功能覆盖率和代码覆盖率两者缺一不可,功能覆盖率表示着代设计是否具备这些功能,代码覆盖率表示我们的测试是否完备,代码是否冗余。当功能覆盖率高而代码覆盖率低时,表示covergroup是不是写少了,case写少了;或者代码冗余。当功能覆盖率很低而代码覆盖率高时,表示代码设计是不是全面,功能点遗漏;covergroup写的是不是冗余了。只有当两者覆盖率都高的时候才表明我们验证的大部分是可靠的。

  • 代码覆盖率很难达到100%,一般情况下达到90%多已经非常不错了。对于未能覆盖率到代码,可以通过code review的方式判断是否是可以允许的或者说可以解释并确认无害,如果可以则可以加入exclude列表;否则就需要追加testcase进行覆盖

18. 类型转换

18.1 基本概念

        类型转换分类:

  1.         (1) 显式转换
    1.                 (1-1) 静态转换
      1.                 (1-2) 动态转换
      2.         (2) 隐式转换        

        静态转换:单引号,不做转换值的检查,编译时转换,转换失败也不会报告。比如说,int`(40)。

        动态转换:$cast(tgt , src),运行时执行转换。转换失败会报告。会检查句柄所实际指向的对象类型,匹配才能够进行转换。

18.2 类的类型转换

        类的类型转换(由大变小可以,由小扩大不行。注意,父类覆盖范围小,子类覆盖范围大):

        子类句柄 赋值 父类句柄,可以直接进行

        父类句柄 赋值 子类句柄,需要动态转换:

$cast(Hchild, Hparent)

        父类句柄虽然指向一个子类对象,但是不能引用属于子类的属性。


        通过$cast()语句将父类句柄赋值给子类句柄,即将子类句柄指向父类句柄所指向的对象。此时若父类句柄指向的是子类类型的对象则结果就是子类句柄指向子类对象,仿真不会报错;但如果父类指向的是父类对象则结果就是子类句柄指向了父类对象,这是仿真不允许的,会报错。

        因此简而言之:$cast会检查父类句柄是不是指向子类对象,是则转化成功,否则失败。

19. force/release

        在一个过程块中,可以用两种不同的方式对信号变量或表达式进行连续赋值:force; assign

  •  assign是可以综合的,可以用在设计中,也可以用在验证平台中;force是不可综合的,只能用在验证平台中
  •  两种方式都有各自配套的命令来停止赋值过程。
    • assign vs deassign
    • force vs release
  •  两种不同方式均不允许赋值语句间的时间控制。

        force 和 release 用于对信号进行强制赋值,覆盖其它驱动源对该信号的驱动。

wire [7:0] in1;
wire [7:0] in2;
wire [7:0] out;
MY_ADDER u_adder(.IN1(in1),.IN2(in2),.OUT(out));

assign in1 = 8'd10;
assign in2 = 8'd20;

initial
 begin  
   #20 force u_adder.IN1 = 8'd100;
   #30 release u_adder.IN1;
end

        在以上例子中,in1和in2使用assign语句进行赋值,因此从0时刻开始u_adder的两个输入端口的值就分别是10和20,则其输出为30.

        但是,从#20开始,force语句将u_adder.IN1强制改写为100,这样其输出就变为120了;到#30时,该force语句被释放了,u_adder.IN1的值回到10,输出值也恢复为30.

注意:

     1、force的赋值优先级高于assign。

     2、如果先使用assign,再使用force对同一信号赋值,则信号的值为force所赋的值,当执行release后,则信号的值为assign所赋的值。

     3、如果使用force语句对同一个信号(相同时刻或者不同时刻)多次赋值,该信号的值总是由最近一次的force语句决定。而执行release后,则所有关于该信号的force赋值均被解除。换句话说,force与release不是相括号那样配对,一个信号只有被force和未被force(包括force后被release了)两种状态

     4、可以对多比特信号的某个(确定)位、某些(确定)位或拼接的信号,使用force和release赋值,换句话说force/release是按比特单位其作用的。

20.  assign/deassign

21. $test$plusargs和$value$plusargs

        $test$plusargs和$value$plusargs作为进行Verilog和SystemVerilog仿真运行时调用的系统函数,可以在仿真命令中直接通过进行赋值的方式将参数传递进入到设计中,避免了调换参数带来的频繁编译等问题。由于是系统函数,所以不依赖于仿真器,主流仿真器的命令行选项都支持。灵活使用这两个函数可以给搭建测试平台带来很大的便利,同时对于理解Factory中用例是如何传递进ProxyClass有一定的帮助。

$test$plusargs

        用于从仿真命令行指定运行选项(run-options)。

        通常在验证代码中有基于该运行选项参数选择不同分支的处理,从而实现根据命令行参数来动态选择不同的仿真处理的功能。

        例:如以下代码示例所示,在仿真中需要考虑将三种不同的数据加载到mem变量中。在仿真命令行指定“+test01”(或者“+test02”、“+test03”),仿真运行时,$test$plusargs会在命令行中搜索指定的字符,若找到相应字符,在函数返回“1”,并执行相应语句;否则返回“0”。

// test.v

initial begin

    if($test$plusargs("test01"))
        $readmemh("test01.dat",mem);
    else if($test$plusargs("test02"))
        $readmemh("test02.dat",mem);
    else if($test$pluargs("test03"))
        $readmemh("test03.dat",mem);
    else
        $display("Invalid simulation options\n");

end

$value$plusargs

        $value$plusargs(与$test$plusargs)略有区别,用于将命令行运行选项参数(run-options)中的参数值传递给指定的信号或者字符,其语法格式如下:

Integer=$value$plusargs(“string”,signalname);

        其中string=”plusarg_format”+”format_string”,”plusarg_format”指定了用户定义的要进行传递的值,”format_string”指定了要传递的值的格式(类似$display中定义的%s、%h、etc.),并且string中”plusarg_format”和”format_string”格式应该为”plusarg_format”(=/+)"format_string”。如果转换后的位宽和传递的值不一致,则按照如下规则转换:

plusargs位宽与signalname的关系signalname值
<plusarg左补零
>plusarg截位
plusargs为负数按照正数处理
不匹配若为指定默认值,则reg类型为x

        $value$plusargs使用示例如下:

test.v
    if ($value$plusarg("FINISH=%d",stop_clk))  begin
        repeat (stop_clk) @(posedge clk);
        $finish;
    end
 
if($value$plusargs("TESTNAME=%s",testname))
    begin
        $display("Running test %0s ,testname");
        ....
    end
 
if($value$plusargs("FREQ=%0f",frequency))
    begin
        frequency=8.3333333;
        .....
    end

        然后便可以在仿真命令行指定FINISH、TESTNAME和FREQ等参数,比如说:

vcs -full64 +FINISH=1000 +TESTNAME=dma_test0 +FREQ=0.333 ... 

相关文章:

SystemVerilog: 动态数组

SystemVerilog: Dynamic part-select and array slicing

SystemVerilog: What is and why to use pass-by-reference?

  • 11
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨牛慢耕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值