3. sequence的层次化组织:嵌套sequence、子sequence的调用与同步,sequence的层次化启动方式
好,咱们今天聊点硬核的。sequence的层次化组织,说白了就是怎么把一堆小的sequence拼成一个大的,再让它们协同工作。我在项目里见过不少新人,写sequence就是一个大平层,所有激励逻辑全塞在一个body里,结果代码又臭又长,复用性基本为零。嗯,今天咱们就来治治这个毛病。
3.1 为什么需要层次化sequence?
你想想看,一个复杂的验证场景,比如DMA传输、多通道数据交织,如果只用一个sequence去描述,那body函数得写几百行。调试的时候,你根本分不清是哪个环节出了问题。
我个人习惯是把激励拆成多个小sequence,每个小sequence只干一件事。比如:
- 配置寄存器用一个sequence
- 发送数据包用一个sequence
- 等待中断再用一个sequence
然后把这些小sequence组合起来,形成一个大的测试场景。这就是层次化的核心思想——分而治之。
核心原则:每个sequence只负责一个原子操作,组合逻辑交给上层sequence去编排。
3.2 嵌套sequence:子sequence的调用
UVM里调用子sequence,最常用的就是`uvm_do系列宏,或者手动调用start()方法。咱们先看一个简单的例子。
// 子sequence:发送一个数据包
class seq_send_packet extends uvm_sequence #(packet);
`uvm_object_utils(seq_send_packet)
virtual task body();
req = packet::type_id::create("req");
start_item(req);
// 随机化数据包内容
assert(req.randomize());
finish_item(req);
endtask
endclass
// 父sequence:组合多个子sequence
class seq_complex_scenario extends uvm_sequence #(packet);
`uvm_object_utils(seq_complex_scenario)
virtual task body();
seq_send_packet seq1;
seq_send_packet seq2;
// 方式一:使用`uvm_do宏
`uvm_do(seq1)
`uvm_do(seq2)
// 方式二:手动创建并启动
// seq1 = seq_send_packet::type_id::create("seq1");
// seq1.start(m_sequencer, this);
endtask
endclass
这里要注意,`uvm_do宏会自动帮你创建、随机化、启动子sequence,并且把父sequence的sequencer传递给子sequence。我在项目中遇到过一个问题:如果父sequence和子sequence用的sequencer不一样,那`uvm_do就会报错。这时候就得手动指定sequencer了。
小技巧:如果你需要子sequence在另一个sequencer上运行,用seq.start(other_sequencer),别用`uvm_do。
3.3 子sequence的同步:等待与握手
有时候,子sequence之间需要同步。比如,先发一个配置包,等DUT处理完,再发数据包。这时候就需要用到uvm_event或者uvm_barrier。
我曾经在一个多通道项目中,需要两个子sequence交替发送数据。一个发通道0,一个发通道1,必须严格交替。我当时用了uvm_event来做握手。
class seq_ch0 extends uvm_sequence #(packet);
uvm_event e_done_ch0;
virtual task body();
repeat(10) begin
`uvm_do(req)
e_done_ch0.trigger(); // 通知通道0完成
e_done_ch1.wait_trigger(); // 等待通道1完成
end
endtask
endclass
class seq_ch1 extends uvm_sequence #(packet);
uvm_event e_done_ch1;
virtual task body();
repeat(10) begin
e_done_ch0.wait_trigger(); // 等待通道0完成
`uvm_do(req)
e_done_ch1.trigger(); // 通知通道1完成
end
endtask
endclass
嗯,这里要注意,uvm_event是全局的,你得确保两个子sequence拿到的是同一个event对象。我一般会在父sequence里创建event,然后通过set_config或者构造函数传进去。
避坑指南:我曾经犯过一个错误——在子sequence的body里用new()创建event,结果两个子sequence各用各的event,永远等不到对方。记住,同步对象必须共享。
3.4 sequence的层次化启动方式
启动一个层次化的sequence,有几种常见方式。我按使用频率排个序:
| 启动方式 | 适用场景 | 个人评价 |
|---|---|---|
| 在test中直接启动顶层sequence | 大多数情况,简单直接 | 最常用,推荐 |
| 在virtual sequence中启动多个子sequence | 需要跨sequencer的场景 | 灵活,但要注意同步 |
| 在sequence的body中递归启动 | 树形结构的sequence | 小心死循环 |
举个例子,在test中启动顶层sequence:
class test_complex extends uvm_test;
`uvm_component_utils(test_complex)
virtual function void build_phase(uvm_phase phase);
// 创建sequencer等
endfunction
virtual task run_phase(uvm_phase phase);
seq_complex_scenario seq;
seq = seq_complex_scenario::type_id::create("seq");
phase.raise_objection(this);
seq.start(m_sequencer); // 启动顶层sequence
phase.drop_objection(this);
endtask
endclass
你看,启动顶层sequence后,它内部会自动调用子sequence。你根本不用关心子sequence是怎么组织的——这就是层次化的好处。
3.5 实战经验:一个多通道DMA测试的例子
我记得有一次做DMA验证,有8个通道,每个通道要独立发送数据,但最后要一起完成。我设计了一个三层结构:
- 底层:每个通道一个子sequence,只负责发数据
- 中间层:一个virtual sequence,同时启动8个子sequence
- 顶层:一个控制sequence,负责配置、启动、等待完成
中间层的virtual sequence大概长这样:
class vseq_dma_channels extends uvm_sequence;
`uvm_object_utils(vseq_dma_channels)
virtual task body();
seq_dma_ch ch_seq[8];
for(int i=0; i<8; i++) begin
ch_seq[i] = seq_dma_ch::type_id::create($sformatf("ch_seq_%0d", i));
// 每个子sequence在不同的sequencer上启动
ch_seq[i].start(p_sequencer.ch_sequencer[i]);
end
// 等待所有子sequence完成
// 可以用uvm_barrier或者简单的fork-join
fork
for(int i=0; i<8; i++) begin
ch_seq[i].get_response(rsp);
end
join
endtask
endclass
这里有个关键点:p_sequencer是virtual sequence特有的,它让你能访问到virtual sequencer下的子sequencer。如果你用m_sequencer,那就只能拿到一个sequencer,没法做多通道分发。
总结一下:层次化sequence的核心就三点——拆分、组合、同步。拆分要细,组合要稳,同步要准。做到这三点,再复杂的验证场景你也能驾驭。
好了,这一章就到这里。下一章咱们聊聊sequence的进阶用法——如何用sequence控制DUT的复位行为。到时候我会分享一个我踩过的坑,保证让你印象深刻。