SV学习记录(五)

SV学习记录(五)

目录

5.1 面向对象编程(OOP)

5.2 类

5.3 OOP中的术语

5.4 创造新对象

创建handle

定制new()函数

new()和new[]

a handle on objects

5.5 释放object

5.6 使用object

5.7 静态vs全局变量

简单静态变量

静态变量初始化

静态方法(static methods)

5.8 在class外创建method

5.9 作用域

this是什么?

5.10 在一个class中用另一个class

一个class该有多大?

编译顺序问题

5.11 理解动态对象

传递给method的handle、object

传递一/多个object

handle数组

5.12 复制object

用new复制object

5.13 public vs local

5.14 测试平台


5.1 面向对象编程(OOP)

OOP要求我们更多地关注名词而非动词。具体来讲,传统的测试强调我们要进行的操作,比如产生信号,发送等等。而在OOP中我们应该更在意整体的结构,每一块的功能,以及每一块之间如何相互通信。

5.2 类

什么是类?class...endclass就是类。类包含数据和操作数据的函数。

Sample 5.1 Simple transaction class 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以及这些地方之外都可以。

在哪里用类?module, program

如果文件比较少,你可以把类分别定义在不同文件中;但如果文件较多,应该把相关的类和声明放到同一个package中,比如把都是SCSI/ATA事务的放到一起,把不相关的分散在各自的文件中。

5.3 OOP中的术语

class(类):包含变量和子程序的基本块。与v中的module相对应。

object(对象):类的例化。和v中对module的例化是一样的。

handle(句柄):指向object的指针。存储着object的address。

property(属性):存储数据的变量。对应v中的reg/wire。

method(方法):task/function中操作变量的代码。

prototype(原型):程序头,包含名称,类型,参数列表。程序体包含可执行的代码。

object就像是具体的房子。而class就像房子各个部分的蓝图,每个class能造出一部分。handle是房子的地址。房子里有灯,有开关灯的电路。就像property和method。

5.4 创造新对象

v的module在编译的时候就被例化了,而sv中的class只有在被testbench用到的时候才会被例化。

v的例化是静态的,硬件不发生改变,sv的object在被用到时创建,不用时释放,以节省资源。

v的顶层文件不需要例化,而sv的class必须例化了才能用。

v的例化名只会指向一个模块,而sv的handle可以指向多个object,但同一时间只能指向一个。

创建handle

下面声明了一个handle,指向类型为Transaction的object,并用new()为它初始化。

声明完,tr被初始化为null。new为它分配了地址和默认值,2-state赋0,4-state赋X。

Sample 5.2 Declaring and using a handle Transaction tr; // Declare a handle tr = new(); // Allocate a Transaction object

定制new()函数

我们可以按照自己的想法来定制new()函数,自己决定初始值。

自己写的function也可以没有传入值,只执行功能。

Sample 5.4 A new() function with arguments 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] = d; endfunction endclass ​ initial begin Transaction tr; tr = new(10); // data uses default of 5 end

如果每个类都有自己独特的new(),编译器怎么知道调用哪个?答案是根据左侧的句柄类型,调用对应类里的new()

Sample 5.5 Calling the right new() function class Transaction; ... endclass : Transaction ​ class Driver; Transaction tr; function new();//define new for class Driver tr = new();// Call the Transaction new function endfunction endclass : Driver

建议把handle的声明和建构分开,即不要写成Transaction tr = new();

new()和new[]

new[]是在2.3节中为动态数组分配空间并初始化的函数。

它们的区别一是在于作用对象,二是new()可以调用参数。

a handle on objects

如果学过C,比较好理解。handle就是指针,能指向不同的object

Sample 5.6 Allocating multiple objects Transaction t1, t2; t1 = new(); // Allocate first Transaction object t2 = t1; // t1 & t2 point to it t1 = new(); // Allocate second Transaction object

类比:人就是object,如果你去参加会议,handle就是给你分配的名牌,上面写了你的名字,也可以擦除写别人的名字。有了名牌主办方才能记住你,给你分配位置,如果你的名牌没了,就得离开。

5.5 释放object

如果我们要用成百上千的事务,因为内存是有限的,用完总要把对应的内存释放,sv是怎么知道一个事务不再被使用的?答案是追踪指向这个object的handle,如果没有任何handle指向它,就把对应的内存空间释放。

Sample 5.7 Creating multiple objects Transaction t; // Create a handle t = new(); // Allocate a new Transaction t = new(); // Allocate a second one, free the first t = null; // Deallocate the second

sv中的handle相对于C, C++有两个特点:1.一个handle只能指向特定类型,不存在void handle。2.sv会自动执行内存回收,所以你使用的handle一定一直是有效的,不用担心它指向一个不存在的object。

如果有子程序有handle正在指向object,子程序尚未停止,这个object也不会被释放。

5.6 使用object

使用方法和module的引用方法一样。

