SV OOP
前言
SV和verilog的区别
HDL硬件描述语言 | OOP面向对象编程 | |
Verilog | SystemVerilog | |
模块定义 | module | class |
模块实例 | instance | object |
模块名称 | instance name | handle |
数据类型 | registers & wires | properties: variables |
索引 | 通过层次化的索引来找到结构中的设计实例 | 通过句柄来索引对象的变量和方法 |
例化 |
是静态的, 在编译链接时完成 |
动态的, 在任意时间点发生,更加灵活和节省空间 |
执行代码 |
behavioral blocks (always, initial,) task, function |
Methods: task & function |
模块间通信 |
ports or cross-module task calls |
calls, event, mailboxes,semaphores |
特性 |
分开处理数据结构和算法; |
通过封装的方式对数据进行组织和管理 |
面向对象编程
面向对象编程的优势
传统的编程:分开处理数据结构和算法。
面向对象编程:通过封装的方式对数据进行组织和管理。
面向对象基本概念
类:class,封装了数据和对数据的处理方法。
对象:object,类的实例。
类是由成员组成的,成员分为:
- 属性,properties,数据 或 变量;
- 方法,methods,task或 function;
OOP 三要素:
- 封装:封装类的成员和方法;
- 继承:允许对已经存在的类进行扩展;
- 多态:在运行时将数据和函数进行绑定 ;
类的设计原则
- 一个类的功能应该尽可能简单,不应该承担过多的职责,更不应该承担不符合他的职责,这在设计模式中称之为单一职责原则,SRP(single responsibility principle)
SystemVerilog 面向对象
定义类
class 在 SV 中属于软件范畴,所以不能出现硬件范畴的定义:
- 类的变量只能是变量类型:bit 等,不能出现寄存器或线网类型: reg, wire
- 类中不能出现 initial 或 always
- 定义类位置:module,interface,program,package,也就是所有的“盒子”中;
- 类的编译顺序:先编译基本类,再编译高级类
class Transaction; bit [31: 0] addr, crc, data[8]; //class properities function void display; //class method $display("Transaction: %h", addr) ; endfunction: display function void calc_crc: crc=addr^data. xor; endfunction: calc_crc
endclass: Transaction
实例化
Transaction tr1, tr2; // 声明句柄tr1, tr2, 此时 tr1 = tr2 = null tr1 = new(); // 创建对象 tr2 = tr1; // 此刻tr1和tr2指向同一个对象 tr1 = new(); // 创建第二个对象,并且由tr1指向
- 对象是存储空间,句柄是空间指针;
- 创建了对象之后,对象的空间位置不会改变,而指向该空间的句柄可以有多个;
- 声明一个句柄 tr:该句柄描指向一个 Transaction 类型的对象;当声明一个句柄时,它的初始值为null;
- 调用new()函数创建一个对象;
- 创建对象时,可以通过自定义构建函数来完成变量的初始化和其他操作;
- 构建函数new() 是系统预定义函数,不需要指定返回值,函数会隐式地返回例化后的对象指针;
- 构建函数也可以定义多个参数作为初始化时外部传入数值的手段。
对象内存空间
创建一个对象:
- 调用new函数时,系统会分配一块内存空间,用于存储对对象中的成员变量和方法;
- 将实例中的变量值初始化,默认情况下,二值逻辑变量初始值为0,四值逻辑变量的初始值位x;
- 返回存放实例的地址(指针pointer);
释放句柄指向的对象的内存空间:
- 如果没有句柄指向个对象, Systemverilog将释放该对象的内存空间;
- 当句柄指向一个对象时, SystemVerilog不会释放该对象的内存空间;
- 将句柄设置为null,将手动释放所有的句柄,重新new()一下,就会重新开辟内存空间。
静态属性
静态变量
- 在class中声明的变量默认类型为动态变量;其生命周期始于对象创建,终于对象销毁;
- 使用关键字 static 声明 class 内的变量时,则为静态变量;其生命周期始于编译阶段,贯穿整个仿真阶段;
- 如果在类中声明了静态变量,无论例化多少个对象,只可以共享一个同名的静态变量,因此类的静态变量在使用时需要注意共享资源的保护;
- static 变量的初始化:static变量通常在声明时初始化。不能在构造函数中初始化,因为每一个新的对象都会调用构造函数。
// 如果一个变量需要被其他对象所共享,如果没有OPP,就需要创建全局变量,这样会污染全局名字空间,导致你想定义局部变量,但变量对每个人都是可见的。 // 类中static变量,将被这个类的所有实例(对象)所共享,使用范围仅限于这个类。当你打算创建一个全局变量的时候,首先考虑创建一个类的静态变量。 class transaction; Static int count=0; Int id; Endclass Trasaction tr1,tr2; // Id不是静态变量,所以每个trasaction对象都有自己的id;count 是静态变量,所有对象只有一个count变量。
transactio::count; // class::var tr1.count; // obj.var
静态句柄
当类的每一个对象,都需要从同一个对象(另一个类)中获取信息的时候。如果定义成非静态句柄,则每个对象都会有一份copy,造成内存浪费。
静态方法
- 在class 中定义的方法默认类型是动态方法,通过关键字 static 修改其类型为静态方法
- 静态方法内可以声明并使用动态变量,但是不能使用类的动态成员变量
// 因为在调用静态方法时,可能并没有创建具体的对象,也没有为动态成员变量开辟空间;
// 因此在静态方法中使用类的动态成员变量时禁止的,可能会造成内存泄漏;
// 但是静态方法可能使用类的静态变量,因为静态方法和静态变量一样在编译阶段就已经为其分配好内存空间
类的封装
类的封装特性使得类可以根据需要来确定外部访问的权限级别,一般可以将变量声明为以下三种形式:
- public: 子类和外部均可以访问(默认);
- protected: 只有当前类或者子类可以访问,外部无法访问;
- local: 只有当前类可以访问,子类和外部均无法访问;
class Foo; int id; local int count; ... endclass Foo f1; f1 = new(); f1.id; f1.count; // 报错 // 默认情况下,通过 handle.id 就可以访问实例的成员变量id,默认的变量申明都是public; // 成员变量 count 被声明成local类型,那么只有 class 内部方法可以访问count,其子类不能以继承的方式访问它,也不可以通过 f1.count 这种外部方式访问它,
// 否则VCS工具会报错:Could not find member 'num' in class 'xxx'。
类的继承
类的继承特性使得子类可以使用父类的成员变量(variable)或方法(method),当我们在子类实例上调用某一个变量或者方法时,分两种情况说明:
- 子类中没有同名的变量或方法,那么编译器会去父类中查找该变量或方法并且使用它。也就是说:在子类中可以直接使用父类的变量和方法;
- 子类中有同名的变量或方法,那么在子类中通过super.value或者super.method的方法才能使用父类的变量或方法。如果不使用super., 会默认使用子类中的变量或方法,那么相当于父类中的变量或方法会被子类覆盖掉。
构造函数
- 如果一个类没有定义new函数,那么默认的new函数会被自动定义;
- 子类在定义new函数时,首先调用父类的new函数即super.new(...);那么在调用子类new函数创建对象时,遵循如下顺序:
- 首先调用父类的new()函数,将父类变量初始化;
- 然后执行父类new()函数中自定义的代码;
- 接着将子类变量初始化:按定义时显示的默认值初始化,无默认值则不被初始化;
- 最后执行子类new()函数中自定义的代码;
成员覆盖
在父类和子类里,可以定义相同名称的成员变量和方法(形式参数和返回类型也相同),而在引用时,也将按照句柄类型来确定作用域。
super:强调同名的变量或方法是父类的
this:调同名的变量或方法是子类的,this.X表示当前类的成员X,如果当前类没有,则去父类寻找;而非同名的局部变量或者形式参数
当引用某一变量或者方法时,如果没有用 super 或者 this 的方式指明作用域,则会根据由近到远的原则查找该变量和方法:
- 查看变量/方法是否是函数内部定义的变量/方法;
- 查看变量/方法是否是当前类定义的变量/方法;
- 查看变量/方法是否是父类定义的变量/方法;
类型转换
静态转换:在需要转换的表达式前加上单引号即可,该方式不会对转换值做检查;如果转换失败,无从得知
动态转换:使用系统函数$cast( tgt, src)做转换
- 向下转换:父类句柄转换为子类句柄时,需要使用$cast() 函数进行转换,否则会出现编译错误
- 向上转换:将子类句柄赋值给父类句柄时,编译器则认为赋值是合法,但是分别利用子类句柄和父类句柄调用相同对象的成员时,将可能有不同表现。
显示转换:静态和动态转换都需要操作符号或者系统函数介入,称之为显示转换
隐式转换:不需要进行转换的一些操作,称之为隐式转换;例如,赋值语句右侧是4位矢量,左侧是5位的矢量,隐式转换会先做位宽扩展,再做赋值
父类子类句柄转换
- 父类句柄赋值给子类句柄并不是总是非法的
- SV编译器对这种直接赋值的做法是禁止的,也就是说无论父类句柄是否真正指向一个子类对象,赋值给子类句柄时,编译都将出现错误
- 因此需要使用 $cast() 来实现句柄的动态转换
- $cast() 会检查句柄所指向的对象类型,不仅仅检查句柄本身,一旦源对象和目的句柄是同一类型,或者是目的句柄的扩展类,$cast() 函数执行会成功,返回 1, 否则返回 0
类的多态
静态绑定:在编译阶段就可以确定下来调用方法所处作用域的方式称之为静态绑定
动态绑定:在调用方法时,会在运行时来确定句柄指向对象的类型,在动态指向应该调用的方法
虚方法
虚方法是多态的主要特征。
虚方法:用父类句柄指向子类对象,然后通过父类的句柄调用实际子类的成员方法。
如果父类和子类中有同名的 method,且父类中的method声明为virtual,那么当通过 指向子类对象的父类句柄调用method时,SV的虚方法重载机制会检测到句柄指向的对象是子类,从而只调用子类中的task/function。
虚方法的应用
父类句柄指向子类对象 的常见场景如下:
- 在构建上层框架(比如方法学)时,为了通用性,很多句柄都是用父类对象声明;
- 在子类中调用一些上层的方法时,往往会有参数传递,参数传递时容易发生隐式转换,即将子类对象的句柄赋给父类句柄,从而使得父类句柄指向了子类对象;
- 当用户使用上层框架中的方法时,很容易将自己创建的子类对象的句柄赋给上层框架中的父类句柄,从而使得父类句柄指向了子类对象;
虚方法的不足
一旦在父类中定义了某虚方法,那么在子类中实现此方法时,要求子类方法和父类虚方法的函数名,参数名都要一样,而且不能增删参数,否则多态就不适用了;
因此这要求在构建父类虚方法时必须提前做好计划,全面考虑子类实现此方法时的不同情况。
虚方法使用
- 在为父类定义方法时,如果该方法日后可能会被覆盖或者继承,那么应该声明为虚方法
- 虚方法如果要定义,尽量定义在底层父类中。如果virtual 是声明在类继承关系的中间层类中,那么只有从该类中间类到其子类的调用会遵循动态查找,而最底层类到中间类的方法仍然会遵循静态查找
- 虚方法通过virtual声明,只需要声明一次即可。其子类可以声明,也可以不需要声明
- 虚方法的继承需要遵循相同参数和返回类型,否则子类定义的方法会归为同名不同参数的其他方法
句柄转换
- 尽管通过虚方法的声明使得 "在指定子类对象的父类句柄上调用子类方法时,可以正确地找到该方法”,但是,"指向子类对象的父类句柄"仍然有一些变量和方法无法通过虚方法来解决索引问题:
- 父类没有定义,只有在子类中定义了的方法; ( 虚方法只能解决父类和子类同时存在method的问题)
- 父类没有声明,只有在子类中声明了的变量; ( 虚方法只针对method)
- 父类和子类同时声明了的变量; ( 虚方法只针对method)
- 通过$cast(target, source)方法解决上面的三个问题。$cast(target, source)执行成功的前提是:"source指向的对象"和句柄target是同一类型,或者"source指向的对象"是句柄target的扩展类。
- 因此source一般是"指向子类对象的父类句柄",target一般是"用子类声明的句柄",完成转换后,就可以通过target句柄来访问子类的变量或方法。
实例
class axi_sbx_item extends uvm_object; typedef axi_sbx_item this_type_t; this_type_t original_item; this_type_t split_item; function void split(); if(!$cast(this.split_item, this.clone())) `uvm_error("split", "cast failed!") ... this.split_item.original_item = this; endfunction endclass // this指axi_sbx_item类创建的对象; // 由于clone()是uvm的方法,uvm事先不知道axi_sbx_item的存在,因此this.clone()必定返回"指向子类对象的父类句柄" // 而this.split_item是用子类声明的句柄,执行$cast(this.split_item, this.clone())后,this.split_item就指向了clone出的对象;
一个类的多态的例子
class base_test; int data; int crc; virtual function base_test copy(); base_test t = new(); // 首先创建父类对象,由于在父类中实现,因此该对象类型为base_test; copy_data(t); return t; endfucntion virtual function void copy_data(base_test t); t.data = data; t.crc = crc; endfunction endclass class my_test extends base_test; int id; function base_test copy(); // 为了实现虚方法重载,子类方法/参数的名字和类型要与父类保持一致,因此被迫声明为base_test类型; my_test t = new(); // 首先创建子类对象,在创建子类对象时,会自动调用super.new()方法; copy_data(t); return t; endfucntion function void copy_data(base_test t); // 为了实现虚方法重载,子类方法/参数的名字和类型要与父类保持一致,因此被迫声明为base_test类型; my_test h; super.copy_data(t); $cast(h, t); h.id = id; endfunction endclass module tb; my_test m_t1; my_test m_t2; initial begin m_t1 = new(); $cast(m_t2, m_t1.copy()); // copy 返回的是一个父类句柄,所以必须要做转换 ...... end endmodule // 相比于UVM中通过Field Automation提供了比较简单的对象复制,SV中需要我们人工定义copy()方法来实现对象复制,上面的例子就是SV中用于复制对象的代码; // 将成员复制函数copy_data()和新对象生成函数copy()分为两个方法,便于子类的继承和方法的复用; // 从自顶向下构建copy()/copy_data()方法的角度来看,首先我们在父类中定义copy()/copy_data()方法,实现父类新对象的生成以及父类成员变量的复制。但是为什么父类的copy()/copy_data()方法要声明为virtual呢 // 如果想在"指向子类对象的父类句柄"上调用copy()/copy_data()方法来复制对象,必须要将父类的copy()/copy_data()方法声明为virtual,否则子类的变量无法复制; // 一旦将父类的copy()/copy_data()方法声明成virtual,麻烦也就接踵而至,因为虚方法要求子类方法/参数的名字和类型必须与父类一致,因此子类方法的类型也必须声明为base_test; // 那么子类在调用copy()方法时,返回一个"指向子类对象的父类句柄",必须通过$cast()方法实现句柄转换。
对象拷贝
赋值
Packet p1, p2; p1 = new; p2 = p1; // 声明变量和创建对象是两个过程,也可以一步完成 // p2 和 p1 指向同一个对象,句柄的赋值
浅拷贝
对象的拷贝无法通过 “=” 来实现,这一操作是句柄的赋值,而不是对象的拷贝
参考上面实例:多态的实例
-
- 将成员拷贝函数分为copy_data 和 对象生成函数 copy 分为两个方法,使得子类继承和方法复用更容易
- 为了保证父类和子类的成员均可以完成拷贝,将拷贝方法声明为虚方法,且遵循只拷贝该类的域成员的原则,父类的成员拷贝应由父类的拷贝方法完成
- 在实现copy_data 过程中,应该注意句柄的类型转换,保证转换后的句柄可以访问类成员变量
Packet p1, p2; p1 = new; p2 = new p1; // 在创建p2 时,将从p1 拷贝其成员变量和句柄等,该方式称为浅拷贝
回调函数:钩子函数
理想的验证环境是在被移植做水平复用或者垂直复用时,应该尽可能少地修改模块验证环境本身,只在外部做少量的配置,或者定制化修改就可以嵌入到新的环境中
实现方法:
-
- 一方面,通过顶层环境的配置对象自顶向下进行配置参数传递
- 另一方面,在测试程序不修改原始类的情况下注入新的代码
修改stimulator的行为时,有两种选择:
-
- 一种选择:修改父类,但针对父类的会传播到其他子类
- 另一种选择:在弗雷定义方法时,预留回调函数入口,使得在继承的子类中填充回调函数,就可以完成对父类方法的修改
回调函数使用
- 预留回调函数入口
- 定义回调函数
- 例化及添加回调类实例
参数化的类
提高代码复用率
class mailbox #(type T=int); local T queue[$]; task put(input T i ); queue.push_back(i); endtask task get (ref T o); wait(queue.size()>0); o=queue.pop_front(); endtask task peek(ref T o); wait(queue.size(0>0); o=queue[0]; endtask endclass