4. UVM phase机制:phase分类与执行顺序、run_phase与task phase、phase跳转与控制
UVM的phase机制,说白了就是给验证环境装了一个「调度器」。它规定了每个组件在什么时候干活、干完活怎么交接。我刚学UVM那会儿,总觉得phase是个麻烦事——不就是顺序执行吗?后来被一个时序bug折磨了三天,才发现是自己对phase理解不到位。
嗯,咱们今天就把这个「调度器」彻底讲透。
4.1 phase的分类:你该在哪个阶段干活?
UVM把仿真过程分成了两大类phase:
- function phase:不消耗仿真时间,说白了就是「一瞬间」完成的事。比如建环境、连组件、配参数。
- task phase:消耗仿真时间,可以等时钟、等信号、等事件。比如驱动激励、监测数据。
我个人习惯把function phase叫做「搭台子阶段」,task phase叫做「唱戏阶段」。台子搭不好,戏肯定唱砸。
具体有哪些phase?我整理了一张表,你收藏好:
| phase名称 | 类型 | 主要用途 |
|---|---|---|
| build_phase | function | 创建组件、配置参数 |
| connect_phase | function | 连接TLM端口、建立通信 |
| end_of_elaboration_phase | function | 做最终检查、打印环境拓扑 |
| start_of_simulation_phase | function | 仿真开始前的初始化 |
| run_phase | task | 主仿真过程(核心) |
| extract_phase | function | 从监测器提取数据 |
| check_phase | function | 检查数据完整性、协议正确性 |
| report_phase | function | 生成测试报告 |
| final_phase | function | 收尾工作、关闭文件 |
重点提醒:build_phase是自顶向下执行的,而connect_phase是自底向上执行的。这个顺序搞反了,你的TLM端口就接不上。我曾经在这个坑里爬了整整一个下午。
4.2 run_phase与task phase:到底有啥区别?
很多新手会问:run_phase不就是task phase吗?为什么还要单独拎出来讲?
其实UVM在run_phase之外,还提供了12个细分的task phase。比如:
- pre_reset_phase / reset_phase / post_reset_phase
- pre_configure_phase / configure_phase / post_configure_phase
- pre_main_phase / main_phase / post_main_phase
- pre_shutdown_phase / shutdown_phase / post_shutdown_phase
你想想看,如果你的DUT需要先复位、再配置、再发激励、最后关断,用这些细分phase是不是特别清晰?
我个人习惯是:简单的测试用run_phase就够了,复杂的场景一定要用细分phase。为什么?因为细分phase可以让你精确控制每个阶段的起止时间,而且不同组件之间天然同步。
举个例子:
class my_driver extends uvm_driver#(my_transaction);
`uvm_component_utils(my_driver)
// 用main_phase替代run_phase
task main_phase(uvm_phase phase);
phase.raise_objection(this);
// 发送激励
forever begin
seq_item_port.get_next_item(req);
drive_transaction(req);
seq_item_port.item_done();
end
phase.drop_objection(this);
endtask
endclass
小技巧:如果你用细分phase,记得在对应的phase里raise/drop objection。我曾经见过有人只在run_phase里raise objection,结果细分phase里的代码根本没执行——因为仿真早就结束了。
4.3 phase的执行顺序:谁先谁后?
UVM的phase执行顺序是固定的,你不能乱改。但理解这个顺序背后的逻辑,比死记硬背重要得多。
我画了一张图,帮你理清思路:
从这张图你能看到:function phase先执行,把环境搭好;然后进入task phase,跑真正的仿真;最后再回到function phase,做收尾工作。
注意:run_phase和细分task phase是并行执行的!也就是说,如果你同时在run_phase和main_phase里写了代码,它们会一起跑。这很容易导致激励冲突。我的建议是:要么只用run_phase,要么只用细分phase,别混着用。
4.4 phase跳转与控制:让仿真更灵活
有时候,你需要在仿真过程中「跳回」某个phase。比如DUT突然需要重新复位,或者配置参数需要更新。UVM提供了phase跳转机制来解决这个问题。
常用的跳转方法有:
- phase.jump():直接跳转到指定phase。比如从main_phase跳回reset_phase。
- phase.raise_objection() / drop_objection():控制phase的结束时间。没有raise objection,phase会瞬间结束。
我举个例子,你感受一下:
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
task main_phase(uvm_phase phase);
// 监测到复位信号,跳回reset_phase
forever begin
@(posedge vif.reset_n);
if (vif.reset_n == 1'b0) begin
`uvm_info("MONITOR", "检测到复位,跳回reset_phase", UVM_MEDIUM)
phase.jump(uvm_reset_phase::get());
end
end
endtask
endclass
避坑指南:我曾经在项目里用phase.jump()做复位重试,结果发现跳转后之前的phase状态没有完全清理干净,导致一些队列里还残留着旧数据。后来我养成了一个习惯:跳转前先调用phase.reset(),把状态清干净。
另外,objection机制是phase控制的核心。记住一句话:没有objection,就没有仿真时间。如果你在run_phase里忘了raise objection,仿真器会直接跳过run_phase,你的激励代码根本不会执行。
我一般会在test层统一管理objection:
class my_test extends uvm_test;
`uvm_component_utils(my_test)
task run_phase(uvm_phase phase);
phase.raise_objection(this);
// 等待所有sequence完成
#1000;
phase.drop_objection(this);
endtask
endclass
这样写的好处是,你不需要在每个driver、monitor里都去raise/drop objection,管理起来更集中。当然,如果你的场景比较复杂,也可以在组件级别分别控制。
嗯,phase机制的内容差不多就这些。记住三个核心点:分类要清楚、顺序不能乱、objection不能忘。把这三点吃透了,UVM的phase对你来说就不再是黑盒。
最后说一句:phase机制是UVM的骨架,理解它比记住API更重要。我见过太多人把UVM用成了「高级的Verilog」,就是因为没搞懂phase的调度逻辑。花点时间把这张图刻在脑子里,后面学sequence、学reg模型都会轻松很多。