Sample 5.8 Using variables and routines in an object Transaction t; // Declare a handle to a Transaction t = new(); // Construct a Transaction object t.addr = 32'h42; // Set the value of a variable t.display(); // Call a routine

在严格的OOP中,只能用公用函数进行操作,比如get(),put()等。这是因为直接访问property会限制以后对代码的修改。

5.7 静态vs全局变量

简单静态变量

如果我们想有一个能够被所有的特定class共享的variable,就在class内部创建static变量。无论给这个class创建了多少个object,都只会有一个count。

它类似于传统C中的全局变量,但是全局变量会被Transaction这个类以外的代码看见,这是我们不希望的。

Sample 5.9 Class with a static variable class Transaction; static int count = 0; // Number of objects created int id; // Unique instance ID function new(); id = count++; // Set ID, bump count endfunction endclass ​ Transaction t1, t2; initial begin t1 = new(); // 1st instance, id=0, count=1 t2 = new(); // 2nd instance, id=1, count=2 $display("Second id=%d, count=%d", t2.id, t2.count); end

用id是一个很好的办法,因为sv中不允许打印object的地址,我们可以通过写id判断当前执行到的object。

如果你只是单纯想查看count的值,也可以不用handle,直接用class name

Sample 5.10 The class scope resolution operator class Transaction; static int count = 0; // Number of objects created ... endclass ​ initial begin run_test(); $display("%d transaction were created", Transaction::count); // Reference static w/o handle end

静态变量初始化

初始化一定要在declaration的部分,不要放在构建的new()中。更详细的初始化,可以放在initial块中。

一定注意:静态变量的初始化要在第一个object创建之前。

静态方法(static methods)

method指的是定义在class中的task/function。method默认是automatic的。

如果我们现在有一个class1,但是它的所有实例化的配置方式依赖于另一个class2相同的object,这时handle也可以用static

Sample 5.11 Static storage for a handle class Transaction; static Config cfg; // A handle with static storage MODE_E mode; function new(); mode = cfg.mode; endfunction endclass ​ Config cfg; initial begin cfg = new(MODE_ON); Transaction::cfg = cfg; ... end

如果静态变量很多,也可以写一个专门用来读写静态变量的static function(也叫method),在实例化之前就可以读写。

static method不允许读写非静态变量。

Sample 5.12 Static method displays static variable class Transaction; static Config cfg; static int count = 0; int id; static function void display_statics();// Static method to display static variables. $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(); // Static method call end

5.8 在class外创建method

我们最好让所有代码都能展现在一页中,这样方便理解,但有的method很长,那么就可以把它定义在class之外的body中。

class中的method加上extern关键字,body中定义的时候直接用class中的property就可以

Sample 5.14 Out-of-block method declarations class Transaction; bit [31:0] addr, crc, data[8]; extern function void display(); endclass ​ function void Transaction::display(); $display("@%0t: Transaction addr=%h, crc=%h", $time, addr, crc); $write("\tdata[0-7]="); foreach (data[i]) $write(data[i]); $display(); endfunction

注意,默认值的定义必须放在class...endclass内,不能放在外部function中。

5.9 作用域

sv允许在未命名的begin...end块(如下面的initial块)中定义变量。

这里的问题是,我们在多个层次都定义了相同名称的变量,如果想没有歧义地引用,最好用绝对路径。但是如果我们在未命名地块中定义了一个变量,可能会导致全局的变量路径都发生改变。

Sample 5.16 Name scope int limit; // $root.limit program automatic p; int limit; // $root.p.limit class Foo; int limit, array[]; // $root.p.Foo.limit ​ function void print (int limit); // $root.p.Foo.print.limit for (int i=0; i<limit; i++) $display("%m: array[%0d]=%0d", i, array[i]); endfunction endclass initial begin int limit = $root.limit; // **see note above Foo bar; bar = new(); bar.array = new[limit]; bar.print (limit); end endprogram

建议在package中声明class,在使用到地最内层定义临时变量。

下面是常见错误,class在使用i的时候忘定义了,编译器会自然地向上找,使用program中的i。

Sample 5.17 Class uses wrong variable program test; int i; // Program-level variable class Bad; logic [31:0] data[]; // Calling this function changes the program variable function void display; // Forgot to declare i in next statement for (i=0; i<data.size(); i++) $display("data[%0d]=%x", i, data[i]); endfunction endclass endprogram

如果我们把class的定义放在package中,它就看不到program层的i了。

Sample 5.18 Move class into package to find bug package Mistake; class Bad; logic [31:0] data[]; // Will not compile because of undeclared i function void display; for (i = 0; i<data.size(); i++) $display("data[%0d]=%x", i, data[i]); endfunction endclass endpackage program test; int i; // Program-level variable import Mistake::*; ... endprogram

this是什么?

如果你在class的function中构建method的时候,传入值和class中的变量同名,可以用this来直指class层的变量

Sample 5.19 Using this to refer to class variable class Scoping; string oname; function new(string oname); this.oname = oname; // class oname = local oname endfunction endclass

