OOP概述
特性
- SV面向对象编程(Object Oriented Programming, OOP)是在传统Verilog基础上扩展的核心特性
- 能够创建复杂的数据类型及方法,并且将它们和子程序紧密第结合在一起
- 能够在更加抽象的层次建立测试平台和系统级模型,使之更加易于维护
- 通过封装、继承、多态等机制实现代码复用、模块化和灵活扩展
- 传统的测试平台强调操作:创建一个事务、发送、接收、检查结果、产生报告
- 在OOP中更加聚焦于各个组件及功能,如
Driver、Monitor、Packet、Transaction、Scoreboard等
术语
- 在Verilog中,通过创建模块并在编译时逐层例化,可以得到一个复杂的设计,其顶层模块是隐式例化的
- 在OOP中创建类并在运行时构造它们(创建对象),可以得到一个相似的层次结构,SV类在使用前都需例化
| SV |
简介 |
对应Verilog |
类class |
包含变量和子程序的基本构建块 |
模块module |
对象object |
类的实例化 |
模块实例化 |
句柄handle |
指向对象的指针 |
``top.u_t.func/task/variable` |
| 属性Property |
存储数据的成员变量 |
reg或wire |
| 方法method |
操作变量的子程序 |
任务和函数 |
| 原型prototype |
extern方法名和参数 |
不支持 |
类
定义
- 类是对象的模板,类中的成员变量用来保存数据,而子程序用来控制这些数值
- 下例为一个通用事务的类,属性:地址、校验和、存储值的数组;方法:显示地址、计算数据校验和
1 2 3 4 5 6 7 8 9 10 11 12
| class Transaction; bit [31:0] addr, csm, data[8]; function void display(); $display("Transaction: %h", addr); endfunction : display function void calc_csm(); csm = addr ^ data.xor; endfunction : calc_csm endclass : Transactoin
|
习惯性地使用标记,在有很多嵌套块的复杂代码中用处很大,可以很好地配对end、endtask、endfunction、endclass
包
- 类应在
program或module,或其外的package中定义
- 在SV数据类型这篇文章中也曾介绍过包的概念,当时是用于封装常数和结构体,包还能用于封装类
- 项目文件较多时,可以使用包
package将一组相关的类和类型定义捆绑在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package abc; class Transaction; int pd; ... endclass endpackage
program automatic test; import abc::*; int d; Transaction tr; class xyz; int l; ... endclass ... endprogram
|
方法
- 类中的程序也称为方法,即类的作用域内定义的内部
task和function
- 类中的方法默认使用自动存储,即隐式
automatic
原型方法
- 为了提高代码可读性,可以在类外定义方法
- 将方法的原型定义(方法名和参数)放在类的内部
- 方法的程序体放在类的后面定义
1 2 3 4 5 6 7 8
| class PCI_Tran; bit [31:0] addr, data; extern function void display(); endclass function void PCI_Tran::display(); $display("@0t: PCI: addr = %h, data = %h", $time, addr, data); endfunction
|
静态方法
- 类中的自动变量必须通过类的实例(对象)访问
- 类的静态变量和静态方法属于类的全局资源,而非某个具体对象实例
- 可以直接通过类名访问
类名::静态成员,无需实例化对象
- 类作用域操作符
::,用于明确指定访问类作用域中的成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Counter; static int total_count = 0; static function void display_statics(); $display("display statics test"); endfunction endclass
module test; initial begin Counter::total_count = 100; $display("total_count:%0d", Counter::total_count); Transaction::display_static(); end endmodule
|
对象
- 若将类视为描述房子结构的蓝图,而对象则是一幢可以实际居住的房子,房子的地址就是句柄
构造
- 创建类的实例的过程被称为实例化,实例化后的类称为对象,
new()函数被称为构造函数(Constructor)
new()为对象分配空间,并初始化变量,构造完成后,返回保存对象的地址,即句柄
- 创建对象时使用的是
new(),而在创建动态数组大小时使用的是new[],注意区分
参数化构造
- 使用参数化
new()可以实现自定义初始值,否则默认初始化二值变量为0、四值变量为X
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Transaction; logic [31:0] addr, csm, data[8]; function new(input logic [31:0] a = 3); addr = a; data = `{default:5}; endfunction endclass
initial begin Transaction tr; tr = new(.a(10)); end
|
- 当方法的参数名或局部变量与类的成员变量同名时,使用
this可以明确指定要访问的是当前对象的成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class MyClass; bit [7:0] data;
function new(bit [7:0] data); this.data = data; endfunction
function void set_data(bit [7:0] data); this.data = data; endfunction
function bit [7:0] get_data(); return this.data; endfunction endclass
MyClass mc; initial begin mc = new(.data(123)); end
|
访问
直接访问
- 通过对象句柄直接访问成员变量或方法
- 本质是直接操作对象内部数据,依赖成员变量的公有访问权限
- SV中所有成员都是公有的,除非标记为私有
local或protected
- 直接访问破坏封装性,适用于需要在测试平台的地方访问时
- 外部代码需知晓内部数据结构,无法限制输入合法性,可能导致数据异常
- 一旦内部变量重命名或变更类型,外界所有直接访问的代码都需要同步更改
1 2 3 4 5 6 7 8 9 10 11 12
| class Counter; int count; endclass
module test; initial begin Counter c = new(); c.count = 10; c.count = -5; $display("Count: %0d", c.count); end endmodule
|
访问函数
- 通过类的访问函数间接访问内部数据
- 本质是通过接口方法控制数据交互,内部数据通常被声明为
local或protected
- 访问函数具备更好的安全性和可维护性,但同样也会使测试平台变得更大更复杂
- 可在访问函数内添加验证逻辑,确保数据合法性,同时实现日志功能,这在直接访问中是无法实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Counter; local int count;
function int get_count(); $display("[LOG] get_count: %0d", count); return count; endfunction
function void put_count(int val); if (val < 0) begin $error("[ERROR] Invalid count: %0d (must be non-negative)", val); return; end count = val; $display("[LOG] put_count: %0d", count); endfunction endclass
module test; initial begin Counter c = new(); c.put_count(10); c.put_count(-5); $display("Current count: %0d", c.get_count()); end endmodule
|
方法
对象句柄
- 句柄本质是对象的引用,存储的是对象在内存中的地址
- 通过修改句柄的值,可以让同一各句柄在仿真过程中先后指向不用的对象示例
- 而在Verilog中名字和内存是静态捆绑在一起的
1 2 3 4
| Transaction t1, t2; t1 = new(); t2 = t1; t1 = new();
|
- 当没有任何句柄指向一个对象时,SV会自动回收其内存,避免了忘记手动释放对象时可能发生的内存泄露
- 若对象包含从一个线程派生出的程序,只要线程仍在运行,对象的空间就不会释放
- 同样,被线程所使用的对象在该线程结束之前也不会解除分配
静态句柄
- 当类中的变量声明和方法原型过多时,可以将类分成几个更小、相关的类
- 当类的每个实例都需要从同一个对象获取信息时,使用静态句柄可以避免内存的浪费
- 注意使用类中类时,一定要先例化,否则其句柄是
null
- 若类中类在被包含类之后定义,需要使用
typedef进行前向声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| typedef class Config; class Transaction; static Config cfg; ... endclass
class Config; ... endclass Config cfg; initial begin cfg = new(.num_trans(42)); Transaction::cfg = cfg; end
|
传递句柄

