2. 搭建你的第一个UVM环境:从零开始搭建一个简单的UVM验证平台,理解uvm_component与uvm_object
好,咱们直接动手。这一章的目标很明确——从零开始,搭一个能跑起来的UVM验证平台。别怕,它很简单,但能帮你搞清楚两个最核心的概念:uvm_component 和 uvm_object。
我记得刚学UVM那会儿,看了好多理论,什么树形结构、phase机制,头都大了。后来我发现,与其死磕概念,不如先写一个最小系统。跑通了,自然就懂了。你想想看,是不是这个理?
2.1 先搞清楚:component 和 object 到底有啥区别?
这是UVM里第一个绕不开的坎。说白了,uvm_component 和 uvm_object 都是UVM世界里的“基类”,但它们的定位完全不同。
| 特性 | uvm_component | uvm_object |
|---|---|---|
| 生命周期 | 贯穿整个仿真,有phase机制 | 临时创建,用完即销毁 |
| 树形结构 | 有parent,构成层次化树 | 没有parent,独立存在 |
| 典型代表 | driver, monitor, agent, env | transaction, sequence_item, config |
| 创建方式 | 使用 type_id::create() 并指定parent |
使用 type_id::create() 或 new() |
核心记忆点: 有“生命”的、需要被组织起来的,用component。临时传递数据的“包裹”,用object。
我在项目中遇到过一位同事,把transaction写成了component,结果仿真器里生成了无数个永远不会被销毁的实例,内存直接爆了。嗯,这就是没分清两者的代价。
2.2 搭建最小验证平台:一个driver就够了
咱们的目标是:写一个driver,它能从sequencer拿到transaction,然后驱动到DUT的接口上。为了简化,咱们先不连DUT,只让driver打印出它收到的数据。
先定义一个transaction,它是个object:
class my_transaction extends uvm_sequence_item;
rand bit [7:0] addr;
rand bit [7:0] data;
rand bit wr_en;
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(wr_en, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "my_transaction");
super.new(name);
endfunction
endclass
注意看,这里用的是 uvm_sequence_item,它继承自 uvm_transaction,而 uvm_transaction 又继承自 uvm_object。所以它本质上还是个object。
接下来,写driver。driver是个component:
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
my_transaction tr;
forever begin
seq_item_port.get_next_item(tr);
`uvm_info("DRV", $sformatf("Got transaction: addr=%0d, data=%0d, wr=%0d",
tr.addr, tr.data, tr.wr_en), UVM_LOW)
seq_item_port.item_done();
end
endtask
endclass
这里有个细节:run_phase 是UVM自动调用的。你不需要手动去启动它。这就是component的phase机制在起作用。
个人习惯: 我写driver时,喜欢在 run_phase 开头加一句 phase.raise_objection(),在结尾加 phase.drop_objection()。虽然这个例子没用到,但养成习惯后,以后做复杂场景时不容易漏掉。
2.3 把它们组装起来:写一个简单的test
有了driver和transaction,咱们还需要一个test来把它们串起来。test本身也是个component:
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_driver drv;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = my_driver::type_id::create("drv", this);
endfunction
task run_phase(uvm_phase phase);
my_transaction tr;
phase.raise_objection(this);
// 手动创建一个transaction,发给driver
tr = my_transaction::type_id::create("tr");
tr.randomize();
drv.seq_item_port.put(tr);
#100;
phase.drop_objection(this);
endtask
endclass
注意 build_phase 里的 create 函数。第二个参数 this 就是parent。这行代码执行后,UVM会自动把 drv 挂到 my_test 下面,形成树形结构。
最后,在顶层模块里启动UVM:
module top;
initial begin
run_test("my_test");
end
endmodule
就这么简单。跑一下仿真,你会看到driver打印出类似这样的信息:
UVM_INFO @ 0: drv [DRV] Got transaction: addr=123, data=45, wr=1
2.4 避坑指南:新手最容易犯的三个错误
- 忘记注册宏: 不管是component还是object,都必须用
`uvm_component_utils或`uvm_object_utils注册。我曾经有一次漏掉了,结果仿真器报了一堆奇怪的错误,查了半天才发现是宏没写。 - component的new函数参数写错: component的new函数必须有两个参数:
string name和uvm_component parent。少一个都不行。object的new函数只需要一个string name。 - 在component里手动new另一个component: 永远不要用
new()去创建component。必须用type_id::create()。否则UVM的phase机制和树形结构都不会生效。
我曾经... 在刚转UVM时,为了图省事,直接在driver的new函数里用 new() 创建了一个monitor。结果那个monitor的 build_phase 根本没执行,数据全丢了。从那以后,我养成了习惯:只要是component,一律用 create。
2.5 小结:你学到了什么?
这一章,咱们从零开始搭了一个能跑的UVM环境。虽然它很简陋,但你已经亲手实践了两个最核心的概念:
uvm_object:用来描述数据,比如transaction。它没有生命周期管理,用完就扔。uvm_component:用来描述验证组件,比如driver。它有phase机制,有parent-child关系,是UVM树形结构的节点。
说白了,object是“数据”,component是“处理数据的模块”。这个区分搞清楚了,后面学agent、env、scoreboard这些,就会顺畅很多。
下一章,咱们会引入sequencer和monitor,让这个平台真正具备“激励生成”和“数据监测”的能力。到时候,你就能看到一个更完整的UVM验证环境了。