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

如果一个程序员所接触的第一门语言是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

猜你喜欢

转载自www.cnblogs.com/xuqing125/p/9107614.html