一、UVM核心组件(上):从uvm_component到uvm_monitor
各位同学好,我是老李。今天咱们聊聊UVM里最基础也最绕不开的几个组件。说实话,我刚开始学UVM那会儿,也被uvm_component和uvm_object的区别搞得晕头转向。后来踩了几个坑,才慢慢摸清楚门道。
1.1 uvm_component vs uvm_object:到底差在哪?
这个问题,几乎每个UVM新手都会问。我个人的理解是这样的:
uvm_object是UVM中最基本的类。它没有时间概念,不参与仿真过程。说白了,它就是用来装数据的。比如你定义一个transaction,里面放地址、数据、控制信号,这就是典型的uvm_object。
uvm_component则不一样。它是有生命的,有仿真阶段的(phase)。它会创建、运行、销毁。你想想看,driver、monitor、agent这些,哪个不是在仿真过程中干活的?它们就是uvm_component。
核心区别一句话总结:
- uvm_object:数据载体,不参与仿真流程
- uvm_component:功能单元,参与仿真,有层次结构
我在项目中遇到过这样一个坑:有人把monitor写成了uvm_object,结果发现它根本收不到transaction。为什么?因为uvm_object没有phase机制,没法在run_phase里干活。嗯,这里要注意,凡是需要在仿真过程中持续运行的,必须是uvm_component。
来看个代码对比:
// uvm_object:纯数据
class my_transaction extends uvm_object;
rand bit [31:0] addr;
rand bit [31:0] data;
`uvm_object_utils(my_transaction)
endclass
// uvm_component:有行为的组件
class my_driver extends uvm_component;
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
// 这里才能干活
endtask
endclass
你看,uvm_object用uvm_object_utils注册,uvm_component用uvm_component_utils注册。这个区别不是随便定的,它背后是UVM的工厂机制在起作用。
1.2 uvm_agent:把组件串起来
agent是什么?说白了就是一个容器。它把driver、monitor、sequencer这些组件打包在一起。我习惯把agent理解成一个"小环境",负责管理某个接口的所有组件。
举个例子,你的DUT有一个AXI接口,那你就需要一个AXI agent。这个agent里面包含:
- 一个driver:负责驱动信号
- 一个monitor:负责监视信号
- 一个sequencer:负责管理sequence
agent有两种模式:active和passive。active模式下,agent会包含driver和sequencer,可以主动发送激励。passive模式下,只有monitor,只做观察。这个设计很巧妙,我在做验证IP复用时经常用到。
class my_agent extends uvm_agent;
my_driver drv;
my_monitor mon;
my_sequencer seqr;
`uvm_component_utils(my_agent)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = my_monitor::type_id::create("mon", this);
if(get_is_active() == UVM_ACTIVE) begin
drv = my_driver::type_id::create("drv", this);
seqr = my_sequencer::type_id::create("seqr", this);
end
endfunction
endclass
注意看,我用了get_is_active()来判断模式。这个函数是uvm_agent自带的,你不需要自己实现。我曾经见过有人自己写了个变量来控制模式,结果跟UVM内置机制冲突了,调试了半天。
1.3 uvm_driver:激励的搬运工
driver的任务很简单:从sequencer拿到transaction,然后驱动到DUT的接口上。但简单不代表容易。我见过太多人把driver写得又臭又长。
好的driver应该遵循几个原则:
- 只做驱动,不做判断
- 只处理时序,不处理数据
- 只负责发送,不负责检查
来看一个标准的driver模板:
class my_driver extends uvm_driver #(my_transaction);
virtual my_if vif;
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db #(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "virtual interface not found")
endfunction
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
drive_transaction(req);
seq_item_port.item_done();
end
endtask
task drive_transaction(my_transaction tr);
@(posedge vif.clk);
vif.addr <= tr.addr;
vif.data <= tr.data;
vif.valid <= 1'b1;
@(posedge vif.clk);
vif.valid <= 1'b0;
endtask
endclass
这里有个细节:seq_item_port.get_next_item和item_done必须成对出现。我曾经漏写过item_done,结果sequencer卡死了,整个仿真跑不下去。这种错误很难查,因为UVM不会报错,只是默默等待。
1.4 uvm_monitor:安静的观察者
monitor和driver正好相反。driver是往外发信号,monitor是往里收信号。它默默地观察接口上的信号变化,然后打包成transaction,发给scoreboard或者coverage collector。
monitor的设计要点:
- 不能修改信号,只能观察
- 要能处理各种时序异常
- 最好支持协议检查
个人经验:我习惯在monitor里加一个简单的协议检查。比如检查地址对齐、数据有效信号是否在正确的时间拉高。这样可以在早期发现问题,不用等到scoreboard去比对。
class my_monitor extends uvm_monitor;
virtual my_if vif;
uvm_analysis_port #(my_transaction) ap;
`uvm_component_utils(my_monitor)
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
forever begin
my_transaction tr = my_transaction::type_id::create("tr");
@(posedge vif.clk);
if(vif.valid) begin
tr.addr = vif.addr;
tr.data = vif.data;
ap.write(tr);
end
end
endtask
endclass
注意看,monitor通过uvm_analysis_port把数据发出去。这个port是广播式的,可以连到多个组件。我在项目中经常一个monitor同时连scoreboard和coverage collector,省事。
避坑指南:我曾经在monitor里直接用了$monitor系统函数,结果仿真速度慢得像蜗牛。后来才意识到,UVM有自己的观察机制,不要用Verilog的那一套。用uvm_analysis_port才是正道。
知识体系总览
下面这张图,是我自己总结的UVM核心组件关系。你看一眼,应该能明白它们是怎么配合的。
这张图里,我特意把uvm_object和uvm_component放在最上面,因为它们是所有组件的基础。往下看,agent包含了driver和monitor,而driver又和sequencer通过TLM通信。最后,driver和monitor都跟DUT打交道——一个发信号,一个收信号。
好了,这一章的内容就到这里。记住一句话:uvm_object装数据,uvm_component干活,agent打包,driver发,monitor收。这个口诀我用了好多年,从来没出过错。