4. UVM组件家族(下):深入理解uvm_sequencer、uvm_sequence、uvm_scoreboard、uvm_env、uvm_test
好,咱们接着聊UVM组件家族的下半部分。上一章我们把driver、monitor、agent这些跑腿的伙计们捋清楚了。这一章要聊的这几个,才是真正的大脑和心脏——sequencer、sequence、scoreboard、env、test。说白了,前面那些是手和脚,后面这些才是做决策、做检查、搭框架的。
4.1 uvm_sequencer:指挥交通的调度员
Sequencer这玩意儿,我刚开始学的时候总觉得它可有可无。不就是把sequence产生的transaction发给driver吗?我自己写个fifo不就行了?后来被现实狠狠教育了一回——当你同时跑十几个sequence的时候,没有sequencer帮你仲裁,代码会乱成一锅粥。
Sequencer的核心职责就两个:
- 接收来自sequence的transaction请求
- 按照优先级和仲裁策略,把transaction发给driver
你想想看,如果driver正在处理一个读操作,突然来了个写操作,谁先谁后?Sequencer就是干这个的。它内部维护了一个仲裁队列,默认是FIFO策略,但你也可以改成优先级仲裁。
实际项目中我常用的sequencer声明方式:
class my_sequencer extends uvm_sequencer #(my_transaction);
`uvm_component_utils(my_sequencer)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
注意那个参数化写法 #(my_transaction)。这个模板参数告诉sequencer:我管的是哪种类型的transaction。我曾经见过有人忘了写这个参数,结果sequence和sequencer类型对不上,编译报错找了半天。嗯,这种坑踩过一次就记住了。
4.2 uvm_sequence:造数据的工厂
Sequence是干什么的?说白了就是造transaction的。它不负责发送,只负责生成。生成完了交给sequencer,sequencer再转发给driver。
我个人的习惯是把sequence分成两类:
- 基础sequence:只产生一种类型的transaction,比如只产生读操作
- 组合sequence:把多个基础sequence串起来,模拟复杂的业务场景
一个小技巧:写sequence的时候,记得在body任务里加个超时保护。我曾经有个sequence死循环了,仿真跑了三天三夜没停,最后发现是body里忘了加`uvm_do宏的结束条件。
class my_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(my_sequence)
task body();
my_transaction tr;
repeat(10) begin
`uvm_do(tr)
// 这里可以加一些随机约束
tr.addr = $urandom_range(0, 255);
end
endtask
endclass
看到那个 repeat(10) 了吗?这就是控制sequence产生多少笔transaction的地方。实际项目中,这个数字通常是从配置里读的,而不是硬编码。
4.3 uvm_scoreboard:火眼金睛的裁判
Scoreboard,名字起得挺唬人,其实就是个比较器。它从monitor那里拿到DUT的实际输出,再跟参考模型(reference model)的预期输出做对比。对上了就pass,对不上就报错。
我在项目中遇到过最头疼的问题,不是scoreboard不会写,而是参考模型本身就有bug。你想想看,参考模型错了,scoreboard拿错误的结果去比对,那能对吗?所以我的建议是:先单独验证参考模型,再把它集成到scoreboard里。
避坑指南:我曾经犯过一个低级错误——scoreboard里用了全局变量来存预期结果。结果多个sequence同时跑的时候,全局变量被覆盖了,比对结果全乱套。后来老老实实改成用关联数组(associative array),以transaction的ID作为key来存储。
class my_scoreboard extends uvm_scoreboard;
`uvm_component_utils(my_scoreboard)
uvm_analysis_imp #(my_transaction, my_scoreboard) actual_imp;
uvm_analysis_imp #(my_transaction, my_scoreboard) expect_imp;
my_transaction expect_queue[$];
function void write_actual(my_transaction tr);
my_transaction expect_tr;
if(expect_queue.size() == 0) begin
`uvm_error("SCOREBOARD", "收到实际结果,但预期队列为空")
return;
end
expect_tr = expect_queue.pop_front();
if(tr.data !== expect_tr.data)
`uvm_error("SCOREBOARD", $sformatf("数据不匹配: actual=%0d, expect=%0d", tr.data, expect_tr.data))
endfunction
endclass
这段代码里,expect_queue是个队列,先进先出。参考模型产生的预期结果先入队,monitor抓到的实际结果来了就出队比对。这个模式叫"顺序比对",是最常用的一种。
4.4 uvm_env:搭积木的底盘
Env,全称environment,就是验证环境的顶层容器。它负责把agent、scoreboard、reference model这些组件实例化,然后连起来。
我习惯把env分成两层:
- sub_env:每个协议或每个接口对应一个子环境
- top_env:把所有的sub_env拼在一起
这样做的好处是复用。比如你有一个AXI接口和一个APB接口,各写一个sub_env。下次换个项目,接口没变,直接把sub_env拿过来用就行。
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent agent;
my_scoreboard sb;
my_ref_model ref;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = my_agent::type_id::create("agent", this);
sb = my_scoreboard::type_id::create("sb", this);
ref = my_ref_model::type_id::create("ref", this);
endfunction
function void connect_phase(uvm_phase phase);
agent.monitor.item_collected_port.connect(sb.actual_imp);
ref.expect_port.connect(sb.expect_imp);
endfunction
endclass
看到 connect_phase 了吗?这里就是把monitor和scoreboard、参考模型和scoreboard连起来的地方。端口连接是UVM里最容易出错的地方,我建议你每次写完env,先检查一遍port的宽度和方向对不对。
4.5 uvm_test:总指挥
Test是验证环境的最高层。它决定跑哪个sequence,用哪个env,配置参数是什么。一个项目里通常有几十个test,每个test对应一个测试用例。
我个人的习惯是:
- 写一个base_test,把公共的东西都放进去(比如env的创建、公共配置)
- 每个具体的test继承base_test,只写自己特有的逻辑
一个典型的base_test长这样:
class base_test extends uvm_test;
`uvm_component_utils(base_test)
my_env env;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
uvm_top.print_topology(); // 打印环境结构,方便调试
endfunction
endclass
class test_simple_rw extends base_test;
`uvm_component_utils(test_simple_rw)
task run_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(env.agent.sequencer);
phase.drop_objection(this);
endtask
endclass
注意那个 raise_objection 和 drop_objection。这是UVM里控制仿真结束的机制。如果你忘了raise,仿真可能一启动就结束了。我刚开始学的时候经常犯这个错,仿真跑了个寂寞。
4.6 组件之间的协作关系
说了这么多,咱们用一张图来总结一下这些组件是怎么配合的:
这张图里,test创建env,env创建agent和scoreboard。Sequence产生transaction,通过sequencer发给driver。Monitor抓到DUT的输出,送给scoreboard。参考模型算出预期结果,也送给scoreboard。Scoreboard一比对,结果就出来了。
说白了,整个验证环境就是一条流水线:造数据 → 发数据 → 收数据 → 比数据。每个组件各司其职,谁也别越界。我在项目中见过有人把比对逻辑写在monitor里,结果monitor变得又臭又长,维护起来想死的心都有。记住:单一职责原则,每个组件只干自己那一亩三分地的事。
最后分享一个经验:写test的时候,别忘了在run_phase里加个超时。我曾经有个test因为sequence里的循环条件写错了,仿真跑了整整一个周末。从那以后,我每个test都会加一句 phase.phase_done.set_drain_time(this, 100ns),确保仿真不会无限跑下去。
好了,UVM组件家族到这里就聊完了。从driver到test,每个组件都有自己的定位和职责。你写代码的时候,多想想这个组件该不该干这件事,不该干的千万别往里塞。保持组件的纯净,你的验证环境才能经得起项目的考验。