第四章:验证环境架构:从零搭建一个基于UVM的验证环境
说实话,很多刚入行的朋友问我:「UVM环境到底该怎么搭?」
我的回答通常是:别急着抄代码,先想清楚你要测什么。我在项目里见过太多人,一上来就搭了个巨复杂的环境,结果连基本功能都跑不通。嗯,今天我们就从零开始,一步步搭一个真正能用的UVM验证环境。
4.1 验证环境的核心组件
一个标准的UVM环境,说白了就这几样东西:
- testbench顶层:连接DUT和验证环境的桥梁
- interface:信号驱动和采样的管道
- transaction:描述数据包的类
- driver:把transaction变成时序信号
- monitor:把时序信号变成transaction
- agent:把driver和monitor打包在一起
- scoreboard:比对期望值和实际值
- env:把agent、scoreboard等组件组装起来
- test:配置环境、启动sequence
你想想看,这就像搭积木。每个组件都有明确的职责,谁也别越界。我在项目中遇到过有人把driver和monitor写在一起,结果调试时根本分不清是驱动问题还是采样问题——血的教训啊。
4.2 第一步:定义transaction
一切从数据开始。我们先定义一个简单的AXI-like transaction:
class my_transaction extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit wr_en; // 1: write, 0: read
rand bit [3:0] burst_len;
constraint c_addr_aligned {
addr[1:0] == 2'b0;
}
constraint c_burst_range {
burst_len inside {[1:8]};
}
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(wr_en, UVM_ALL_ON)
`uvm_field_int(burst_len, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "my_transaction");
super.new(name);
endfunction
endclass
这里有个小细节:uvm_field_*宏不是必须的,但我建议你加上。为什么?因为后面做compare、print、copy时,这些宏能省你大量时间。我曾经为了省事没加,结果调试时手动打印transaction,那叫一个痛苦。
4.3 第二步:搭建interface
interface是验证环境和DUT之间的物理连接。我个人习惯把interface分成两块:
- 驱动信号:从driver到DUT
- 采样信号:从DUT到monitor
interface my_if (input clk, input rst_n);
logic [31:0] addr;
logic [31:0] wdata;
logic [31:0] rdata;
logic wr_en;
logic ready;
logic valid;
// 时钟块,用于驱动和采样
clocking drv_cb @(posedge clk);
output addr, wdata, wr_en;
input ready;
endclocking
clocking mon_cb @(posedge clk);
input addr, wdata, rdata, wr_en, ready, valid;
endclocking
modport DRV (clocking drv_cb);
modport MON (clocking mon_cb);
endinterface
注意这里的clocking block。很多新手觉得它多余,直接用手动时序控制。但我在项目中吃过亏——手动控制时序很容易出现竞争,尤其是当DUT和验证环境跑在不同时钟域时。clocking block能帮你自动处理采样边沿,避免亚稳态问题。
4.4 第三步:实现driver和monitor
driver负责把transaction变成时序信号。核心逻辑在run_phase里:
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
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);
@(vif.drv_cb);
vif.drv_cb.addr <= tr.addr;
vif.drv_cb.wdata <= tr.data;
vif.drv_cb.wr_en <= tr.wr_en;
// 等待ready信号
while (!vif.drv_cb.ready) @(vif.drv_cb);
endtask
endclass
monitor则是反过来,把时序信号转成transaction:
class my_monitor extends uvm_monitor;
virtual my_if vif;
uvm_analysis_port #(my_transaction) mon_ap;
`uvm_component_utils(my_monitor)
function new(string name, uvm_component parent);
super.new(name, parent);
mon_ap = new("mon_ap", this);
endfunction
task run_phase(uvm_phase phase);
my_transaction tr;
forever begin
@(vif.mon_cb);
if (vif.mon_cb.valid) begin
tr = my_transaction::type_id::create("tr");
tr.addr = vif.mon_cb.addr;
tr.data = vif.mon_cb.wr_en ? vif.mon_cb.wdata : vif.mon_cb.rdata;
tr.wr_en = vif.mon_cb.wr_en;
mon_ap.write(tr);
end
end
endtask
endclass
这里有个关键点:monitor里用了uvm_analysis_port。为什么不用普通的fifo?因为analysis port支持多消费者——你可以同时连到scoreboard和coverage collector,而不用改monitor的代码。这就是UVM的灵活性。
4.5 第四步:组装agent和env
agent就是把driver和monitor打包,再加上sequencer:
class my_agent extends uvm_agent;
my_driver drv;
my_monitor mon;
uvm_sequencer #(my_transaction) sqr;
`uvm_component_utils(my_agent)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
drv = my_driver::type_id::create("drv", this);
mon = my_monitor::type_id::create("mon", this);
sqr = uvm_sequencer #(my_transaction)::type_id::create("sqr", this);
endfunction
function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
env则是整个验证环境的顶层容器:
class my_env extends uvm_env;
my_agent agt;
my_scoreboard scb;
`uvm_component_utils(my_env)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
agt = my_agent::type_id::create("agt", this);
scb = my_scoreboard::type_id::create("scb", this);
endfunction
function void connect_phase(uvm_phase phase);
agt.mon.mon_ap.connect(scb.exp_ap);
endfunction
endclass
重要原则:env只负责组件的创建和连接,不包含任何业务逻辑。业务逻辑应该放在scoreboard、coverage等组件里。这样当你要换DUT时,只需要改env的连接方式,核心验证逻辑不用动。
4.6 第五步:编写test和sequence
test是验证的入口。它负责配置环境、启动sequence:
class my_test extends uvm_test;
my_env env;
`uvm_component_utils(my_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_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.agt.sqr);
phase.drop_objection(this);
endtask
endclass
sequence则定义了具体的激励场景:
class my_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(my_sequence)
function new(string name = "my_sequence");
super.new(name);
endfunction
task body();
my_transaction tr;
// 写操作
tr = my_transaction::type_id::create("tr");
tr.addr = 32'h1000;
tr.data = 32'hA5A5A5A5;
tr.wr_en = 1;
tr.burst_len = 4;
start_item(tr);
finish_item(tr);
// 读操作
tr = my_transaction::type_id::create("tr");
tr.addr = 32'h1000;
tr.wr_en = 0;
tr.burst_len = 1;
start_item(tr);
finish_item(tr);
endtask
endclass
个人经验:sequence里不要写死地址和数据。我一般会在sequence里加一些randomize约束,或者从外部配置文件读取参数。这样同一个sequence可以复用在不同的测试场景中。
4.7 验证环境的启动流程
最后,我们需要一个顶层模块来启动整个验证:
module top;
logic clk, rst_n;
my_if if_inst(clk, rst_n);
my_dut dut_inst(
.clk(clk),
.rst_n(rst_n),
.addr(if_inst.addr),
.wdata(if_inst.wdata),
.rdata(if_inst.rdata),
.wr_en(if_inst.wr_en),
.ready(if_inst.ready),
.valid(if_inst.valid)
);
initial begin
clk = 0;
forever #5 clk = ~clk;
end
initial begin
rst_n = 0;
#20 rst_n = 1;
end
initial begin
uvm_config_db #(virtual my_if)::set(null, "*.agt.*", "vif", if_inst);
run_test("my_test");
end
endmodule
避坑指南:我曾经在顶层模块里忘记设置uvm_config_db,结果driver和monitor拿到的vif是null,仿真直接崩溃。检查了整整一天才发现问题。所以我的习惯是:在build_phase一开始就检查vif是否为空,不为空才继续往下走。
4.8 环境架构的检查清单
搭建完环境后,我建议你对照这个清单检查一遍:
| 检查项 | 说明 | 常见问题 |
|---|---|---|
| transaction完整性 | 所有字段都有约束和field宏 | 漏掉field宏导致compare失败 |
| interface连接 | driver和monitor使用不同的clocking block | 共用clocking导致驱动和采样冲突 |
| agent结构 | driver、monitor、sequencer三者齐全 | 忘记连接seq_item_port |
| env连接 | monitor的analysis port正确连接到scoreboard | 连接顺序错误导致数据丢失 |
| test启动 | 有raise/drop objection | 忘记drop objection导致仿真不结束 |
嗯,到这里,一个完整的UVM验证环境就搭好了。你可能会觉得步骤有点多,但相信我——一旦你理解了每个组件的职责,搭建环境就像搭积木一样自然。我在带新人时,通常让他们先手写一遍这个流程,而不是直接复制粘贴。因为只有亲手敲过代码,才能真正理解UVM的设计哲学。
下一章我们会深入讨论如何编写高效的sequence和scoreboard。到时候你会发现,环境搭好了,真正的挑战才刚刚开始。