- 被传递句柄的方法,可以修改共享对象,但不能修改原句柄的指向
1 2 3 4
| task transmit(input Transaction t); t.addr = 100; t = new(); endtask
|
- 当所传递的句柄使用
ref修饰时,被传递句柄的方法修改句柄,可以改变原句柄的指向
1 2 3 4 5 6 7 8 9 10
| function void create(ref Transaction tr); tr = new(); ... endfunction
Transaction t; initial begin create(t); $display(t.addr); end
|
句柄数组
- 在搭建测试平台时,需要保存并引用许多对象,可以创建句柄数组,数组的每个元素指向一个对象
1 2 3 4 5 6 7
| task generator(); Transaction tarray[10]; foreach (tarray[i]) begin tarray[i] = new(); transmit(tarray[i]); end endtask
|
复制
- 复制一个对象,以防止被传递句柄的方法修改原始对象的值
浅层复制
1 2 3 4 5 6 7 8 9 10 11 12
| class Transaction; bit [31:0] addr, csm, data[8]; function new(); $display("In %m"); endfunction endclass
Transaction src, dst; initial begin src = new(); dst = new src; end
|
- 使用
new进行复制时,若类中包含一个指向另一个类的句柄,则只会复制句柄的值
- 若对复制后的对象中的句柄进行操作,也会影响原型对象中的句柄所指向的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Statistics; time startT; function void start(); starrT = $time; endfunction endclass
class Transaction; bit [31:0] addr, csm, data[8]; static int count = 0; Statistics stats; int id; 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; $display(src.stats.startT); end
|

- 若要复制一个不包含另一个对象句柄的简单类,编写简单的自定义
copy()函数也能起到和使用new一样的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Transaction; bit [31:0] addr, csm, data[8]; function Transaction copy(); copy = new(); copy.addr = addr; copy.csm = csm; copy.data = data; endfunction endclass
Transaction src, dst; initial begin src = new(); dst = src.copy(); end
|
深层复制
- 对于并非简单的类,需要进行深层复制
- 通过调用类所包含的所有对象的
copy函数
- 在
copy()函数中构造目标对象,避免出现只复制句柄值得情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| class Statistics; time startT; function void start(); starrT = $time; endfunction function Statistics copy(); copy = new(); copy.startT = startT; endfunction endclass
class Transaction; bit [31:0] addr, csm, data[8]; static int count = 0; Statistics stats; int id; function new(); stats = new(); id = count++; endfunction function Transaction copy(); copy = new(); copy.addr = addr; copy.csm = csm; copy.data = data; copy.stats = stats.copy(); endfunction endclass
Transaction src, dst; initial begin src = new(); src.stats.startT = 42; dst = src.copy(); dst.stats.startT = 96; $display(src.stats.startT); end
|

UVM数据宏会自动创建复制函数,无需手动编写它们。手动创建这些函数在添加变量时非常出错