面向对象编程


前言

结构化编程语言:Verilog和C语言

面向对象编程(OOP)能创建复杂的数据类型,并使用这些数据类型的程序紧密结合。用户可以在更加抽象的层次建立测试平台和系统级模型,调用函数来执行一个动作而不是改变信号的电平。使用事务代替信号反转的时候,你就会更高效。而且测试平台和设计细节分开了,他们变得更加可靠,易于维护,在将来的项目中可以重复使用。

事务就是测试平台的焦点


一、OOP术语

  1. 类(class):包含变量和子程序的基本构建块。 对应verilog中的模块
  2. 对象(object):类的一个实例。verilog中,你需要实例化一个模块才能使用它。
  3. 句柄(handle):指向对象的指针(像一个对象的地址)。在verilog中,通过实例名在模块外部引用信号和方法。
  4. 属性(property):存储数据的变量。在verilog中,是reg或者wire类型的信号
  5. 方法(method):任务或函数中操作变量的程序性代码。在verilog 模块除了initial 块和 always块,还有任务和函数
  6. 原型(prototype):程序的头:包含程序名、返回类型和参数列表。程序体则包含了执行代码。
    在verilog中,通过创建模块并逐层例化;
    在OOP中创建类并例化创建的对象,你可以得到一个相似的层次结构。

1. 创建新对象

verilog:的实例名只可以指向一个实例,例化是在代码被编译的时候,是静态的,在仿真时不会变化,只有信号值在改变。
OOP:对象不断地被创建,也可以被释放,在运行测试平台的时候创建。

声明和使用一个句柄;
transaction tr; //声明句柄,初始化为特殊值null
tr = new();     //调用new函数创建transaction对象,为其分配空间,
//将变量初始化为默认值(二值变量为0,四值变量为X),并返回保存对象的地址。

2. 定制构造函数(constructor)

SV会为四值变量使用更多的内存,并会保存一定的信息,如对象的类型;

构造函数 :会分配内存,和初始化变量。new也是构造函数,new函数不能有返回值,返回一个指向类对象的句柄,其类型就是类本身。

2.1. 带有参数的new()函数:

class Transaction;
    logic [31:0] addr, crc, data[8];

    function new(logic [31:0] a=3, d=5);
        addr = a;
        foreach (data[i])
             data[i] = 5;
    endfunction
endclass
initial begin
   Transaction tr;
   tr = new(10);//addr=10, data采用默认值5
end

2.2 调用正确的new()函数

class Transaction;
...
endclass
class driver;
  Transaction tr;
  function new(); //driver 的 new 函数
      tr = new(); // transaction的new函数
  endfunction
class:driver

3. 将声明和创建分开

避免 使用 Transaction tr = new(); 要分开使用上文形式。
原因:你应该是希望先声明和初始化为特殊值null,再创建对象和分配空间,初始化变量; 但是合并在一起写,你不能控制这个顺序,如果忘记了使用automatic存储空间,构造函数将在仿真开始时,而非进入块的时候调用

4. new( ) 和new[ ] 区别

共同点:都申请内存并初始化变量
区别:
new[ ] 设置动态数组大小,通过写一个数字设置数组大小;
new() 函数创建一个对象, 通过使用参数设置对象的数值;

5. 为对象创建一个句柄

一个句柄可以指向多个对象;

Transaction tr1,tr2;
tr1 = new(); // 为第一个transaction 对象分配地址
tr2 = tr1;   // tr1 和 tr2 都指向该对象
tr1 = new(); // 为第二个transaction 对象分配地址

这样tr1指向第二个transaction对象,
tr2指向第一个transaction对象。
SV:在你需要的时候动态创建对象,能够创建任意数量的事务。
Verilog:固定的数组,数组能够容纳最大数量的事务; 模块的实例和他的名字是固定绑定在一起的,即使仿真过程中产生和自动注销的automatic变量,名字和内存总是绑定在一起的。

6. 对象的解除分配 deallocation

因为 一个事务产生并发送到DUT,得知发送事务成功,也统计到结果,那么这个事务就没什么用了。否则,长时间的仿真会将内存消耗尽,或者运行的慢。

一旦没有句柄指向对象,对象会自动消失。
SV不能回收一个被句柄引用的对象,而且在线程没有结束前也不能回收对象。

transaction t; // 创建一个句柄
t = new();     // 分配一个新的transaction
t = new();     // 分配第二个,并释放第一个t,因为第一个没有句柄指向它了
t = null;      // 解除分配第二个,实质在清除t句柄

