第五章:面向对象编程基础

如果一个程序员所接触的第一门语言是Verilog的话,那么这一章我们讲述的面向对象编程OOP(object-oriented programming)就要改变Verilog这种面向结构化的编程语言的思路。Verilog语言中没有数据结构,只有向量和数组。
面向对象编程OOP使用户能够创建复杂的数据类型,并且将它们跟使用这些数据类型的程序紧密地结合在一起。

5.1概述

面向对象编程(OOP)使用户可以在更加抽象的层次上建立测试平台和系统级模型,通过调用函数来执行一个动作而不是改变信号的电平。当使用事务处理器来代替信号翻转的时候,我们就可以变得很高效。
测试平台跟设计细节分开了,它们变得更加可靠,更加易于维护,在将来的项目中可以重复使用。
传统的测试平台要做的操作:创建一个事务、发送、接收、检查结果、然后产生报告。而在OOP中,你需要重新考虑测试平台的结构,以及每部分的功能。发生器(generator)创建事务并且将它们传给下一级,驱动器(driver)和设计进行会话,设计返回的事务将被监视器(monitor)捕获,计分板(scoreboard)会将捕获的结果跟预期的结果进行比对。因此,测试平台应该分成若干块(block),然后定义它们相互之间如何通信。

5.2类(class)

类封装了数据和操作这些数据的子程序。下面的例子是一个通用数据包类。这个数据包包含了地址、CRC和一个存储数值的数组。

      class Transaction;
                bit [31:0] addr,crc,data[8];
       
              function void display;
                       $display("Transaction:%h",addr);
               endfunction:display
      
               function void calc_crc;
                         crc=addr^data.xor;
                endfunction:calc_crc
       endclass:Transaction

类可以定义在program module package中,或者在这些块之外的任何地方。类可以在程序和模块中使用。可以使用SystemVerilog的包(package)将一组相关的类和类型定义捆绑在一起。

