一、搭建第一个UVM环境:从零开始
说实话,我第一次接触UVM的时候,看着那一堆类名——uvm_component、uvm_sequence、uvm_driver——头都大了。但后来我发现,只要理解了UVM的"骨架",搭建环境其实就是往骨架上填肉。
这一章,我们就来搭建第一个完整的UVM环境。我会带着你,从DUT开始,一步步把Transaction、Sequence、Driver、Monitor、Agent、Env、Test这些组件串起来。
1.1 DUT简介:我们要验证什么?
先说说DUT(Design Under Test)。说白了,就是被验证的设计。我习惯用一个简单的FIFO作为入门例子,因为它逻辑清晰,接口也典型。
DUT功能描述:
- 同步FIFO,深度16
- 数据位宽8bit
- 支持写使能、读使能
- 输出空标志、满标志
嗯,这里要注意:DUT的接口定义直接决定了你的验证环境怎么搭。我在项目中见过有人把DUT接口搞错了,结果整个验证环境重写了一遍。所以,先花10分钟把DUT的接口搞清楚,绝对值得。
1.2 Transaction和Sequence:数据长什么样?
Transaction,就是你要发给DUT的数据包。在UVM里,它继承自uvm_sequence_item。
class fifo_trans extends uvm_sequence_item;
rand bit [7:0] data_in;
rand bit wr_en;
rand bit rd_en;
`uvm_object_utils_begin(fifo_trans)
`uvm_field_int(data_in, UVM_ALL_ON)
`uvm_field_int(wr_en, UVM_ALL_ON)
`uvm_field_int(rd_en, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "fifo_trans");
super.new(name);
endfunction
endclass
Sequence呢?它负责生成一串Transaction。我习惯把Sequence想象成"剧本",它告诉Driver:接下来该发什么数据了。
class fifo_sequence extends uvm_sequence #(fifo_trans);
`uvm_object_utils(fifo_sequence)
function new(string name = "fifo_sequence");
super.new(name);
endfunction
virtual task body();
fifo_trans tr;
repeat(10) begin
tr = fifo_trans::type_id::create("tr");
start_item(tr);
// 随机化,生成数据
assert(tr.randomize());
finish_item(tr);
end
endtask
endclass
你想想看,Sequence和Transaction的关系,就像工厂和产品。Sequence是流水线,Transaction是流水线上一个个的零件。
1.3 Driver和Monitor:数据的搬运工和观察者
Driver,负责把Transaction里的数据,按照接口时序,驱动到DUT的管脚上。Monitor,负责从DUT的管脚上采集数据,打包成Transaction。
我曾经在一个项目中,Driver和Monitor的时序写反了,结果仿真波形看起来都对,但Scoreboard就是报错。查了两天才发现,原来是Monitor采样点差了半个时钟周期。
class fifo_driver extends uvm_driver #(fifo_trans);
`uvm_component_utils(fifo_driver)
virtual fifo_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
drive_item(req);
seq_item_port.item_done();
end
endtask
virtual task drive_item(fifo_trans tr);
@(posedge vif.clk);
vif.wr_en <= tr.wr_en;
vif.rd_en <= tr.rd_en;
vif.data_in <= tr.data_in;
endtask
endclass
个人经验:Driver里一定要加时序检查。我习惯在drive_item里加一个assert,确保驱动时序和DUT的spec一致。否则,时序错了,验证环境跑得再欢也是白搭。
Monitor的代码和Driver有点像,但方向相反:
class fifo_monitor extends uvm_monitor;
`uvm_component_utils(fifo_monitor)
virtual fifo_if vif;
uvm_analysis_port #(fifo_trans) mon_ap;
function new(string name, uvm_component parent);
super.new(name, parent);
mon_ap = new("mon_ap", this);
endfunction
virtual task run_phase(uvm_phase phase);
fifo_trans tr;
forever begin
@(posedge vif.clk);
if (vif.wr_en || vif.rd_en) begin
tr = fifo_trans::type_id::create("tr");
tr.wr_en = vif.wr_en;
tr.rd_en = vif.rd_en;
tr.data_in = vif.data_in;
mon_ap.write(tr);
end
end
endtask
endclass
1.4 Agent和Env:打包和组装
Agent,就是把Driver、Monitor、Sequencer打包在一起。为什么要有Agent?说白了,就是为了复用。一个Agent对应一种接口协议。比如,你有一个AXI Agent,一个APB Agent,以后换个项目,直接拿来用就行。
class fifo_agent extends uvm_agent;
`uvm_component_utils(fifo_agent)
fifo_driver driver;
fifo_monitor monitor;
uvm_sequencer #(fifo_trans) sequencer;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
driver = fifo_driver::type_id::create("driver", this);
monitor = fifo_monitor::type_id::create("monitor", this);
sequencer = uvm_sequencer #(fifo_trans)::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,就是环境。它把所有的Agent、Scoreboard、Reference Model都组装在一起。我习惯把Env想象成"总装车间",所有组件在这里完成最后的连接。
class fifo_env extends uvm_env;
`uvm_component_utils(fifo_env)
fifo_agent agent;
fifo_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
agent = fifo_agent::type_id::create("agent", this);
sb = fifo_scoreboard::type_id::create("sb", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
agent.monitor.mon_ap.connect(sb.analysis_export);
endfunction
endclass
1.5 Test和Testbench:启动一切
Test,是UVM环境的入口。它负责配置、启动Sequence。Testbench,是顶层模块,它实例化DUT和接口,然后调用run_test。
class fifo_test extends uvm_test;
`uvm_component_utils(fifo_test)
fifo_env env;
fifo_sequence seq;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
env = fifo_env::type_id::create("env", this);
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
seq = fifo_sequence::type_id::create("seq");
seq.start(env.agent.sequencer);
#100;
phase.drop_objection(this);
endtask
endclass
Testbench顶层:
module top;
logic clk;
logic rst_n;
fifo_if vif(clk, rst_n);
fifo dut(.clk(clk), .rst_n(rst_n), .*);
initial begin
clk = 0;
forever #5 clk = ~clk;
end
initial begin
uvm_config_db #(virtual fifo_if)::set(null, "uvm_test_top.env.agent.*", "vif", vif);
run_test("fifo_test");
end
endmodule
避坑指南:我曾经忘记在Test里raise_objection,结果仿真一启动就结束了,Sequence根本没执行。记住:只要你的Test里有动态行为(比如发Sequence),就必须raise_objection,告诉UVM:"别急着结束,我还有活要干!"
1.6 知识体系总览
下面这张图,是我自己总结的UVM环境搭建路线图。每次搭新环境,我都会对照着检查一遍,确保没有遗漏。
从这张图可以看得很清楚:Testbench在最顶层,它实例化DUT和接口,然后启动Test。Test里创建Env,Env里包含Agent,Agent里又包含Sequencer、Driver、Monitor。Driver和Monitor直接和DUT打交道。
我个人习惯,每次搭新环境时,都从这张图出发,先确定DUT的接口,然后定义Transaction,接着写Driver和Monitor,再打包成Agent,最后组装成Env和Test。按这个顺序来,思路特别清晰。
好了,第一个UVM环境就搭好了。虽然看起来组件很多,但每个组件各司其职,结构非常清晰。你想想看,如果没有UVM,你得自己写多少重复的代码?有了这套框架,以后换个DUT,改改Transaction和Driver的时序,其他组件基本不用动。