SystemVerilog测试平台编写指南

1.数据类型

  • VerilogHDL中有2种变量类型:wirereg,这两种变量是4值类型的(即有四种状态)。

  • SystemVerilog在此基础上拓展了一种变量类型:logic类型,该变量类型可以取代wire型变量和reg型变量。但需要注意的是,logic型的变量不能够有多个结构性的驱动,所以在对inout端口变量进行声明的时候,需要使用wire变量类型。

  • 其余的变量类型如下表所示:

    数据类型状态数量类型说明
    bit双状态单比特
    bit [31:0] bit32双状态32比特无符号整数
    int unsigned ui双状态32比特无符号整数
    int i双状态32比特有符号整数
    byte b8双状态8比特有符号整数
    shorint si双状态16比特有符号整数
    longint li双状态64比特有符号整数
    integer i四状态32比特有符号整数
    time四状态64比特无符号整数
    realtime双状态双精度浮点数
  • 定宽数组。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 = array[5]

  • 队列。队列是SystemVerilog新引进的数据类型,它结合了链表和数组的优点,可以随时增删数组里的元素,也可以做到快速的数组操作。在对队列进行声明的时候,需要使用[$]

  • 关联数组。类似于Python里的字典。关联数组的声明有些复杂,例如:bit [63:0] assoc[bit[63:0]]

  • 数组的方法。数组拥有各类方便用户的方法,但是这些方法不能够使用在合并数组上。

    方法方法示例
    数组求和array.sum()
    数组求积array.product()
    数组求与array.and()
    数组求或array.or()
    数组求异或array.xor()
    数组找最小值min = array.min()
    数组找最大值max = array.max()
    数组找唯一值(不重复的值)unique = array.unique()
    寻找数组元素tq = array.find with (item > 3)
    寻找数组元素的下标tq = array.find_index with (item > 3)
    寻找数组中第一个出现的元素tq = array.find_first with (item > 3)
    寻找数组中第一个出现的元素的下标tq = array.find_first_index with (item > 3)
    寻找数组中最后一个出现的元素tq = array.find_last with (item > 3)
    寻找数组中最后一个出现的元素的下标tq = array.find_last_index with (item > 3)
    数组的反序array.reverse()
    数组从低到高排序array.sort()
    数组从高到低排序array.rsort()
    数组元素随机array.shuffle()
  • 流操作符。流操作符<<>>用在赋值表达式的右边,后面带表达式、结构或者数组,流操作符用于把其后面的数据打包成一个比特流。

    initial begin
        int h;
        bit [7:0] b, g[4], j[4] = '{8'ha,8'hb,8'hc,8'hd};
        bit [7:0] q, r, s, t;
        
        h = {>>{j}};
        h = {<<{j}};
        h = {<<byte{j}};
        h = {>>byte{j}};
        b = {<<{8'b1101_0100}};
        b = {<<4{8'b1101_0100}};//半字节倒序:0100_1101
        {>>{q,r,s,t}} = j;
        h = {>>{t,s,q,r}};
    end
    
  • 枚举类型。枚举类型用enum和一个中括号{}定义,常用于状态机中的状态定义,常用的枚举类型方法如下所示。

    方法方法示例
    返回第一个枚举变量color_e.first()
    返回最后一个枚举变量color_e.last()
    返回下一个枚举变量color_e.next()
    返回以后第N个枚举变量color_e.next(N)
    返回上一个枚举变量color_e.prev()
    返回之前第N个枚举变量color_e.prev(N)

2.过程语句和子程序

  • 任务和函数之间有着很大的差别,最大的差别在于:任务内部可以消耗仿真时间,而函数是表达式,不能够消耗仿真时间,例如:不能有wait或者posedge clk这样的语句存在。并且对于函数来说,必须要有返回值,而且返回值必须要立即使用,不带返回值的函数需要用void进行声明。
  • 参数的方向:inputoutputinoutrefref类型是参考类型,直接传递参数的指针,所以在对ref型的数据进行修改时,是直接对原始数据进行修改,类似于C语言的“实参”。
  • VerilogHDL中的对象是静态分配的,不像其它的编程语言那样将对象放在堆栈里面,所以在多个地方同时调用同一个子程序(函数或者任务),可能会使用到同一个局部变量。解决这一问题的方法在于声明任务或者函数的时候,添加automatic自动存储关键词,这样会迫使仿真器使用堆栈进行仿真。
  • 仿真时间值。SystemVerilog提供多种指明仿真时间的方法:``timescale 1ns/1pstimeunittimeprecision$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而卡死,此时虽然现实时间在流逝,但仿真时间不再向前推进了。

  • 对于时间片而言,主要有以下几个执行区域,其中AcitiveInactiveNBA区域属于Active Region,主要执行的是module(即RTL)中的代码;而ReacitiveRe-inactiveRe-NBA区域属于Reactive Region,主要执行的是program(即testbench)中的代码:

    区域名称功能
    Preponed只读区域,对数值进行采样(例如并发断言和时间块),可以理解为上一个时间片的Postponed
    Active执行阻塞赋值、计算非阻塞赋值的右值、执行连续赋值、执行系统函数、执行原语输出、函数和任务的返回值输出
    Inactive激活#0(零延时)语句
    NBA更新非阻塞赋值的左值
    Observed对并发断言进行判断(只进行判断)、时钟块输入信号采样(时钟块默认偏移为0)、时钟块事件触发(意味着@cb晚于@posedge clk)
    Reactive执行阻塞赋值、计算非阻塞赋值的右值、执行连续赋值、执行并发断言的相关代码
    Re-inactive执行#0(零延时语句)
    Re-NBA更新program中非阻塞赋值的左值、默认情况下(#0驱动输出)时钟块进行输出
    Postponed执行$strobe和$monitor和系统函数或任务、采样信号的值收集功能覆盖率
  • 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种不同的复制方式:简易复制和深层次复制。简易复制方式就像是对原对象的简单粗暴赋值,不会考虑对象内部的类;深层次复制可以解决这个问题,但用户需要自己维护复制函数。

    class transaction;
        static int count=0;
        int id;
        int addr, data;
        statistics stats;//另一个类,内有成员变量int starttime
        
        function new();
            stats = new();
            id = count++;
        endfunction
    endclass
    
    //shallow copy
    transaction src, dst;
    initial begin
        src = new();
        src.stats.starttime = 30;
        dst = new src;
        dst.stats.starttime = 90;//同时src.stats.starttime也变成了90
    end 
    
    //deep copy
    class transaction;
        static int count=0;
        int id;
        int addr, data;
        statistics stats;//另一个类,内有成员变量int starttime
        
        function new();
            stats = new();
            id = count++;
        endfunction
        
        function transaction copy();
            copy = new();
            copy.addr = addr;
            copy.data = data;
            copy.stats = stats.copy();//类statistics内部也需要维护一个复制函数
            id = count++;
        endfunction
    endclass
    
  • 类的公有和私有。在SystemVerilog中,所有的成员都是公有的,若在声明时加localprotected关键词则可以将其变为私有成员或保护成员。网页、游戏等代码需要的是长时间的稳定性,为此需要将所用到的类都声明为私有类型,但是测试平台不同,有时我们不仅需要注入正确的激励,还需要注入错误的激励,这时候公有的类可以使我们的操作更加方便。

5.随机化

  • 带有约束的随机是SV的灵魂,我们不可能指望用一个接着一个的定向激励去覆盖所有的DUT功能点,也不可能完全让激励放任自由地随机化,最好的设想就是利用带有约束的随机产生某一个方向上的随机。下面的代码展示了一个简单的带有随机的类:

    class packet;
        rand bit [7:0] data;
        randc int count;
        constraint count_cons {count>10;count<15;}//约束是声明性语句,需要用到{}
    endclass
    
    //创建一个对象并对其进行随机化
    packet pkt;
    initial begin
    	pkt = new();
        assert(pkt.randomize());
        else $fatal(0, "packet::randomize failed!")
    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操作符产生一个集合,使得随机变量在这个集合中随机取值。另外可以使用$运算符代指最小值或最大值。

    rand int a, low, high;
    const int array[5]='{1,3,5,7,9};
    rand bit [6:0] b;
    rand bit [5:0] c;
    rand int d;
    constraint a_cons {a inside [low:high];}//low<=a<=high
    //constraint a_cons {!(a inside [low:high]);}//a<low||a>high
    constraint b_cons {b inside {[$,4],[20:$]};}//(0<=b<=4)||(20<=b<=127)
    constraint c_cons {c inside {[$,2],[20,$]};}//(0<=c<=2)||(20<=c<=63)
    constraint d_cons {d inside array;}//d==1||d==3||d==5||d==7
    
  • 条件约束。使用条件约束,可以使得约束条件在某些情况下才起作用,条件约束可以使用->符号,也可以利用关键词if()...else()

    class transaction;
        bit workmode;
        int crc;
        constraint cons_crc {if(workmode) crc==0; else crc=$random();}
    endclass
    
    class transaction;
        bit workmode;
        int crc;
        constraint cons_crc {workmode -> crc==0;}
    endclass
    
  • 解的概率。SystemVerilog并不保证随机约束器能够给出精确的解,但是用户可以控制概率的分布。另外需要注意的是,SystemVerilog中随机的约束是双向的。

    //x有2种解:0和1,y有4种解:0、1、2、3,每一种随机结果的概率是一样的
    class imp;
        rand bit x;
        rand bit [1:0] y;
    endclass
    
    //共有5种解:x=0,y=0;x=1,y=0;x=1,y=1;x=1,y=2;x=1,y=3,但是每种解的概率不一定相同
    class imp;
        rand bit x;
        rand bit [1:0] y;
        constraint cons_xy {(~x)->(y==0);}//当x=0的时候,y只能为0
    endclass
    
    //共有3种解:x=1,y=1;x=1,y=2;x=1,y=3,且每种解的概率一样,均为1/3
    class imp;
        rand bit x;
        rand bit [1:0] y;
        constraint cons_xy {y>0;(~x)->(y==0);}//当x=0的时候,y只能为0
    endclass
    
    //共有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),但是每种解的概率不一定相同
    class imp;
        rand bit x;
        rand bit [1:0] y;
        constraint cons_xy {solve x before y;(~x)->(y==0);}//当x=0的时候,y只能为0
    endclass
    
    //共有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),但是每种解的概率不一定相同
    class imp;
        rand bit x;
        rand bit [1:0] y;
        constraint cons_xy {solve y before x;(~x)->(y==0);}//当x=0的时候,y只能为0
    endclass
    
  • 对于class内的约束模块constraint,可以通过constraint_mode()开启或者关闭约束模块;而对于class内的成员变量,可以通过rand_mode()开启或者关闭随机。

  • SystemVerilog允许使用randomize() with来增加额外的约束,with后面的{}里,SystemVerilog使用的是类的作用域。

  • SystemVerilog提供了许多常用的系统函数,具体的函数列在下表:

    函数名函数示例
    平均分布函数,返回32位有符号随机数$random()
    平均分布函数,返回32位无符号随机数$urandom()
    指定范围内的平均分布函数$urandom_range()
    指数衰落函数$dist_exponential()
    自然分布函数$dist_normal()
    钟型分布函数$dist_poisson()
    平均分布函数$dist_uniform()

6.线程的使用

  • SystemVerilog提供了创建多线程的方法,即fork...join方法。

    //启动完之后,所有线程结束才继续执行后面的程序
    fork
        ...
    join
    
    //线程启动之后,只要有一个线程结束就继续执行后面的程序
    fork
        ...
    join_any
    
    //线程启动之后,继续执行后面的程序
    fork
        ...
    join_none
    
  • 线程中的自动变量。当使用循环来创建线程的时候,如果在进入下一个循环之前没有保存当前的变量,那么该变量会在下一个线程当中被覆盖,这是一个很难发现的漏洞。

    //最终结果会显示3个3
    program no_auto;
        initial begin
            for(int i=0; i<3; i++) begin
                fork
                    $write(i);
                join_none
            end
            #0 $display("\n");
        end
    endprogram
    
    //最终结果:1 2 3
    program no_auto;
        initial begin
            for(int i=0; i<3; i++) begin
                fork
                    automatic int j = i;
                    $write(j);
                join_none
            end
            #0 $display("\n");
        end
    endprogram
    
    //最终结果:1 2 3
    program automatic no_auto;
        initial begin
            for(int i=0; i<3; i++) begin
                fork
                    int j = i;
                    $write(j);
                join_none
            end
            #0 $display("\n");
        end
    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个基本的操作:newgetput

  • SystemVerilog提供信箱供线程之间进行信息交换,可以将信箱看作是一个连接了收端和发端的FIFO,当信箱为空的时候,读取信息(mailbox.get())会被阻塞;当信箱为满的时候,放入信息(mailbox.put())这个动作会发生阻塞。信箱用mailbox关键词进行声明,mailbox是一个类,所以在使用之前需要new()操作,在创建对象的时候可以传入参数,这个参数是可容纳数据的上限,如果不传或者传入数据为0,则视为信箱的容量无限大。

    program automatic synchronized;
        class producer;
            mailbox mbx;
            
            function new(mailbox mbx);
                this.mbx = mbx;
            endfunction
            
            task run();
                for(int i=1; i<4; i++) begin
                    $display("producer:before put information(%d)", i);
                    mbx.put(i);
                end
            endtask
        endclass
        
        class consumer;
            mailbox mbx;
            
            function new(mailbox mbx);
                this.mbx = mbx;
            endfunction
            
            task run();
                repeat(3) begin
                    int i;
                    mbx.get(i)
                    $display("consumer:after get information(d%)", i);
                end
            endtask
        endclass
        
        producer p;
        comsumer c;
        initial begin
            maibox mbx;
            mbx = new();
            p = new(mbx);
            c = new(mbx);
            fork
                p.run();
                c.run();
            join
        end
    endprogram
    
    //result
    producer:before put information(1)
    producer:before put information(2)
    consumer:after get information(1)
    consumer:after get information(2)
    producer:before put information(3)
    consumer:after get information(3)
    

    假若在用信箱mailbox的同时使用事件event的话,就可以实现线程间的完全同步了。

    program automatic synchronized;
        class producer;
            mailbox mbx;
            event handshake;
            
            function new(mailbox mbx, event handshake);
                this.mbx = mbx;
                this.handshake = handshake;
            endfunction
            
            task run();
                for(int i=1; i<4; i++) begin
                    $display("producer:before put information(%d)", i);
                    mbx.put(i);
                    @handshake;//等待事件handshake的触发
                end
            endtask
        endclass
        
        class consumer;
            mailbox mbx;
            event handshake;
            
            function new(mailbox mbx, event handshake);
                this.mbx = mbx;
                this.handshake = handshake;
            endfunction
            
            task run();
                repeat(3) begin
                    int i;
                    mbx.get(i)
                    $display("consumer:after get information(d%)", i);
                    ->handshake;//触发事件handshake
                end
            endtask
        endclass
        
        producer p;
        comsumer c;
        initial begin
            maibox mbx;
            mbx = new();
            event handshake;
            p = new(mbx, handshake);
            c = new(mbx, handshake);
            fork
                p.run();
                c.run();
            join
        end
    endprogram
    
    //producer和consumer两个线程之间实现了完全同步
    producer:before put information(1)
    consumer:after get information(1)
    producer:before put information(2)
    consumer:after get information(2)
    producer:before put information(3)
    consumer:after get information(3)
    

7.面向对象编程的高级技巧

  • OOP中类的变量称为属性(property),类中的任务或者函数称为方法(method),子程序的原型(prototype)是指明了参数列表和返回类型的第一行。

  • 类在使用extends关键词进行继承的时候,假若构造函数eg.function new(int value)中有参数传递,那么扩展类中的构造函数的第一行也必须调用基类的构造函数super.new(value)

  • 可以将一个派生类的句柄赋值给一个基类的句柄,并且不需要任何特殊的代码;但是相反的,假若将一个扩展类的句柄赋值给一个基类句柄时,最好使用$cast()进行类型匹配,类型不匹配时会返回0值。

  • 虚方法。虚方法需要在最前面添加virtual关键词进行声明,类在调用虚方法的时候,会根据对象的类型进行调用而不是句柄的类型。假若没有对方法进行virtual虚方法的声明,则SystemVerilog会根据句柄的类型而非对象的类型调用方法。

  • 抽象类和纯虚方法。SystemVerilog提供了2种构造方法来创建一个可以共享的基类:第一种是抽象类,即可以被扩展但是不能被直接实例化的类,使用virtual关键词进行修饰;第二种是纯虚方法,这是一种没有实体的方法原型,用关键词pure修饰,并且纯虚方法只能够在抽象类中定义。

    virtual class base_transaction;
        static int count = 1;
        int id;
        function new();
            id = count++;
        endfunction
        pure virtual function bit compare(input base_transaction to);
        pure virtual function base_transaction copy(input base_transaction to=null);
        pure virtual function void display(input string prefix="");        
    endclass
    
  • 回调。回调功能其实是在类的方法中预设了一些虚方法,这些虚方法的内部是没有代码实现的,所以在使用回调函数的时候需要对虚方法进行重写。SystemVerilog中的自建方法randomize()即是一种回调方法,其前后还包括了pre_randomize()post_randomize()

    //代码来自数字IC小站:SystemVerilog中的callback(回调)
    class abc_transactor;
    	virtual task pre_send(); endtask
    	virtual task post_send(); endtask
    
    	task xyz();
      	// Some code here
      	this.pre_send();
      	// Some more code here
      	this.post_send();
      	// And some more code here
    	endtask : xyz
    endclass : abc_transactor
    
    class my_abc_transactor extend abc_transactor;
    	virtual task pre_send();
      		...     // This function is implemented here
    	endtask
    
    	virtual task post_send();
      		...     // This function is implemented here
    	endtask
        ...
    endclass : my_abc_transactor
    
  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值