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


    如果一个程序员所接触的第一门语言是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
  • 相关阅读:
    C# Process.Start()方法详解 .
    任务管理器
    20160113 js中选择多个check一块删除
    20160113 JS中CheckBox如何控制全选
    20151217JS便签
    20151216Repeater
    20151215单选按钮列表,复选框列表:CheckBoxList
    20151214下拉列表:DropDownList
    !!!SqlHelper 传智!
    !!! SQL 数据库开发基础 传智!
  • 原文地址:https://www.cnblogs.com/xuqing125/p/9107614.html
Copyright © 2020-2023  润新知