5.10 在一个class中用另一个class

就像v中一个module能例化另一个module,构成层次化设计,sv中通过handle也可以在一个class中例化另一个class。

注意声明完一定要初始化了才能用,初始化最好放在new()中。

Sample 5.21 Encapsulating the Statistics class class Transaction; bit [31:0] addr, crc, data[8]; Statistics stats; // Statistics handle function new(); stats = new(); // Make instance of stats endfunction task create_packet(); // Fill packet with data stats.start(); // Transmit packet endtask endclass

当代码越来越多的时候,要及时思考重构代码层次,尽量使顶层能在一页放下,把相似的封装进更小的class中。

一个class该有多大?

一个class肯定不能太大,但也不应该太小。如果一个class只有一两个member,那你可能需要在顶层和底层之间来回跳转理解,这也是不好的。如果一个小class只用了一次,那可以考虑封装进父class中。

编译顺序问题

有时候从上向下编译,先编译到的class1用了其他class2,但是class2还没出现过,就会报错。

解决方式如下

Sample 5.22 Using a typedef class statement typedef class Statistics; // Define a lower level class ​ class Transaction; Statistics stats; ... endclass ​ class Statistics; ... endclass

5.11 理解动态对象

传递给method的handle、object

这里要区分好handle和object。

对于transmit,我们传了一个handle进去,这个handle是initial中handle的copy,如果你尝试在transmit中修改这个handle,比如写t=new(),不会对顶层handle产生影响,除非你写了ref Transaction t。handle指向的是object,这个object是供我们任意修改的。

Sample 5.23 Passing objects // Transmit a packet onto a 32-bit bus task transmit(Transaction t); CBbus.rx_data <= t.data; t.stats.startT = $time; ... endtask ​ Transaction t; initial begin t = new(); // Allocate the object t.addr = 42; // Initialize values transmit(t); // Pass object to task end

下面的定义方式就可以修改handle了

Sample 5.25 Good transaction creator task with ref on handle function void create(ref Transaction tr); ... endtask

传递一/多个object

下面展示了两种method的方法,method1是坏的,method2是好的。他们的区别只在那一句代码出现的位置。

为什么1坏2好?对于1,当我们调用这个class,执行repeat行为给DUT传递object时,虽然我们用random生成了多个值,但是注意,所有的handle都指向一个object,那么DUT就会收到多个请求,但是所有请求的内容一读都是一样的,值取决于最后往里写了什么。而method2就解决了这个问题,传递多个object进去。

Sample 5.27 generator creates many objects task generator(int n); Transaction t; t = new(); // method1 Create one new object repeat (n) begin t = new(); // method2 Create one new object t.addr = $random(); // Initialize variables $display("Sending addr=%h", t.addr); transmit(t); // Send it into the DUT end endtask

handle数组

可以通过handle数组传递多个object

Sample 5.28 Using an array of handles task generator(); Transaction tarray[10]; foreach (tarray[i]) begin tarray[i] = new(); transmit(tarray[i]); end endtask

注意,没有object数组这个东西,虽然我们用起来是object数组,但一定要记住,handle可能不指向任何一个object。

5.12 复制object

用new复制object

Sample 5.29 Copying a simple class with new class Transaction; bit [31:0] addr, crc, data[8]; endclass ​ Transaction src, dst; initial begin src = new(); // Create first object dst = new src; // Make a copy with new operator end

这种方式的特点是简单,但是问题是:如果class中有一个handle,那么通过new复制时候会创建一个新handle,但是指向相同的object

Sample 5.30 Copying a complex class with new operator class Transaction; bit [31:0] addr, crc, data[8]; static int count = 0; int id; Statistics stats; // Handle points to Statistics object ​ function new(); stats = new(); // Construct a new Statistics obecjt id = count++; endfunction endclass ​ Transaction src, dst; initial begin src = new(); // Create a Transaction object src.stats.startT = 42; dst = new src; // Copy src to dst with new operator dst.stats.startT = 96; // Changes stats for dst & src $display(src.stats.startT); end

上面代码的最终结果如下所示

5.13 public vs local

sv中默认可见性是公有,共有的含义是我们可以通过handle直接改变object的值。

传统OOP中默认为私有,私有需要通过函数来修改object的值,修改完会自动修改CRC(循环冗余校验,用于检验数据包是否出错),但sv本身就是为了检验的,所以CRC出错也可以检验DUT是否具有正确的解包能力。

5.14 测试平台

现在我们再来回看最开始的测试平台

其中environment class中每一个小方块就是一个类处理器(class transactor),test放在例化environment的program中,每个transactor的通过结构如下所示

Sample 5.38 Basic Transactor class Transactor; // Generic class Transaction tr; task run(); forever begin // Get transaction from upstream block ... // Do some processing ... // Send it to downstream block ... end endtask endclass

如果我们想把一个block的事务传递给下一个block,就需要调用对应的class,但之后我们会学到mailbox