3. 面向对象编程基础
SystemVerilog 验证平台本质上是一组协同工作的类对象:transaction 描述激励,driver 把激励送到接口,monitor 采样并重建 transaction,scoreboard 比较期望与实际。要让这些对象在仿真中正确协作,必须理解类、对象、继承、多态、虚方法以及拷贝语义。本节从验证平台的实际需求出发,说明这些概念如何落到代码上。
类与对象:模板与实例
类(class)是数据与操作的封装模板,对象(object)是类在运行时通过 new() 创建的实例。验证平台中,一个 packet 类可以定义 addr、data、kind 等字段,以及 display()、compare() 等方法。每次调用 new() 都会在堆上分配一块独立内存,返回该对象的句柄(handle)。句柄是引用语义:两个句柄可以指向同一个对象,修改其中一个句柄所指向的字段,另一个句柄看到的值也会改变。
class packet;
rand bit [31:0] addr;
rand bit [31:0] data;
function new(bit [31:0] a = 0, bit [31:0] d = 0);
addr = a; data = d;
endfunction
virtual function void display(string tag = "PKT");
$display("%s addr=%0h data=%0h", tag, addr, data);
endfunction
endclass
packet p1 = new(32'h1000, 32'hDEAD_BEEF);
packet p2 = p1; // p2 与 p1 指向同一对象
p2.addr = 32'h2000; // p1.addr 也变成 32'h2000
这段代码说明:p2 = p1 只复制句柄,不复制对象内容。验证平台中若把同一个 transaction 句柄同时交给 driver 和 scoreboard,任何一方修改字段都会影响另一方,这是很多“数据对不上”问题的根源。
继承与多态:扩展而不破坏
继承用 extends 表达“是一种”的关系。例如 base_test 定义公共的 build、connect、run 流程,smoke_test 和 random_test 继承它并覆盖 run()。子类对象可以赋值给父类句柄,这就是向上转型(upcast):
class base_test;
virtual task run();
$display("base_test run");
endtask
endclass
class smoke_test extends base_test;
virtual task run();
$display("smoke_test run");
endtask
endclass
base_test t;
smoke_test s = new();
t = s; // 向上转型,合法
t.run(); // 输出 smoke_test run(多态)
关键在于 virtual。若 run() 未声明为虚方法,t.run() 会按句柄的静态类型 base_test 调用父类版本,输出 base_test run;声明为虚方法后,调用在运行时根据对象的实际类型分派,输出 smoke_test run。验证平台中 test、sequence、driver、monitor 的公共接口几乎都应声明为 virtual,否则工厂覆盖(factory override)和回调机制都会失效。
多态的价值在于:验证环境只依赖父类接口,具体行为由实际对象决定。这样新增一个测试用例只需扩展基类并注册到工厂,无需修改已有环境代码。
深拷贝与浅拷贝
拷贝是验证平台中最容易出错的地方。SystemVerilog 没有自动的拷贝构造函数,需要自己实现 copy()。浅拷贝只复制字段值:对整型、枚举等标量字段是独立副本,但对类句柄字段只复制句柄,源对象与副本仍共享同一个子对象。深拷贝则递归复制所有子对象,得到完全独立的副本。
class header;
bit [15:0] len;
function header copy();
header h = new();
h.len = this.len;
return h;
endfunction
endclass
class packet;
rand bit [31:0] addr;
header hdr; // 句柄字段
function packet copy();
packet p = new();
p.addr = this.addr; // 标量:值复制
p.hdr = this.hdr.copy(); // 句柄:递归深拷贝
return p;
endfunction
endclass
判断该用哪种拷贝,可以看对象生命周期:如果副本会独立修改子对象,必须深拷贝;如果子对象是只读共享配置,浅拷贝即可。典型场景是 driver 从 sequencer 取到 transaction 后,若直接修改原对象字段再发送,scoreboard 收到的参考数据也会被改掉,此时应先 req.copy() 得到独立副本再处理。
在验证平台中的落地方式
把上述概念组合起来,一个典型 UVM 风格环境的结构是:uvm_sequence_item 派生 transaction,uvm_driver 派生 driver,uvm_monitor 派生 monitor,uvm_scoreboard 派生 scoreboard,uvm_test 派生各测试用例。driver 的 run_phase 中通过 seq_item_port.get_next_item(req) 取得 transaction,若需要修改字段,应先 req.copy() 再操作,然后 item_done() 通知 sequence 完成握手。monitor 采样接口信号后 new() 一个 transaction 并写入 analysis port,scoreboard 在 write() 中接收并比较。
工厂覆盖依赖虚方法与类型注册:set_type_override_by_type(packet::get_type(), error_packet::get_type()) 之后,所有通过工厂创建 packet 的地方都会得到 error_packet 对象,而调用方仍按 packet 接口使用。这正是多态在验证平台中最大的工程价值——不修改环境代码即可替换行为。
掌握类与对象、继承与多态、虚方法分派以及深浅拷贝的边界,是后续搭建可复用、可扩展验证平台的前提。把这些语义用对,环境才能在替换激励、增加检查、复用组件时保持稳定。