UVM 入门:它到底解决了什么问题?
从零理解 UVM 为何存在、testbench 的分层结构,以及 uvm_component 的基本写法。
在没有统一方法学之前,不同团队、不同项目的 SystemVerilog testbench 往往各写各的:有的用类封装驱动逻辑,有的直接在 initial 块里堆代码。结果是 testbench 难以复用、难以维护,新人接手成本很高。
UVM(Universal Verification Methodology) 是建立在 SystemVerilog 面向对象特性之上的一套标准化验证方法学,它规定了:
- testbench 各部件(驱动、监测、记分板等)应该如何分层组织
- 部件之间应该如何通信(TLM,Transaction Level Modeling)
- 激励(stimulus)应该如何以可复用、可随机化的方式生成(Sequence)
- 如何在不改动 testbench 结构代码的前提下替换某个组件的实现(Factory 机制)
Testbench 的典型分层
一个典型的 UVM testbench 层级大致如下:
uvm_test
└── uvm_env
├── uvm_agent
│ ├── uvm_sequencer (产生/管理事务)
│ ├── uvm_driver (把事务转换为引脚级信号,驱动 DUT)
│ └── uvm_monitor (从引脚采样,还原成事务)
└── uvm_scoreboard (比对预期结果与实际结果)
uvm_agent 通常包含 sequencer、driver、monitor 三件套,可以配置为 active(既驱动又监测)或 passive(只监测,常用于总线一侧无需驱动的场景)。
一个最简单的 uvm_component
UVM 中几乎所有结构性部件都继承自 uvm_component。下面是一个只打印日志的最简驱动骨架:
class simple_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(simple_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
my_transaction tr;
seq_item_port.get_next_item(tr);
`uvm_info("DRV", $sformatf("driving transaction: %s", tr.convert2string()), UVM_MEDIUM)
// 此处将 tr 中的字段转换为对 DUT 引脚的实际驱动
seq_item_port.item_done();
end
endtask
endclass几个关键点:
`uvm_component_utils(simple_driver)向 UVM 的 Factory 注册这个类,使其可以在不修改代码的情况下被子类覆盖(override)。new(string name, uvm_component parent)是所有uvm_component的标准构造函数签名,parent用于维护组件树。run_phase是 UVM 的运行期 phase 之一,driver 通常在这里通过seq_item_port从 sequencer 拿到事务并驱动 DUT。
uvm_component 与 uvm_object 的区别
uvm_component:testbench 的结构性部件(driver、monitor、env、test 等),在整个仿真期间存在,构造时必须传入name和parent,参与 UVM 的 phase 机制。uvm_object:轻量级数据对象(如事务 transaction、配置对象),生命周期更灵活,构造函数一般只需要name,不参与 phase。
一个典型的 uvm_agent 通常包含哪三个核心子组件?
以下哪一项是 uvm_component 与 uvm_object 的关键区别?