3、UVM环境搭建:搭建第一个UVM验证环境
好,咱们今天来动手搭第一个UVM环境。说实话,很多新手一上来就被UVM的组件树吓住了——test、env、agent、driver、monitor……一堆东西。其实你想想看,核心就三件事:创建、配置、连接。搞明白这三步,剩下的就是重复劳动。
我个人习惯,先搭一个最简的框架跑通,再往里填东西。别一上来就想搞个完美的环境,那反而容易把自己绕进去。
3.1 从uvm_test开始:验证的入口
uvm_test是整个验证环境的总指挥。它不干具体的活,但它知道该叫谁干活、按什么顺序干。
我见过不少新手把test写得巨复杂,又是建sequence又是搞scoreboard。其实test就两个职责:创建env,启动sequence。别的别往里塞。
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_env env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(env.agent.sqr);
phase.drop_objection(this);
endtask
endclass
注意看,build_phase里只创建了env。agent、driver那些东西,让env去管。这叫职责分层——test只管战略,env管战术。
3.2 uvm_env:验证环境的骨架
uvm_env就是你的验证环境本体。它里面装什么?agent、reference model、scoreboard、coverage collector。但第一次搭建,我建议只放一个agent就够了。跑通了再慢慢加。
env的build_phase里,要创建所有子组件。connect_phase里,要把它们连起来。这里有个坑——connect_phase的执行顺序。我曾经在项目里吃过亏:在connect_phase里访问了还没创建好的组件,结果仿真直接崩了。
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent agent;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = my_agent::type_id::create("agent", this);
endfunction
function void connect_phase(uvm_phase phase);
// 这里可以做一些agent内部的连接
// 但注意:agent内部的组件此时已经创建好了
endfunction
endclass
3.3 uvm_agent:真正的干活人
agent是UVM里最常用的组件。它封装了driver、monitor、sequencer这三件套。说白了,agent就是一个小分队——driver负责发数据,monitor负责看数据,sequencer负责排任务。
我建议把agent分成两种:active和passive。active agent会驱动DUT,passive agent只观察。这个区分很重要,因为验证环境里经常需要多个agent,有些是主动的,有些是被动的。
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver driver;
my_monitor monitor;
my_sequencer sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
monitor = my_monitor::type_id::create("monitor", this);
if(get_is_active() == UVM_ACTIVE) begin
driver = my_driver::type_id::create("driver", this);
sqr = my_sequencer::type_id::create("sqr", this);
end
endfunction
function void connect_phase(uvm_phase phase);
if(get_is_active() == UVM_ACTIVE) begin
driver.seq_item_port.connect(sqr.seq_item_export);
end
endfunction
endclass
看到没?只有active agent才创建driver和sequencer。passive agent只需要monitor。这个设计很巧妙——同样的agent代码,通过一个`get_is_active()`就能切换模式。
3.4 组件之间的连接:TLM通信
组件搭好了,怎么连起来?UVM用的是TLM(Transaction Level Modeling)。说白了就是端口对接——一个组件往外发transaction,另一个组件往里收。
最常见的连接方式:
| 连接类型 | 端口 | 方向 | 典型用途 |
|---|---|---|---|
| seq_item_port | driver → sequencer | 请求 | driver向sequencer要transaction |
| analysis_port | monitor → scoreboard | 广播 | monitor把采集到的数据发给多个组件 |
| analysis_export | scoreboard → monitor | 接收 | scoreboard接收monitor发来的数据 |
嗯,这里要注意:analysis_port是一对多的。一个monitor可以同时把数据发给scoreboard、coverage collector、甚至reference model。这个特性在复杂验证环境里特别有用。
3.5 完整的环境搭建流程
好,咱们把上面这些串起来,看看一个完整的搭建流程:
- 写test:创建env,启动sequence
- 写env:创建agent,后续再加其他组件
- 写agent:创建driver、monitor、sequencer
- 写driver:实现与DUT的接口驱动
- 写monitor:采集DUT的输入输出
- 写sequencer:管理sequence的调度
- 连接:在connect_phase里完成TLM端口对接
这个顺序是自顶向下的。我建议你也按这个顺序写代码——先写test,再写env,最后写agent和底层的driver。这样每写一层,你都知道上一层需要什么接口。
3.6 避坑指南
最后分享几个我踩过的坑:
- 别在build_phase里做连接:build_phase只负责创建,connect_phase才负责连接。混在一起,调试起来会疯掉。
- 注意phase的执行顺序:build_phase是自顶向下,connect_phase是自底向上。这个顺序决定了你什么时候能访问什么组件。
- 别忘了`uvm_component_utils:每个组件都要注册。我刚开始学UVM时,经常忘了加这个宏,然后仿真报错说找不到组件,排查半天。
- agent的is_active要设置:默认是UVM_ACTIVE。如果你想要passive agent,记得在test里设置`env.agent.set_is_active(UVM_PASSIVE)`。
说实话,UVM环境搭建这件事,第一次做会觉得繁琐。但当你搭过两三个项目后,就会发现——套路都是一样的。test、env、agent、driver、monitor、sequencer,这六个组件就是UVM的六脉神剑。搞明白它们怎么配合,剩下的就是填业务逻辑了。
下一章,咱们会深入driver和monitor的实现,看看它们怎么跟DUT打交道。到时候我会分享一个我在项目中遇到的时序问题——那个问题让我debug了整整三天,最后发现是driver的复位处理没做好。