4、UVM序列与序列器:uvm_sequence、uvm_sequencer、sequence的启动与仲裁
各位做验证的朋友,今天我们来聊聊UVM里最核心的“生产-调度”机制——序列与序列器。说白了,sequence就是你的测试用例脚本,sequencer就是那个负责派发任务的调度员。我刚开始学UVM时,总觉得这两兄弟的关系有点绕,后来亲手搭了几个项目,才真正摸透它们的脾气。
4.1 什么是uvm_sequence?
uvm_sequence,你可以把它理解成一个“事务生成器”。它不负责驱动信号,只负责造数据。我习惯把sequence比作“剧本”,而driver就是“演员”——剧本写好台词,演员照着演。
一个典型的sequence长这样:
class my_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(my_sequence)
function new(string name = "my_sequence");
super.new(name);
endfunction
virtual task body();
my_transaction tr;
repeat(10) begin
tr = my_transaction::type_id::create("tr");
// 随机化事务
assert(tr.randomize());
// 发送给sequencer
start_item(tr);
finish_item(tr);
end
endtask
endclass
注意看,body()是sequence的主入口。你所有的激励生成逻辑都写在这里。我个人习惯在body()里先做一次“预热”——比如先发几个固定pattern,再进入随机化循环。这样做的好处是,仿真一开始就能看到明确的波形,方便定位问题。
核心要点:sequence不直接和DUT打交道,它只负责生成transaction,然后通过sequencer转发给driver。
4.2 uvm_sequencer:调度中枢
sequencer的角色更像一个“路由器”。它接收来自多个sequence的请求,按照仲裁规则决定谁先拿到总线使用权。
定义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
嗯,这里要注意:sequencer是component,而sequence是object。这个区别很重要——component有生命周期,object没有。所以sequencer需要被例化在agent里,而sequence只需要在test里创建并启动。
避坑指南:我曾经犯过一个低级错误——在sequence里直接调用get_full_name()想获取路径,结果发现返回的是空字符串。原因就是sequence不是component,没有层次路径。如果你需要路径信息,记得通过sequencer来获取。
4.3 Sequence的启动方式
启动sequence有几种常见姿势,我按使用频率排个序:
- 手动启动(最常用):在test的main_phase里直接调用
seq.start(sequencer) - 宏启动:使用
`uvm_do系列宏,适合快速写简单测试 - 默认sequence:通过config_db设置,让sequencer在启动时自动运行
手动启动的代码示例:
class my_test extends uvm_test;
my_sequencer sqr;
my_sequence seq;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
sqr = my_sequencer::type_id::create("sqr", this);
endfunction
virtual task main_phase(uvm_phase phase);
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(sqr); // 启动sequence
phase.drop_objection(this);
endtask
endclass
你想想看,手动启动的好处是什么?灵活。你可以在启动前修改sequence的参数,甚至动态创建多个sequence实例。我在做DMA验证时,就同时启动了三个sequence——一个发配置,一个发数据,一个发中断,全靠手动控制。
个人经验:我建议在项目初期用宏启动快速验证通路,后期全部改成手动启动。宏虽然方便,但调试时很难定位问题——你根本不知道哪个sequence在什么时候发了什么包。
4.4 Sequence的仲裁机制
当多个sequence同时向同一个sequencer发送请求时,仲裁就上场了。UVM提供了几种内置仲裁策略:
| 仲裁模式 | 说明 | 适用场景 |
|---|---|---|
| SEQ_ARB_FIFO | 先到先服务(默认) | 大多数情况 |
| SEQ_ARB_WEIGHTED | 按优先级加权 | 需要控制流量比例 |
| SEQ_ARB_RANDOM | 随机选择 | 随机压力测试 |
| SEQ_ARB_STRICT_FIFO | 严格按请求顺序 | 协议要求严格顺序 |
| SEQ_ARB_STRICT_RANDOM | 严格随机 | 完全随机化场景 |
设置仲裁模式的方法:
// 在sequencer中设置
sqr.set_arbitration(SEQ_ARB_WEIGHTED);
// 或者在sequence中动态设置优先级
seq.set_priority(100); // 数值越大优先级越高
注意:我曾经遇到过一个bug——两个sequence同时发请求,其中一个总是被饿死。查了半天才发现,默认的FIFO仲裁模式下,如果高优先级sequence持续发请求,低优先级的永远拿不到总线。解决方案是改用SEQ_ARB_STRICT_FIFO,确保每个sequence都有机会执行。
4.5 Sequence的嵌套与层次化
实际项目中,我们很少只用一层sequence。更常见的做法是用hierarchy sequence来组合多个子sequence。比如:
class top_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(top_sequence)
virtual task body();
reset_sequence reset_seq;
config_sequence cfg_seq;
data_sequence data_seq;
// 先复位
`uvm_do(reset_seq)
// 再配置
`uvm_do(cfg_seq)
// 最后发数据
`uvm_do(data_seq)
endtask
endclass
这样做的好处是复用性极强。你只需要维护底层的原子sequence,上层组合sequence就像搭积木一样。我在做PCIe验证时,一个top_sequence里嵌套了十几个子sequence,覆盖了所有配置组合,写起来很爽。
小技巧:嵌套sequence时,记得在子sequence的body()里调用get_sequencer()来获取父sequencer的句柄。否则子sequence可能找不到正确的sequencer来发送事务。
4.6 实战中的避坑指南
最后,我总结几个实际项目中容易踩的坑:
- objection管理:sequence启动后一定要记得raise_objection,否则仿真会提前结束。我习惯在test的main_phase里统一管理,而不是在sequence里。
- sequence的结束条件:不要用
#100ns这种硬延时来结束sequence。应该用wait_for_grant()或者检查DUT状态来动态判断。 - 多个sequencer的同步:如果你的验证环境有多个sequencer(比如AXI的读通道和写通道),记得用
uvm_event或uvm_barrier来同步它们。
嗯,关于sequence和sequencer的核心内容就这些。说白了,sequence负责“造”,sequencer负责“派”,两者配合好了,你的验证环境就成功了一半。下一章我们聊聊driver和monitor,看看数据是怎么真正送到DUT手里的。