2. 搭建你的第一个UVM环境:从零开始,构建一个最简单的UVM testbench,理解uvm_component与uvm_object
说实话,很多初学者一上来就被UVM的庞大体系吓住了。什么factory、sequence、driver、monitor……一堆概念砸过来,很容易懵。
我个人习惯是:先别管那么多,动手搭一个能跑的环境出来。哪怕它再简陋,只要能跑通,你就已经迈过了最难的坎。
这一章,我们就从零开始,构建一个最简单的UVM testbench。目标只有一个:让一个transaction从driver发出去,在monitor里被看到。仅此而已。
2.1 先搞清楚两个核心概念:uvm_component 和 uvm_object
在动手之前,我们得先弄明白UVM世界里最基础的两个类。说白了,整个UVM环境就是由这两种东西拼起来的。
核心区别一句话总结:
uvm_component—— 有生命周期的,有层次结构的,需要被build、connect、run。比如driver、monitor、agent。uvm_object—— 没有生命周期的,没有层次结构的,用来封装数据。比如transaction、sequence_item。
嗯,这里要注意:uvm_component 是 uvm_object 的子类。但UVM把它们的职责分得很清楚。你想想看,一个driver需要被创建、连接、运行,它得有“活着”的概念。而一个transaction只是一包数据,发完就完事了,不需要被“运行”。
我在项目中遇到过有人把transaction写成component,结果整个环境跑起来奇慢无比,还各种报错。原因就是component有phase机制,每创建一个component都会触发一堆回调,而object没有这些开销。
2.2 我们的第一个UVM环境长什么样?
先画个图,让你对整体有个感觉。这个环境非常简单,只有四个部分:
这个结构很清晰:test → env → agent → driver + monitor。每个组件都是上一级的“孩子”。
2.3 第一步:定义我们的transaction(uvm_object)
先写数据包。这个transaction很简单,就一个数据域和一个控制信号。
class my_transaction extends uvm_sequence_item;
rand bit [7:0] data;
rand bit valid;
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(valid, 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 的子类。为什么不用直接 uvm_object?因为 uvm_sequence_item 提供了sequence机制的支持,以后扩展起来方便。
小技巧: 我习惯在transaction里加上 do_print 函数,方便调试时看数据内容。虽然UVM有默认的print机制,但自定义格式更清晰。
2.4 第二步:写driver(uvm_component)
driver负责从sequencer拿transaction,然后“驱动”到接口上。但我们现在没有接口,所以先简单打印出来。
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);
phase.raise_objection(this);
repeat(5) begin
seq_item_port.get_next_item(req);
$display("[Driver] Got transaction: data = %0d, valid = %0b", req.data, req.valid);
seq_item_port.item_done();
end
phase.drop_objection(this);
endtask
endclass
这里有个关键点:uvm_driver #(my_transaction) 这个参数化写法。它告诉UVM这个driver处理的是哪种transaction。我在项目中见过有人忘了写参数,结果driver和sequencer连不上,报了一堆类型不匹配的错误。
2.5 第三步:写monitor(uvm_component)
monitor负责“监听”接口上的活动。同样,我们现在没有接口,就模拟接收一下。
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
// 模拟接收5个transaction
repeat(5) begin
#10;
$display("[Monitor] Observed activity at time %0t", $time);
end
endtask
endclass
注意: monitor里不要调用 raise_objection 和 drop_objection。objection的控制应该由test或sequence来管理。monitor只是被动观察,不控制仿真结束。
2.6 第四步:组装agent、env和test
agent把driver和monitor包在一起。env把agent包起来。test是顶层。
// agent
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver driver;
my_monitor monitor;
uvm_sequencer #(my_transaction) sequencer;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
driver = my_driver::type_id::create("driver", this);
monitor = my_monitor::type_id::create("monitor", this);
sequencer = uvm_sequencer #(my_transaction)::type_id::create("sequencer", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction
endclass
// env
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
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = my_agent::type_id::create("agent", this);
endfunction
endclass
// test
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
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
virtual function void end_of_elaboration_phase(uvm_phase phase);
uvm_top.print_topology();
endfunction
endclass
注意看 connect_phase 里的那行代码:driver.seq_item_port.connect(sequencer.seq_item_export)。这是driver和sequencer之间的“管道”。没有这行,driver就拿不到transaction。
2.7 第五步:跑起来!
最后,在顶层模块里启动UVM:
module top;
initial begin
run_test("my_test");
end
endmodule
用VCS或QuestaSim跑一下,你会看到类似这样的输出:
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
[Driver] Got transaction: data = 123, valid = 1
[Monitor] Observed activity at time 10
[Driver] Got transaction: data = 45, valid = 0
[Monitor] Observed activity at time 20
...
看到这个输出,恭喜你,你的第一个UVM环境跑通了!
2.8 避坑指南:我踩过的几个坑
- 忘记调用
super.build_phase:这是最常见的错误。如果不调,父类的build不会执行,factory机制会出问题。 - objection管理不当:我曾经在一个项目里忘了在test里raise objection,结果仿真一启动就结束了,什么都没看到。
- 类型不匹配:driver和sequencer的参数类型必须一致。比如driver是
#(my_transaction),sequencer也得是#(my_transaction)。
2.9 小结
这一章我们做了三件事:
- 理解了
uvm_component和uvm_object的本质区别 - 搭建了一个完整的UVM环境(虽然很简单)
- 看到了transaction从driver发出的全过程
这个环境虽然简陋,但它包含了UVM的核心骨架。后面的所有内容,都是在这个骨架上添砖加瓦。
记住: 能跑通的环境就是好环境。别追求一开始就完美,先跑起来再说。
可可精品资料库