5.3 OOP术语
  • 类(class):包含变量和子程序的基本构建块。
    Verilog中与之对应的是模块(module)。
  • 对象(object):类的一个实例。
    Verilog中,你需要实例化一个模块才能使用它。
  • 句柄(handle):指向对象的指针。一个OOP句柄就像一个对象的地址。
    Verilog中,你通过实例名在模块外部引用信号和方法。
  • 属性(property):储存数据的变量。
    Verilog中,就是寄存器(reg)或者线网(wire)类型的信号。
  • 方法(method):任务或者函数中操作变量的程序性代码。
    Verilog中的任务和函数。
  • 原型(prototype):程序的头,包括程序名、返回类型和参数列表。程序体则包含了执行代码。

    5.4 创建新对象

    Verilog和OOP中,都存在例化的概念。Verilog的例化是静态的,就像硬件一样在仿真的时候不会变化,只有信号值在改变。而SystemVerilog中,激励对象不断地被创建并且用来驱动DUT,检查结果。最后这些对象所占的内存可以被释放,以供新的对象使用。类在使用之前必须例化,句柄可以指向很多对象,但是一次只能指向一个。
    使用new函数来分配并初始化对象。

         Transaction tr;   //声明一个句柄
           tr=new();   //为一个Transaction对象分配空间
  • 声明句柄tr的时候,它被默认初始化null。
  • 调用new()函数来创建Tansaction对象,并分配空间,将变量初始化为默认值(二值变量为0,四值变量为X),并返回保存对象的地址。

    5.4.1 定制构造函数(constructor)

    用户可以自行设置初始值。
    设置成固定的值

          class Transaction;
                  bit [31:0] addr,crc,data[8];
    
                function new;
                         addr=3;
                    foreach(data[i])
                            data[i]=5;
                 endfunction
           endclass:Transaction

    你也可以设计成可以选择使用默认值或者固定值。

          class Transaction;
                  bit [31:0] addr,crc,data[8];
    
                function new(logic[31:0] a=3,d=5);
                         addr=a;
                    foreach(data[i])
                            data[i]=d;
                 endfunction
    
               initial 
                     begin
                    Transaction tr;
                       tr=new(10);    //addr=10,data=5(默认值)
                    end
           endclass:Transaction
  • addr和data的默认值是3和5,可以对它们的值进行改变,没有明确指出时就使用默认值。
  • 将声明和创建分开。
  • new[]和new(),两者都是申请内存并初始化变量。
    不同之处:1、new()函数仅创建了一个对象,new[]则建立一个含有多个元素的数组。2、new()可以使用参数设置对象的数值,而new[]只需要使用一个数值来设置数组的大小。

    5.4.2 为对象创建一个句柄

    通过声明一个句柄来创建一个对象。在一次仿真中,一个句柄可以指向很多对象,但是一次只能指向一个。

         Transaction t1,t2;
         t1=new();
         t2=t1;
         t1=new();

    t2指向第一个Transaction对象,t1指向第二个Transaction对象。

    5.4.3 对象的解除分配
          t=new();   //分配第一个Transaction对象
          t=new();   //分配第二个Transaction对象,释放第一个
          t=null;      //解除分配第二个
    5.4.4使用对象

    如果已经分配了一个对象,可以使用“.”符号来引用变量和子程序。

         Transaction t;
          t=new();
          t.addr=32'h42;    //设置变量的值
          t.display();   //调用一个子程序
    5.5 静态变量和全局变量

    每个对象都有自己的局部变量,这些变量不和任何其他对象共享。如果有两个Transaction对象,则每个对象都有自己的局部变量。但是有时候你需要一个某种类型的变量,被所有的对象所共享。

    5.5.1 简单的静态变量

    在SystemVerilog中,可以在类中创建一个静态变量该变量将被这个类的所有实例所共享,并且它的使用范围仅限于这个类。

         class Transaction;
                 static int count=0;
                  int id;
    
                function new();
                         id=count++;
                 endfunction
           endclass:Transaction
    
            Transaction t1,t2;
               initial 
                    begin
                          t1=new();
                          t2=new();
                          $display("Second id=%d,count=%d",t2.id,t2.count);
                     end
  • t1,第一个实例,id=0,count=1;
  • t2,第二个实例,id=1,count=2;
  • count保存在类中而不是对象中,对t1和t2都是同一个count;
  • id不是静态变量,t1和t2都有不同的id值。
  • 通常在声明时初始化变量。
    SystemVerilog不能输出对象的地址,但是可以考虑创建ID域来区分对象。考虑创建一个类的静态变量,它能自给自足,对外部的应用越少越好。

    5.5.2 通过类名访问静态变量

    我们可以使用在类名加上::来引用静态变量。

         class Transaction;
                 static int count=0;
        endclass
    
               initial 
                    begin
                         run_test();
                          $display("%d transaction were created",Transaction::count);
                     end
    5.6 类的方法

    类中的程序也称为方法,也就是在类的作用域内定义的内部task或者function。

          class Transaction;
                 bit [31:0] addr,crc,data[8];
    
                 function void display();
                        ......
                  endfunction
           endclass:Transaction
    
             Transaction t;
               initial 
                    begin
                          t=new();
                          t.display();    //调用Transaction的方法
                     end
    5.7 在类之外定义方法

    为了增强程序的可读性,我们一般讲class搭配endclass在同一页面中。但是如果内容太多的话,可以将方法名和参数放在类的内部,而方法的程序体(过程代码)放在类的定义后面。

               class Transaction;
                     bit [31:0] addr,crc,data[8];
                     extern function void display();
               endclass:Transaction
    
              function void Transaction::display();
                        ......
               endfunction
  • 在class定义里加入关键词extern
  • 将function移到类的后面,并注意function void Transaction::display()的命名格式。

    5.8 作用域规则

    在编写测试平台的时候,需要创建和引用许多变量。SystemVerilog采用与Verilog基本相同的规则。
    作用域是一个代码块,例如一个module、program、task、function、class、begin-end块。for和foreach循环自动创建一个块。
    类应当在program和module外的package中定义。

             package Mistake;
               class Bad;
                   logic[31:0] data[];
                      function void dispaly;
                            ......
                      endfunction
              endpackage
    
              program test;
                       int i;
                    import Mistake::*;
                       ......
              endprogram

    当你使用一个变量名的时候,SystemVerilog将会在当前作用域寻找,接着在上一级作用域内寻找,直到找到该变量为止。这一点与Verilog相似。
    这里介绍一种直接将局部变量赋给类一级变量的方法。

            class Scoping;
                   string oname;
                          function  new(string oname);   //function的局部变量oname
                                      this.oname=oname;    //将类变量oname=局部变量oname
                           endfunction
            endclass
  • 采用this直接将局部变量赋给类一级的变量。

    5.9 在一个类内使用另一个类

    通过使用指向对象的句柄,一个类内部可以包含另一个类的实例。这就如同Verilog中,在一个模块内部包含另一个模块实例,以建立设计的层次结构。

           class Statistics;
              .......
           endclass

           class Transaction;
             .......
           Statistics stats;   // 例化的类的句柄
                function new();
                    stats=new();
               endfunction
    
             task create_packet();
               .......
                stats.start();    //分层调用使用Statistics里的变量
             endtask
           endclass
  • 最外层的Transaction可以通过分层调用语法来调用Statistics类中的成员
  • 在上层构造函数中,完成对调用类的例化
    注意在调用类的过程中,我们通常有一个编译顺序的问题,如果调用的类在后面,则需要提前声明。如果上例中两个class的顺序颠倒一下,则需要声明typedef class Statistics。

    5.10 理解动态对象

    在OOP中,可能有很多对象,但是只定义了少量的句柄。一个测试平台在仿真过程中可能产生了数千次事务对象,但是仅有几个句柄在操作它们。如果你之前一直在用Verilog代码,你一定要习惯这种情况。

    5.10.1 将对象传递给方法

    当你调用方法的时候,传递的是对象的句柄而不是对象本身。

          task transmit (Transaction t);
                ......
          endtask
    
          Transaction t;
               initial 
                   begin
                        t=new();
                        t.addr=42;
                        transmit(t);
                     end
  • 初始化块先产生一个Transaction对象,并且调用transmit任务,transmit任务的参数是指向该对象的句柄。通过句柄,transmit可以读写对象中的值。
  • 如果transmit试图改变句柄,初始化块将不会看到结果,因为参数t没有ref修饰符。

        task transmit ( ref Transaction tr);
               ......
        endtask
    
          Transaction t;
               initial 
                   begin
                        t=new();
                        transmit(t);
                         $display(t.add); 
                     end
  • transaction修改了参数tr,使用ref关键词,否则的话,只是对tr做了修改,调用块中的句柄t仍为null。

    5.10.2 在程序中修改对象

    在测试平台中,一个常见的错误就是忘记为每个事物创建一个对象。

        task generator_bad(int n);
            Transaction t;
             t=new();
             repeat (n)
                       begin
                       t.addr=$random();//修改变量初始值
                       $display("%0h",t.addr);
                       transmit(t);  //将它发送到DUT
                       end
         endtask 
  • 上面的代码仅创建了一个对象,所以每一次循环generor_bad在发送事务对象的同时又修改了它的内容。
  • 当你运行这段代码的时候,display出不同的addr值,但是transmit的t都具有相同的addr值。
  • 为了避免这种错误,你需要在每次循环的时候创建一个新的Transaction对象。

      task generator_bad(int n);
            Transaction t;
             repeat (n)
                       begin
                        t=new();
                        t.addr=$random();//修改变量初始值
                       $display("%0h",t.addr);
                       transmit(t);  //将它发送到DUT
                       end
         endtask 
    5.10.2 句柄数组

    在写测试平台的时候,可能需要保存并且引用许多对象。你可以创建句柄数组,数组的每一个元素指向一个对象。

           task generator();
              transmit tarray[10];
               foreach (tarray[i])
                       begin 
                           tarray[i]=new();
                            transmit(tarray);
                        end
              endtask         
  • tarray数组是由句柄组成而不是对象,所以在使用时,必须像普通句柄创建对象一样。

    5.11 对象的复制

    可以使用简单的new函数的内建拷贝功能,也可以为更复杂的类编写专门的对象拷贝函数。下面我们就来一一介绍一下。

    5.11.1 使用new操作符复制一个对象

    使用new复制一个对象简单而且可靠,它创建了一个新的对象,并且复制了现在对象的所有变量。

        class Transaction;
                  bit[31:0] addr,crc,data[8];
         endclass
    
       Transaction src,dst;
                initial 
                     begin
                         src=new();
                         dst=new src;
                     end
  • 这是一种简易的复制,只有最高一级的对象被new操作符复制,下层的对象都不会被复制。
    如果Transaction类包含了一个指向Statistics类的句柄,那么我们又该如何处理呢?

            class Transaction;
                  bit[31:0] addr,crc,data[8];
                  static int count=0;
                  int id;
                  Statistics stats;
    
                function new;
                        stats=new();
                        id=count++;
                endfunction
            endclass
    
            Transaction src,dst;
                initial 
                     begin
                          src=new();
                          src.stats.startT=42;
                         dst=new src;
                         dst.stats.startT=96;   //src.stats.startT=dst.stats.startT=96
                     end
  • Transaction对象被拷贝,但是Statistics对象没有被复制;
  • 两个Transaction对象都具有相同的id值;
  • 两个Transaction对象都指向同一个Statistics对象。

    5.11.2 编写自己的简单复制函数

    如果有一个简单的类,它不包含任何对其他类的引用,那么编写copy函数非常容易。

          class Transaction;
                  bit[31:0] addr,crc,data[8];
    
                function Transaction copy;
                        copy=new();
                        copy.addr=addr;
                        copy.crc=crc;
                        copy.data=data;
                endfunction
              endclass

              Transaction src,dst;
                initial 
                     begin
                          src=new();
                         dst=src.copy;
                   end 
    5.11.3 编写自己的深层次复制函数

    你自己的copy函数需要确保所有用户域(id)保持一致。创建自定义copy函数的最后阶段需要在新增变量的同时更新它们。

          class Transaction;
                  bit[31:0] addr,crc,data[8];
                   static int count=0;
                  int id;
                  Statistics stats;
    
                function new;
                        stats=new();
                        id=count++;
                endfunction
    
                function Transaction copy;
                        copy=new();
                        copy.addr=addr;
                        copy.crc=crc;
                        copy.data=data;
                        copy.stats=stats.copy();
                       id=count++;
                endfunction
            endclass

               Transaction src,dst;
                initial 
                     begin
                          src=new();    //id=0
                         dst=src.copy;  //id=1
                   end 
  • 不仅复制了Transaction,而且复制了Statistics,不同的Transaction对应不同的Statistics。

    5.12 使用流操作符从数组到打包对象,或者从打包对象到数组

    按照需要编写自己的pack函数,仅打包你所选的成员变量。

              class Transaction;
                  bit[31:0] addr,crc,data[8];
                   static int count=0;
                  int id;
    
                function new;
                        id=count++;
                endfunction
    
                function void pack (ref byte bytes[40]);
                         bytes={>>{addr,crc,data}};
                endfunction
    
                function void unpack (ref byte bytes[40]);
                        {>>{addr,crc,data}}=bytes;
                endfunction
              endclass

            Transaction tr,tr2;
            byte b[40];
    
                initial 
                     begin
                          tr=new();  //创建对象并填满数据
                          tr.addr=32'h0;    //id=0
                         tr.crc=32'h0;  //id=1
                         foreach(tr.data[i])
                                   tr.data[i]=i;
                        tr.pack(b);
    
                        tr2.unpack(b);
                   end

转载于:https://www.cnblogs.com/xuqing125/p/9107614.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值