7. 使用对象

使用对象的变量和子程序:
transaction t; // 声明一个句柄 
t = new();     // 创建一个transaction 对象
t.addr = 32'h42; // 给变量赋值
t.display();    // 调用子程序
和Verilog一样用“.”引用变量和子程序

OOP规定 只能通过对象的共有方法访问对象的变量,如get(), put() 。 因为直接访问变量会限制以后对代码的修改。
如果变量隐藏在无数的方法中,难以做到最大限度地控制所有变量,以产生最广泛地激励。
坚持变量公有化,以便测试平台在任何地方都可以访问他们。

8. 静态变量和全局变量

8.1 SV 在类中创建一个静态变量,该变量将被这个类所有实例共享,适用范围仅限于这个类。

class transaction;
     static int count = 0; // 已创建的对象地数目
     int id;  // 实例的唯一标志, 相当于对象的地址
     function new();
        id = count++; // 设置标志,count递增
     endfunction
endclass

transaction t1, t2;
initial begin
   t1 = new(); // 第一个实例,id = 0, count =1
   t2 = new(); // 第二个实例  id = 1, count =2 
   $display("Second id = %d, count=%d",t2.id, t2.count);
 end
 无论创建多少个transaction对象,全局变量count 只有一个,count保存在类中而非对象中,
 变量id不是静态的,所以每个transaction都有自己的id变量,
 这样就不需要为count创建一个全局变量。

当你打算建一个全局变量的时候,考虑创建一个类的静态变量。
一个类是最好是独立的,尽量减少外部引用。

8.2 通过类名访问静态变量

8.1中使用了句柄引用静态变量,
可以使用类名加上::,即类作用域操作符,引用静态变量

class transaction;
     static int count = 0; // 已创建的对象地数目
     ...
endclass

transaction t1, t2;
initial begin
....
   $display("Second id = %d, count=%d",t2.id, transaction::count);
 end

8.3 静态变量的初始化

  • 通常在声明时初始化。也可以用初始化块。
  • 不能在类的构造函数中初始化静态变量,因为每个新的对象都会调用构造函数。

8.4 静态方法

当类的每一个实例 都需要从同一个对象获取信息的时候。例如,transaction类可能需要从配置对象获取模式位。
如果

class transaction;
    static Config cfg; // 使用静态存储的句柄
    MODE_E mode;
    function new();
       mode = cfg.mode;
    endfunction
 endclass
 Config cfg;
 initial begin
      cfg = new(MODE_ON);
      Transaction::cfg = cfg;
      ...
  end

当静态变量增多后,可以在类中创建一个静态方法以用于读写静态变量,甚至在第一个实例产生之前读写静态变量。
显示静态变量的静态方法

class transaction;
  static Config cfg;
  static int count = 0;
  int id;
  // 显示静态变量的静态方法
  static function void display_statics();
     $display("Transaction cfg.mode = %s, count = %0d", cfg.mode.name(), count);
  endfunction
endclass
Config cfg;
initial begin
   cfg = new(MODE_ON);
   transaction::cfg = cfg;
   transaction::display_statics(); //调用静态方法
 end

9. 类的方法

类中的程序也叫方法,即类中的task和function
根据句柄类型调用正确的方法

类名 句柄名;
句柄名 = new(); // 创建类名的对象
句柄名.类中方法();//调用类名中的方法

类中的方法时默认自动存储的,不必担心忘记使用automatic修饰符

10. 在类之外定义方法

如果方法过程代码太长,无法在一页内读完整个类或者方法。
方法的原型定义放在类的内部,方法的过程代码放在类的后面定义。
例如:

class transaction;
   extern function void display();
endclass
   function void transaction::display();
   endfunction

11. 作用与规则

当前作用域
类应当在program或者 module外的package中定义。
绝对作用域以$root开始
名字作用域:

int limit;  // $root.limit
program automatic p;
    int limit;  // $root.p.limit
    class FO;
        int limit; //$root.p.Fo.limit
        function void print(int limit); // $root.p.Fo.print.limit
        endfunction
    endclass
 initial begin
     int limit = $root.limit;
endprogram

11.1 this是什么

使用一个变量名时,SV会在当前域寻找,接着在上一级作用域内寻找,直到找到变量为止。和Verlog相同。
但是如果你在类的很深的底层作用域,想明确的引用类一级的对象:this 可以高速SV。
使用this指针指向类一级变量
class Scope;
   string oname;
   function new(string oname);
        this.oname = oname; // 类变量oname=局部变量oname
   endfunction
