4、UVM序列机制:sequence与sequencer的工作原理
好,咱们今天聊聊UVM里最核心的机制之一——序列机制。
说实话,我刚接触UVM那会儿,最头疼的就是sequence和sequencer的关系。总觉得这俩东西长得像,又分不清谁管谁。后来做项目做多了,才慢慢摸到门道。
4.1 sequence与sequencer:谁在干活?谁在调度?
简单来说,sequence是“做什么”,sequencer是“谁来做”。
你想想看,sequence里定义了一串激励——先发一个读命令,再发一个写命令,中间等几个时钟。而sequencer呢?它就是个调度器,负责把这些激励按顺序发给driver。
我在项目中遇到过一种情况:两个sequence都想同时发激励,结果driver那边乱成一锅粥。后来我才明白,sequencer就是干这个的——它用仲裁机制保证同一时刻只有一个sequence能拿到“发激励的权限”。
核心关系:
- sequence:生成transaction,定义激励内容
- sequencer:接收sequence的请求,仲裁后转发给driver
- driver:从sequencer拿到transaction,驱动到DUT接口上
4.2 sequence的启动与执行
sequence怎么启动?嗯,这里要注意,有两种方式。
4.2.1 手动启动
最直接的方法,就是在test里调用start()方法:
class my_test extends uvm_test;
my_sequence seq;
function void build_phase(uvm_phase phase);
seq = my_sequence::type_id::create("seq", this);
endfunction
task run_phase(uvm_phase phase);
phase.raise_objection(this);
seq.start(m_sequencer); // 手动启动
phase.drop_objection(this);
endtask
endclass
我个人习惯用这种方式,因为控制权在自己手里。什么时候启动、什么时候停止,清清楚楚。
4.2.2 自动启动
另一种方式是用uvm_sequence_library或者配置default_sequence。说白了就是告诉sequencer:“你看着办,到了run_phase自己启动。”
function void my_test::build_phase(uvm_phase phase);
uvm_config_db#(uvm_object_wrapper)::set(
this, "env.agent.sequencer.run_phase",
"default_sequence", my_sequence::get_type()
);
endfunction
这种方式适合那些“每次上电都要跑一遍”的基础序列。但我建议新手先用手动启动,等理解了流程再用自动的。
4.2.3 sequence的执行流程
sequence启动后,到底怎么跑的?我画个简单的流程给你看:
- pre_start():准备工作,比如打印日志、设置参数
- body():核心任务,里面写
start_item()和finish_item() - post_start():收尾工作,比如检查结果
你想想看,body()里写的才是真正的激励内容。我曾经犯过一个错——把初始化代码全塞在body()里,结果每次启动sequence都要重新初始化,浪费了不少仿真时间。后来我把初始化挪到pre_start()里,效率高多了。
4.3 sequence的嵌套与同步
实际项目中,很少只有一个sequence从头跑到尾。更多时候是多个sequence嵌套执行,或者需要同步。
4.3.1 嵌套sequence
什么叫嵌套?就是一个sequence里启动另一个sequence。比如:
class reset_sequence extends uvm_sequence #(my_transaction);
task body();
// 先发复位
`uvm_do(req, {req.cmd == RESET;})
// 再启动配置序列
cfg_sequence cfg_seq = cfg_sequence::type_id::create("cfg_seq");
cfg_seq.start(m_sequencer);
// 最后发正常操作
`uvm_do(req, {req.cmd == NORMAL;})
endtask
endclass
我在项目中遇到过一种场景:需要先复位,再配置寄存器,最后才发数据。用嵌套sequence就特别自然——一个sequence管一件事,组合起来就是完整的测试场景。
小技巧:嵌套sequence时,记得把m_sequencer传进去。不然子sequence找不到sequencer,会报空指针错误。我刚开始就踩过这个坑。
4.3.2 sequence的同步
多个sequence之间怎么同步?说白了就是“你等我,我等你”。
UVM提供了几种同步机制:
| 机制 | 说明 | 适用场景 |
|---|---|---|
| uvm_event | 事件触发,一个sequence等另一个sequence发信号 | 两个sequence有先后依赖关系 |
| uvm_barrier | 屏障同步,所有sequence都到了才能继续 | 多个sequence需要同时开始 |
| uvm_phase | 利用phase的同步点 | 不同phase之间的同步 |
举个例子,我曾经做过一个DMA测试:一个sequence负责配置DMA通道,另一个sequence负责发数据。这两个sequence必须同步——配置没完成,数据不能发。我用uvm_event解决了这个问题:
class cfg_sequence extends uvm_sequence;
uvm_event cfg_done;
task body();
cfg_done = new("cfg_done");
uvm_event_pool::add("cfg_done", cfg_done);
// 配置DMA...
cfg_done.trigger(); // 配置完成,发信号
endtask
endclass
class data_sequence extends uvm_sequence;
task body();
uvm_event cfg_done = uvm_event_pool::get("cfg_done");
cfg_done.wait_trigger(); // 等配置完成
// 开始发数据...
endtask
endclass
注意:用uvm_event_pool时,事件名要全局唯一。我见过有人用了相同的名字,结果两个sequence互相等,死锁了。建议用模块名+功能名作为事件名,比如"dma_cfg_done"。
4.4 避坑指南
最后,分享几个我踩过的坑:
- sequence没启动就发transaction:我曾经在build_phase里直接调
start_item(),结果报错。记住,sequence必须在run_phase里启动。 - 忘记raise_objection:sequence启动后,如果不raise_objection,仿真可能瞬间结束。我刚开始经常犯这个错,后来养成了习惯——启动sequence前先raise。
- 嵌套太深:sequence嵌套超过3层,调试起来就很痛苦。我建议最多嵌套2层,复杂场景用virtual sequence来管理。
嗯,关于sequence和sequencer,今天就聊这么多。说白了,理解了这个机制,UVM的激励生成你就掌握了八成。剩下的,就是多写多练了。