第二讲:UVM基础组件——从uvm_component到factory机制
各位同学,今天我们进入UVM的核心地带。说实话,很多初学者一上来就被各种类、宏、phase搞得晕头转向。我当年也一样,第一次看UVM源码时,心里直犯嘀咕:这玩意儿到底在干嘛?
别急,今天我们就把它掰开揉碎了讲。你只要记住一句话:UVM的本质,就是一套帮你搭建验证环境的C++类库。搞懂了它的骨架,剩下的就是往里面填肉。
2.1 uvm_component vs uvm_object:谁是谁的爸爸?
先问个问题:UVM里为什么要有两个基类?直接一个类搞定不香吗?
嗯,这里要注意。UVM的设计者把验证组件分成了两类:
- uvm_object:轻量级对象,没有生命周期概念。比如你构造一个transaction、一个sequence item,它们不需要被“挂”在验证环境的树上。
- uvm_component:重量级组件,有层次结构,有phase控制。比如driver、monitor、scoreboard,它们必须存在于验证环境的树形结构中。
我个人的习惯是:凡是需要“一直存在”的东西,用uvm_component;凡是“用完就扔”的东西,用uvm_object。举个例子,你写一个driver,它从仿真开始到结束都在干活,那它必须是uvm_component。而一个数据包,发完就没了,用uvm_object就够了。
核心区别一句话总结:
uvm_component有“户口”(挂在树上),有“寿命”(受phase控制)。
uvm_object是“流浪汉”,用完就回收。
来看代码,感受一下区别:
// uvm_object的典型用法——transaction
class my_transaction extends uvm_sequence_item;
rand int addr;
rand int data;
`uvm_object_utils(my_transaction)
function new(string name = "my_transaction");
super.new(name);
endfunction
endclass
// uvm_component的典型用法——driver
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
// 有phase方法
virtual task run_phase(uvm_phase phase);
// 干活...
endtask
endclass
看到没?uvm_component的构造函数多了一个parent参数。这就是“挂树”的关键。我曾经见过一个新手,把driver写成了uvm_object,结果run_phase死活不执行。嗯,这就是没搞清楚基类的后果。
2.2 phase机制详解——UVM的“时间线”
phase机制,说白了就是UVM帮你安排好的“执行顺序”。你想想看,一个验证环境里有那么多组件,谁先干活?谁后干活?总不能一窝蜂上吧?
UVM把仿真过程分成了几个阶段:
| Phase名称 | 执行顺序 | 干什么用的 |
|---|---|---|
| build_phase | 1 | 创建子组件、配置对象 |
| connect_phase | 2 | 连接TLM端口 |
| end_of_elaboration_phase | 3 | 打印环境拓扑、做最终检查 |
| start_of_simulation_phase | 4 | 仿真开始前的准备工作 |
| run_phase | 5 | 真正的仿真运行(消耗仿真时间) |
| extract_phase | 6 | 从monitor/scoreboard提取数据 |
| check_phase | 7 | 检查数据是否正确 |
| report_phase | 8 | 打印测试结果 |
| final_phase | 9 | 收尾工作 |
这里有个坑,我必须要说:build_phase是从上往下执行的,而connect_phase是从下往上执行的。为什么?因为父组件要先创建子组件,然后子组件才能连接。这个顺序搞反了,你会得到一堆空指针。
避坑指南:
我曾经在build_phase里试图访问子组件的TLM端口,结果程序直接崩溃。后来才意识到,build_phase执行时,子组件还没创建完呢!正确的做法是:build_phase只做创建和配置,连接的事情留给connect_phase。
另外,run_phase是唯一一个消耗仿真时间的phase。其他phase都是function,不消耗时间。这个设计很巧妙——把“配置”和“运行”彻底分开,代码结构清晰多了。
2.3 factory机制初识——UVM的“对象工厂”
factory机制,是UVM最强大的特性之一。它让你可以在不修改代码的情况下,替换掉验证环境中的任何组件。
举个例子:你有一个base_test,里面用了my_driver。现在你想测试一个新的协议变种,需要换成my_new_driver。传统做法是修改base_test的代码。但有了factory,你只需要在命令行加一个参数:
// 在test中注册
class my_test extends uvm_test;
`uvm_component_utils(my_test)
function void build_phase(uvm_phase phase);
// 设置类型覆盖
set_type_override_by_type(
my_driver::get_type(),
my_new_driver::get_type()
);
super.build_phase(phase);
endfunction
endclass
这样,所有原本创建my_driver的地方,都会自动变成my_new_driver。是不是很神奇?
factory的核心就是两个宏:
`uvm_object_utils:用于注册uvm_object派生类`uvm_component_utils:用于注册uvm_component派生类
这两个宏做了三件事:
- 注册类名到factory的查找表中
- 实现create()方法(用于动态创建对象)
- 实现get_type()方法(返回类型信息)
小技巧:
我建议你在写每个组件时,都养成习惯加上`uvm_component_utils或`uvm_object_utils。哪怕你现在不需要factory覆盖,将来项目迭代时,这个注册会给你省下大量改代码的时间。
最后,我用一张图来总结今天的内容:
这张图把今天讲的三个核心概念串起来了。你仔细看:uvm_component和uvm_object是继承关系,phase机制控制着组件的生命周期,factory机制则提供了灵活的对象创建方式。三者缺一不可。
好了,今天的内容就到这里。记住:搞懂这三个概念,你就掌握了UVM的骨架。剩下的,就是往里面填具体的验证逻辑了。
课后思考:
如果你要设计一个自定义的monitor,它应该继承自uvm_component还是uvm_object?为什么?
提示:monitor需要一直监听总线,而且需要连接到验证环境的树上。
可可精品资料库