endclass

12. 在一个类里使用另一个类

通过使用指向对象的句柄,一个类内部可以包含另一个类的实例。
Verilog中,一个模块内部包含另一个模块的实例,以建立设计的层次结构。 这样包含的目的是重用和控制复杂度。

Statistics类的声明
class Statistics;
    time startT, stopT; //事务的时间
    static int ntrans= 0; //事务的数目
    static time total_elapsed_time = 0;

    function time how_long;
      how_long = stopT-startT;
      ntrans++;
      total_elapsed_time += how_long;
    endfunction
    function void start;
       startT = $time;
     endfunction
 endclass
 可以在另一个类里使用 Statistics类
 封装Statistics类
 class transaction;
    bit [31:0] addr,crc,data[8];
    Statistics  stats; // 声明一个Statistics句柄
    function new();
       stats = new(); // 创建stats实例
    endfunction

    task create_packet();
         //填充数据
         stats.start(); //Statistics句柄调用Statistics的成员
         //传送数据包
    endtask
 endclass

12.1 类该做成多大

看类的使用频率以及类的例化次数。

12.2 编译顺序的问题

编译一个类时,里边包含着一个尚未定义的类。声明这个被包含的类的句柄将会引起错误。
你应该需要使用typedef语句声明一个类名。

typedef class Statistics; //定义低级别类
class Transaction;
    Statistics stats;  // 使用Statistics类
    ...
 endclass
class Statistics; // 定义Statistics类
....
endclass

13. 理解动态对象

在静态分配内存的语言,如Verilog,每个数据都有一个变量与之关联。
OOP中不存在这种对应关系。可能有很多对象,之定义少量句柄。很多次事务的对象,只有几个句柄。
对于保存在邮箱(mailbox)中的对象,句柄就是SV的一个内部结构。

13.1 将对象传递给方法

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

当你调用一个带有标量变量(不是数组,也不是对象)的方法并且使用ref关键词的时候,SV会传递该标量的地址,所以方法可以修改标量变量的值。

13.2 在任务中修改句柄

function void create(Transaction tr); // 错误,缺少ref,应该为 ref Transaction tr
     tr = new();
     tr.addr = 42;
     //初始化其他域
     ...
endfunction
Transaction t;
initial begin
    create(t); //创建一个transaction
    $display(t.addr);//失败,因为 t= null
end
create修改了参数tr,但是t.addr调用块中的句柄t仍为null。

13.3 在程序中修改对象

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

  1. 错误的发生器只创建了一个对象
task generateor_bad(int n);
  Transaction t;
  t = new(); // 创建一个新对象
  repeat (n) begin
        t.addr = $random(); // 变量初始化
        $display("Sending addr = %h", t.addr);
        transmit(t); //  将它发送给DUT
   end
 endtask
 仅创建了一个Transaction, 每循环一次,generator_bad在发送事务对象的同时又修改了它的内容。display会显示很多个addr值,但是发送的其实都是一个相同的addr值。
  1. 正确的发生器,创建了多个对象
task generateor_good(int n);
  Transaction t;
  repeat (n) begin
        t = new(); // 创建一个新对象
        t.addr = $random(); // 变量初始化
        $display("Sending addr = %h", t.addr);
        transmit(t); //  将它发送给DUT
   end
 endtask

13.4 句柄数组

写测试平台时,需要保存并且引用许多对象。
创建句柄数组,让每个句柄都指向一个对象。
tarray 就是句柄数组

task generator();
  transmit tarray[10];
  foreach (tarray[i])
      tarray[i] = new; // 创建一个句柄为i的对象
      transmit(tarray[i]);
endtask

14 对象的复制

有时需要复制要给对象,防止对象的方法修改原始对象的值,或者在一个发生器中保留约束。

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

使用new复制一个对象:创建了一个对象,并复制了现有对象的所有变量。但是你已经定义的任何new()函数都不会被调用。

**使用new复制一个简单类**
class transaction;
    bit [31:0] addr,crc,data[8];
endclass
transaction src, dst;
initial begin
    src = new;     // 创建第一个对象
    dst = new src; // 使用new操作符进行复制
 end
 这是shallow copy, 复制了对象的值,只有最高级一级的对象被new操作符复制,下层的对象不会被复制。
