SystemVerilog(4):class、packet
1、SV的类和对象
- 这个世界是由无数的类(class)和对象(object)构成的;
- 类是抽象的,是将相同的个体抽象出来的描述方式;
- 对象是实体,其具备独立行为能力,一个对象是万千世界的一粒沙;
- 具有相同属性和功能的对象属于同一类,不同的类之间可能由联系(继承关系),或者没有联系;
- 在 C 语言中,编程基于过程方法(function);在Verilog中,提供了笨拙的“类对象编程”的可能性,即在 module 中定义方法,而后调用 module 实例中的方法;
- 类的定义核心即是属性声明(property declaration)和方法定义(metod definition),所以类是数据和方法的自洽体(self-compatible),即可以保存数据和处理数据。
- struct 结构体只是单纯的数据集合,而 class 类则可以对数据做出符合需要的处理。
SV的一些OOP术语如下所示:
- 类(class):包含变量和子程序的基本构成块;
- Verilog中,与之对应的是模块(module);
- 对象(object):类的一个实例;
- Verilog中,你需要实例化一个模块才能使用它;
- 句柄(handle):指向对象的指针。
- Verilog中,通过实例名在模块外部引用信号和方法(A.B.sigX);
- 一个OOP句柄就像一个对象的地址,但是它保存在一个只能指向单一数据类型的指针中;
- 属性(property):存储数据的变量,不能是 reg 和 wire;
- Verilog中,就是寄存器(reg)或者线网(wire)类型的信号;
- 方法(method):任务或者函数中操作变量的程序性代码,不能包含过程快(always、initial);
- Verilog中,就是 initial、always、function、task;
- 原型(prototype):程序的头,包括程序名、返回类型和参数列表;
2、class的创建
2.1 简单的class
如下是一个简单的 class,里面包含了变量和函数。
class Transaction; //module默认是静态的,而class默认是动态的
bit [31:0] addr; //变量不能是reg和wire
bit [31:0] crc;
bit [31:0] data[8];
function void display; //不能出现always和initial
$display("Transaction: %h", addr);
endfunction:display
function void calc_crc;
crc = addr ^ data.xor;
endfunction:calc_crc
endclass:Transaction
2.2 new的使用
new( ) 函数被称为“构造函数”,其作用是对 class 里的变量进行初始化,它是系统预定义函数,不需要指定返回值,它会隐式地返回例化后的对象指针。上面的例子里没有 new( ) 函数,那么它将变量设置为默认数值:二值变量为0,四值变量为X。一个带有 new( ) 函数的 class 如下所示:
//================================================ 类的创建
class Transaction;
bit [31:0] addr;
bit [31:0] crc;
bit [31:0] data[8];
function new(); //对变量进行初始化
addr = 3;
foreach(data[i])
data[i] = 5;
endfunction
function void display;
$display("Transaction: %h", addr);
endfunction:display
function void calc_crc;
crc = addr ^ data.xor;
endfunction:calc_crc
endclass
//================================================ 类的使用
initial begin
//也可以简写为:Transaction tr=new();
Transaction tr; //声明句柄
tr = new(); //开辟空间
end
如果多个地方都对内部变量进行赋值,那么情况变得复杂一点,如下所示:
//================================================ 类的创建
class Transaction;
bit [31:0] addr = 'd16;
bit [31:0] crc;
bit [31:0] data[8];
function new(logic [31:0] a=3, b=5); //对变量进行初始化
addr = a;
foreach(data[i])
data[i] = b;
endfunction
function void display;
$display("Transaction: %h", addr);
endfunction:display
function void calc_crc;
crc = addr ^ data.xor;
endfunction:calc_crc
endclass
//================================================ 类的使用
initial begin
Transaction tr; //声明句柄
tr = new(10); //开辟空间
tr.addr = 12; //设置变量的值
tr.display(); //调用一个子程序
end
那么当程序运行到 tr = new(10);时,tr.addr 的值为10。其原因是:首先创建 class,addr的值为16,然后初始化 class,addr的值为3,最后开辟空间时,传入了10,所以 tr.addr 的值为10。到下一行 tr.addr = 12; 时,程序对其赋上了新的值,再下一行 tr.display( ); 时,则调用了 class 里定义的一个子程序。
3 class高级用法
3.1 class里的静态变量(static)
module 里的变量默认为静态变量,而 class 里的变量默认为动态变量,然而我们可以在 class 中用 static 关键字创建静态变量,在声明时初始化(不能在类的构造函数 new( ) 中初始化静态变量,因为每一个新的对象(实例)都会调用构造函数,就达不到静态变量的全局效果)。该变量被这个类的所有实例所共享,并且它的使用范围仅限于这个类。如下例子中,静态变量 count 用来保存所创建的对象的数目,它在声明时被初始化为0,没构造一个新的对象,它就被标记为唯一的值,同时 count 将被加一。
class Transaction;
static int count=0;
int id;
function new();
id = count++;
endfunction
endclass
Transaction tr1, tr2;
initial begin
tr1 = new(); //第一个实例,id=0, count=1
tr2 = new(); //第二个实例,id=1, count=2
$display("tr2 id=%d, count=%d", tr2.id, tr2.count);
//或者写成:$display("tr2 id=%d, count=%d", Transaction::id, transaction::count);
end
静态变量也可以使用作用域 :: 来索引,不过很少这样用。
class 里也可以使用静态方法(函数),里面可以声明并使用新的动态变量,而不能使用 class 的动态成员变量,因为在调用静态方法时,可能并没有创建具体的对象(实例),也因此没有为动态成员变量开辟空间,可能会造成内存泄漏。但是静态方法里可以使用类的静态成员变量,因为静态方法同静态变量一样在编译阶段就已经为其分配好了内存空间。
3.2 class外的方法定义(extern)
如果希望限制代码段的长度在一页范围内以保证其可读性,可以将方法的原型定义(方法名和参数)放在类的内部,而方法的程序体(过程代码)放在类的后面定义。使用时需要加上作用域 :: 。
//================================================ class内原型定义
class PCI_Tran;
bit [31:0] addr;
bit [31:0] data;
extern function void display();
endclass
//================================================ class内原型定义
function void PCI_Tran::display();
$display("@%0t: PCI: addr = %h, data = %h", $time, addr, data);
endfunction
4、class的成员和变量
4.1 class的封装
类作为载体,也具备了天生的闭合属性,即将其属性和方法封装在内部,不会将成员变量暴露给外部,通过 protected 和 local 关键字来设置成员变量和方法的外部访问权限。所以封装属性在设计模式中称之为开放封闭原则(OCP Open Closed Principle)。
- 如果没有指明访问类型,那么成员的默认类型是 public ,子类和外部均可以访问成员。
- 如果指明访问类型为 protected ,那么只有该类或者子类可以访问成员,而外部无法访问。
- 如果指明访问类型为 local ,那么只有该类可以访问成员,子类和外部都无法访问。
//===================================================== 定义class
class clock
local bit is_summer = 0;
local int nclock = 6;
function int get_clock();
if(!is_summer)
return this.nclock;
else
return this.nclock+1;
endfunction
function bit set_summer(bit s);
this.is_summer = s;
endfunction
endclass
//===================================================== 使用class
clock ck; //句柄声明
initial begin
ck = new(); //开辟空间
$display("now time is %0d", ck.get_clock()); //6
ck.set_summer(1);
$display("now time is %0d", ck.get_clock()); //7
$display("now time is %0d", ck.nclock); //报错,外部是不能访问local的nclock的!
end
什么是 this ?
如果在类中使用了 this,即表明 this.X 所调用的成员是当前 class 的成员,而非同名的局部变量或者形式参数等,如果当前 class 找不到,那么会去其父类 class 中寻找。
function new(string name);
this.name = name;//左边的name是class的成员name
endfunction //右边的name是function的string name
4.2 class的继承
4.2.1 事务基类
当扩展一个类时,原始类被称为父类或者超类,扩展类被称为派生类或子类。基类是不从任何其他类派生得到的类。下面这个例子中,事务基类含有一些变量和子程序,子程序包括用于显示内容和 CRC 计算。calc_crc 函数被标记为 virtual ,这样后面就可以在需要的时候重新定义。继承 class 时,需要使用关键字 extends ,如下所示:
//============================================================================== 父类class
class Transaction;
rand bit [31:0] src,dst,data[8]; //随机变量
bit [31:0] crc; //计算得到的crc值
virtual function void calc_crc;
crc = src ^ dst ^ data.xor;
endfunction
virtual function void display(input string prefix="");
$display("%sTr: src = %h, dst = %h, crc = %h", prefix, src, dst, crc);
endfunction
endclass
//============================================================================== class继承
class BadTr extends Transaction;
rand bit bad_crc;
virtual function void calc_crc;
super.calc_crc(); //继承父类的子程序
if(bad_crc)
crc = ~crc; //新增子程序的功能
endfunction
virtual function void display(input string prefix="");
$write("%sBadTr:bad_crc = %b, ", prefix, bad_crc); //新增子程序的功能
super.display(); //继承父类的子程序
endfunction
endclass
例子中的 BadTr 类可以直接访问 Transaction 原始类和其本身的所有变量,扩展类中的 calc_crc 函数通过使用 super 前缀调用基类中的 calc_crc 函数。
4.2.2 构造函数new
如果父类构造函数 new( ) 有参数,那么扩展类必须有一个构造函数 new( ) 而且必须在其构造函数的第一行调用基类的构造函数。如果父类的 new( ) 函数没有参数,子类可以省略该调用,而系统会在编译时自动添加 super.new( )。
//====================================================== 基类
class base;
int var;
function new(input int var); //带有参数的构造函数
this.var = var;
endfunction
endclass
//====================================================== 子类
class base_extend extends base;
function new(input int var);
super.new(var); //必须在第一行调用new
//......
endfunction
endclass
4.2.3 成员的覆盖
父类和子类拥有同名的变量和方法是允许的,可以使用 super 来选择父类区域,使用 this 来选择子类区域。默认情况下,如果没有使用 super 和 this 来指示作用域,则依照从近到远的原则来引用变量:
- 首先看变量是否为函数内部定义的局部变量;
- 其次看变量是否为当前类定义的成员变量;
- 最后看变量是否为父类定义的成员变量;
class test_wr extends basic_test;
int def = 200;
function new();
super.new(def);
$display("test_wr::super.def = %d", super.def); //显示父类的def值
$display("test_wr::this.def = %d", this.def); //显示当前类的def值
endfunction
......
endclass
4.3 句柄的使用
4.3.1 子类和父类的转换
派生类句柄赋值给一个基类句柄是允许的,而且不需要任何特殊的代码,如下所示:
//============================================================================== 父类class
class Transaction;
rand bit [31:0] src,dst,data[8]; //随机变量
bit [31:0] crc; //计算得到的crc值
function void calc_crc;
crc = src ^ dst ^ data.xor;
endfunction
virtual function void display(input string prefix="");
$display("%sTr: src = %h, dst = %h, crc = %h", prefix, src, dst, crc);
endfunction
endclass
//============================================================================== class继承
class BadTr extends Transaction;
rand bit bad_crc;
function void calc_crc;
super.calc_crc(); //继承父类的子程序
if(bad_crc)
crc = ~crc; //新增子程序的功能
endfunction
virtual function void display(input string prefix="");
$write("%sBadTr:bad_crc = %b, ", prefix, bad_crc); //新增子程序的功能
super.display(); //继承父类的子程序
endfunction
endclass
//============================================================================== 句柄使用
//句柄声明
Transaction u_tr; //父类
BadTr u_bad; //子类
//句柄转换
u_bad = new();
u_tr = u_bad; //子类句柄赋值给父类句柄
$display(u_tr.src); //显示的是父类的变量成员
u_tr.calc_crc; //调用Transaction::calc_crc
u_tr.display; //调用BadTr::display,因为display用了virtual
注意虚方法 virtual 的使用:
- 如果使用了 virtual 修饰符,SystemVerilog 会查找到子类中去,即查找的是对象的类型;
- 如果未使用 virtual 修饰符,SystemVerilog 会查找到父类中去,即查找的是句柄的类型;
类型向下转换或者类型变换是指将一个指向基类的指针转换成一个指向派生类的指针,和上面这样直接赋值是会报错的,但其并不总是非法的,可以采用 $cast 子程序来检查句柄所指向的对象类型。
//$cast做task用时,类型转换失败则在仿真时报错
$cast(u_bad, u_tr);
//$cast做function用时,类型转换失败是不报错的,需要自行设计
if(!$cast(u_bad, u_tr))
$display("can not assign u_tr to u_bad !!!")
4.3.2 函数中的class
function 参数中声明了句柄,必须加上 ref 或者 inout ,否则后面的语句会报错。
function void create(Transaction u_tr); //bug,missing ref or inout
u_tr = new(); //无返回值,后续会报错
u_tr.addr = 42; //加上ref或inout就行了
...
endfunction
Transaction u_t; //只是声明,u_t的值是null
initial begin
create(u_t); //得不到返回值
u_t.addr = 10; //报错,u_t还是null值
$display(u_t.addr);//运行不到这步,上一步去除,这步也是报错
end
4.3.3 class的复制
使用 new 复制一个对象简单而且可靠,它创建了一个新的对象,并且复制了现有对象的所有变量,但是任何 new( ) 函数都不会被调用,这就导致如果复制函数试图修改里面的嵌套的 class,会影响到本体的嵌套的 class。
//====================================== 构造class
class Transaction;
bit [31:0] addr,crc,data[8];
statistics stats;
function new;
stats = new(); //这个不会被后面的new复制
endfunction
endclass
//======================================= new的使用
Transaction src dst;
initial begin
src = new();
src.stats.a = 1; //a赋值为1
dst = new src; //new复制
dst.stats.a = 2; //a赋值为2
$display(src.stats.a); //答案是2,dst改变了src里的stats
end
end
4.4 参数化的类
在 SV 中,可以为类 class 声明一个数据类型参数,并在声明类的句柄时指定类型,提高代码的重用率,如下所示:
后面要换成别的类型,就直接这样做:
5、packet
5.1 包的意义
- SV 提供了一种在多个 module、interface、program 之中共享 parameter、data、type、task、function、class 的方法,即用 package(包)来实现。
- 一般将不同模块的类定义归整到不同的 package 中,使得分属不同模块验证环境的类来自于不同的 package,解决类的归属问题。
5.2 包的定义
如果有两个 DV,一个负责寄存器模块,一个负责仲裁模块,它们的 sv 文件可能同名,那么可以用 package 来分类,如下所示:
package reg_pkg;
`include "stimulator.sv"
`include "monitor.sv"
`include "chker.sv"
`include "env.sv"
endpackage
package arb_pkg;
`include "stimulator.sv"
`include "monitor.sv"
`include "chker.sv"
`include "env.sv"
endpackage
”`include“的关键词用于类在包中的封装,要注意编译的前后顺序来放置各个类文件。编译一个包的背后,实际是将各类文件”平铺“在包中,按照顺序完成包和类的有序编译。在使用时,只需要注明要使用哪一个 package 就行:
module my_tb();
reg_pkg::monitor mon1 = new(); //导入reg_pkg中指定的sv文件
arb_pkg::monitor mon2 = new(); //导入reg_pkg中指定的sv文件
endmodule
5.3 包的使用
在创建 package 的时候,已经在指定包名称的时候隐含地指定了包的路径,如果有其他在默认路径之外的文件,需要在编译包的时候加上额外指定的搜寻路径项”+incdir + PATH“。为了方便的使用包,我们可以在命名 .sv 文件时加上前缀,后面的使用就方便了,如下所示:
package reg_pkg;
`include "reg_stimulator.sv"
`include "reg_monitor.sv"
`include "reg_chker.sv"
`include "reg_env.sv"
endpackage
package arb_pkg;
`include "arb_stimulator.sv"
`include "arb_monitor.sv"
`include "arb_chker.sv"
`include "arb_env.sv"
endpackage
module my_tb();
import reg_pkg::*; //导入reg_pkg的所有文件
import arb_pkg::*; //导入arb_pkg的所有文件
reg_mon mon1=new();
arb_mon mon2=new();
endmodule
参考资料:
[1] 路科验证V2教程
[2] 绿皮书:《SystemVerilog验证 测试平台编写指南》第2版
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2021-07-10 SVN学习笔记
2020-07-10 数电(5):半导体存储电路
2019-07-10 ZYNQ笔记(1):PL端——led灯