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() 得到独立副本再处理。

验证平台中的对象关系与拷贝语义 继承层次(向上转型 + 虚方法分派) base_test virtual task run() smoke_test override run() random_test override run() base_test t; t = new smoke_test(); t.run(); → 运行时调用 smoke_test::run 浅拷贝 vs 深拷贝 浅拷贝:句柄共享 pkt A addr=1000 pkt B addr=1000 hdr 子对象 修改 B.hdr 会影响 A.hdr 深拷贝:子对象独立 pkt A addr=1000 pkt B addr=1000 hdr_A hdr_B 修改 B.hdr 不影响 A.hdr 验证平台实践要点 • 公共接口声明 virtual,保证多态与工厂覆盖生效 • 句柄赋值只复制引用;需要独立副本时实现 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 接口使用。这正是多态在验证平台中最大的工程价值——不修改环境代码即可替换行为。

判断是否需要深拷贝的实用方法:问自己“副本修改这个字段时,原对象是否应该保持不变”。若答案是“是”,该字段是句柄就必须递归拷贝;若字段是标量,赋值本身已是独立副本。
常见错误:在 driver 中直接修改从 sequencer 取到的 req 对象字段,导致 scoreboard 的参考模型数据被同步污染,表现为“随机失败、重跑不复现”。修复方式是先 copy 再改,或让 sequence 在发送前完成所有字段设置。

掌握类与对象、继承与多态、虚方法分派以及深浅拷贝的边界,是后续搭建可复用、可扩展验证平台的前提。把这些语义用对,环境才能在替换激励、增加检查、复用组件时保持稳定。