SV类与对象

OOP概述

特性

  • SV面向对象编程(Object Oriented Programming, OOP)是在传统Verilog基础上扩展的核心特性
    • 能够创建复杂的数据类型及方法,并且将它们和子程序紧密第结合在一起
    • 能够在更加抽象的层次建立测试平台和系统级模型,使之更加易于维护
  • 通过封装、继承、多态等机制实现代码复用、模块化和灵活扩展
    • 传统的测试平台强调操作:创建一个事务、发送、接收、检查结果、产生报告
    • 在OOP中更加聚焦于各个组件及功能,如DriverMonitorPacketTransactionScoreboard

术语

  • 在Verilog中,通过创建模块并在编译时逐层例化,可以得到一个复杂的设计,其顶层模块是隐式例化的
  • 在OOP中创建类并在运行时构造它们(创建对象),可以得到一个相似的层次结构,SV类在使用前都需例化
SV 简介 对应Verilog
class 包含变量和子程序的基本构建块 模块module
对象object 类的实例化 模块实例化
句柄handle 指向对象的指针 ``top.u_t.func/task/variable`
属性Property 存储数据的成员变量 regwire
方法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 // 标记label

function void calc_csm();
csm = addr ^ data.xor;
endfunction : calc_csm

endclass : Transactoin

习惯性地使用标记,在有很多嵌套块的复杂代码中用处很大,可以很好地配对endendtaskendfunctionendclass

  • 类应在programmodule,或其外的package中定义
  • SV数据类型这篇文章中也曾介绍过包的概念,当时是用于封装常数和结构体,包还能用于封装类
    • 项目文件较多时,可以使用包package将一组相关的类和类型定义捆绑在一起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文件abc.svh
package abc; // $root.abc绝对路径
class Transaction;
int pd; // $root.abc.Transaction.pd
...
endclass
endpackage

// 测试文件
program automatic test;
import abc::*;// 导入包
int d; // $root.test.d
Transaction tr;

class xyz;
int l; // $root.test.xyz.l
...
endclass

... // 测试代码
endprogram

方法

  • 类中的程序也称为方法,即类的作用域内定义的内部taskfunction
  • 类中的方法默认使用自动存储,即隐式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); // 输出:100
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}; // 所有元素均为5
endfunction
endclass

initial begin
Transaction tr; // 声明一个对象的句柄
tr = new(.a(10)); // 构造对象,并返回句柄
// 使用.a(10)则addr指定10,若未使用则addr默认3
// data指定固定值,csm被初始化未默认值x
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; // 用 'this' 指定左侧是成员变量
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中所有成员都是公有的,除非标记为私有localprotected
  • 直接访问破坏封装性,适用于需要在测试平台的地方访问时
    • 外部代码需知晓内部数据结构,无法限制输入合法性,可能导致数据异常
    • 一旦内部变量重命名或变更类型,外界所有直接访问的代码都需要同步更改
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); // 输出 -5(错误状态)
end
endmodule

访问函数

  • 通过类的访问函数间接访问内部数据
    • 本质是通过接口方法控制数据交互,内部数据通常被声明为localprotected
  • 访问函数具备更好的安全性和可维护性,但同样也会使测试平台变得更大更复杂
    • 可在访问函数内添加验证逻辑,确保数据合法性,同时实现日志功能,这在直接访问中是无法实现的
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(); // 读取count(getter),可附加日志
$display("[LOG] get_count: %0d", count); // 记录访问日志
return count;
endfunction

function void put_count(int val); // 修改count(putter),验证输入合法性
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); // 合法赋值,日志:[LOG] put_count: 10
c.put_count(-5); // 非法赋值,报错:[ERROR] Invalid count: -5...
$display("Current count: %0d", c.get_count()); // 日志:[LOG] get_count: 10,输出:10
end
endmodule

方法

对象句柄

  • 句柄本质是对象的引用,存储的是对象在内存中的地址
    • 通过修改句柄的值,可以让同一各句柄在仿真过程中先后指向不用的对象示例
    • 而在Verilog中名字和内存是静态捆绑在一起的
1
2
3
4
Transaction t1, t2; // 声明两个句柄
t1 = new(); // 构造第一个对象,并返回内存地址
t2 = t1; // 复制内存地址,使t1和t2都指向第一个对象
t1 = new(); // 构造第二个对象,并返回内存地址,此时t1指向第二个对象
  • 当没有任何句柄指向一个对象时,SV会自动回收其内存,避免了忘记手动释放对象时可能发生的内存泄露
    • 若对象包含从一个线程派生出的程序,只要线程仍在运行,对象的空间就不会释放
    • 同样,被线程所使用的对象在该线程结束之前也不会解除分配

静态句柄

  • 当类中的变量声明和方法原型过多时,可以将类分成几个更小、相关的类
  • 当类的每个实例都需要从同一个对象获取信息时,使用静态句柄可以避免内存的浪费
    • 注意使用类中类时,一定要先例化,否则其句柄是null
    • 若类中类在被包含类之后定义,需要使用typedef进行前向声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef class Config;  // 当Config在Transaction之后定义,需要前向声明
class Transaction;
static Config cfg; // 使用静态存储的句柄,类中类
...
endclass

class Config;
...
endclass

Config cfg;
initial begin
cfg = new(.num_trans(42)); // 例化对象,通过参数名传递初始值
Transaction::cfg = cfg; // 将已例化对象的句柄传递给类中类
// 上述两行等同于Transaction::cfg = new(.(num_trans(42)));
end

传递句柄

  • 将句柄传递给方法,传递的是对象的句柄而非对象本身

image-20250811144144482

  • 被传递句柄的方法,可以修改共享对象,但不能修改原句柄的指向
1
2
3
4
task transmit(input Transaction t); // 形参t是generator中的t的副本(按值传递)
t.addr = 100; // 修改共享对象的addr,t.addr会变为100
t = new(); // 修改句柄t本身,让transmit中的t指向新对象,generator中的t仍指向原对象
endtask
  • 当所传递的句柄使用ref修饰时,被传递句柄的方法修改句柄,可以改变原句柄的指向
    • 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); // 在方法中创建一个transaction
$display(t.addr); // 若无ref,则 t = null,未改变原句柄的指向
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

复制

  • 复制一个对象,以防止被传递句柄的方法修改原始对象的值

浅层复制

  • 使用new操作符复制一个对象
    • 创建一个新的对象,并且复制现有对象的所有变量
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; // 使用new操作符进行复制
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(); // new复制时,构造不会再次执行,见下图中的id
stats = new(); // 构造一个新的Statistics对象
id = count++; // 每构造一个对象都有一个唯一的id
endfunction
endclass

Transaction src, dst;
initial begin
src = new();
src.stats.startT = 42;
dst = new src; // 复制类中另一个对象句柄的值
dst.stats.startT = 96; // 由于复制了同一个句柄的值,会同时改变dst和src中的stats
$display(src.stats.startT); // "96"而非"42"
end

image-20250811170922501

  • 若要复制一个不包含另一个对象句柄的简单类,编写简单的自定义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()时构造目标对象
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()时构造目标对象
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(); // 调用Statics::copy函数
endfunction
endclass

Transaction src, dst;
initial begin
src = new();
src.stats.startT = 42;
dst = src.copy(); // 深层复制src
dst.stats.startT = 96; // 仅改变dst的stats值
$display(src.stats.startT); // "42"
end

image-20250811173731432

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