**使用new复制一个复杂类**
class transaction;
    bit [31:0] addr,crc,data[8];
    static int count=0;
    int id;
    Statistics stats; // 指向Statistics对象的句柄
    function new;
       stats = new(); //构造一个新的Statistics对象
       id = count++;
    endfunction
endclass
transaction src, dst;
initial begin
    src = new();     // 创建一个transaction对象
    src.stats.startT = 42;
    dst = new src; // 使用new操作符进行复制src到dst
    dst.stats.startT = 96; //改变dst和src的stats
 end

在这里插入图片描述
在这里插入图片描述
问题:src和dst的id都为0,是因为new操作符只复制了对象的变量和句柄的值,而不是重新构建一个对象,两个transaction对象都指向同一个Statistics对象。

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

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

class transaction;
    bit [31:0] addr,crc,data[8]; //没有Statistic句柄
    function transaction copy;
         copy = new(); // 创建目标对象
         copy.addr = this.addr; // 填入数值
         copy.crc = this.crc;
         copy.data = this.data // 复制数组
    endfunction
endclass
transaction src, dst;
initial begin
    src = new();    //创建第一个对象
    dst = src.copy; //复制对象
end

14.3 编写自己的深层复制函数

非简单地类,调用类所包含的所有对象地copy函数,左右给深层地拷贝。

typdef class Statistics;
class transaction;
    bit [31:0] addr,crc,data[8]; 
    static int count=0;
    int id;
    Statistics stats; // 指向Statistics对象的句柄
    function new;
       stats = new();
       id = count++;
    endfunction
    function transaction copy;
         copy = new(); // 创建目标 
         copy.addr = this.addr; // 填入数值
         copy.crc = this.crc;
         copy.data = this.data // 复制数组
         copy.stats = stats.copy(); //调用Statics::copy函数, 该copy函数调用了构造函数new(),所以每个对象都有一个唯一的id,
         id = count++;
    endfunction
endclass

class Statistics;
    time startT,stopT; // 
    ...
    function Statistics copy();
        copy = new();
        copy.startT = startT;
        copy.stopT = stopT;
     endfunction
endclass

transaction src, dst;
initial begin
    src = new();    //创建第一个对象
    src.stats.startT = 42; //设置其实时间
    dst = src.copy(); //深层复制对象
    dst.stats.startT = 96; //进改变dst地stats值
end

在这里插入图片描述
id = 0, id = 1;

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

某协议如ATM协议,每次传输一个字节地控制或者数据值。
送出一个transaction之前,需要将对象中的变量打包成一个字节数组。
在接收到一个字符串之后,也需要将它们解包到一个transaction对象中。
你不能将整个对象送入流操作符,因为这样会包含所有地成员,包括 数据成员 和 其他额外信息,如时间戳和你不想要打包的自检信息。

**含有pack 和 unpack 函数的 transaction 类**
class transaction;
    bit [31:0] addr, crc, data[8];
    static int count = 0;
    int id;
    function new();
         id = count++;
    endfunction
    function void display();
       $write("Tr: id = %0d, addr = %x, crc = %x", id, addr, crc);
       foreach (data[i])
            $write("%x",data[i]);
       $display;
    endfunction
    function void pack(ref byte bytes[40]);
         bytes = {>>{addr,crc,data}};
    endfunction
    function transaction unpack(ref byte bytes[40]);
         {>>{addr, crc, data}} = bytes;
    endfunction
endclass: transaction
使用pack和unpack函数
transaction tr,tr2;
byte b[40];  // addr+crc+data = 40 字节

initial begin
    tr = new();
    tr.addr = 32'ha0a0a0a0; //填满对象
    tr.crc = '1;
    foreach (tr.data[i])
        tr.data[i] = i;
    tr.pack(b); //打包对象到字节数组
    foreach (b[i])
       $write("%h", b[i]);
    $display;
    
    tr2 = new();
    tr2.unpack(b);
    tr2.display();

15 公有和私有

OOP的核心概念是 把数据和相关的方法封装成一个类。
数据默认是类私有的,防止其他类对内部数据成员的随意访问。
但在SV中,所有成员都是共有的,除非为 local 或 protected。因为这样可以保证你的成员对DUT行为的最大程度的控制。例如,CRC公有 将使你能够轻易地往DUT中注入错误。被CRC是局部的,需要编写额外的代码来避开数据隐藏机制,最终使测试平台变得更大更复杂。

16 建立一个测试平台

在这里插入图片描述

总结

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值