1. UVM序列基础回顾:sequence、sequencer、driver的握手关系,sequence的启动与执行机制
1.1 三个核心组件的“三角恋”关系
做UVM验证,说白了就是搞明白三个东西怎么配合:sequence、sequencer、driver。这三者的关系,我习惯叫它“验证界的三角恋”。
sequence是“造数据”的,它知道要发什么激励。driver是“干活”的,它负责把数据打到接口上。sequencer呢?它就是个“中间人”,负责调度和传递。
握手流程其实很简单:
- sequence发起请求:sequence通过`uvm_do`或`start_item`告诉sequencer:“我要发数据了!”
- sequencer仲裁转发:sequencer拿到请求,看看有没有其他sequence也在抢。如果有,就按优先级仲裁。仲裁通过后,把请求转给driver。
- driver申请数据:driver通过`get_next_item`从sequencer那里拿到请求,然后说:“给我数据吧!”
- sequence发送数据:sequence把具体的数据内容(transaction)通过`finish_item`发给driver。
- driver驱动并反馈:driver驱动数据到DUT接口,完成后调用`item_done`告诉sequencer:“活干完了!”
核心要点:这个握手是“阻塞”的。sequence发完数据后,会一直等driver说“done”才会继续发下一个。你想想看,如果driver处理得慢,sequence就得等着。
1.2 sequence的启动方式——两种姿势
sequence怎么跑起来?我见过不少新手在这栽跟头。其实就两种方式:
方式一:手动启动(显式调用)
在test或env里,直接调用sequence的start方法:
class my_test extends uvm_test;
`uvm_component_utils(my_test)
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 创建sequence实例
my_sequence seq = my_sequence::type_id::create("seq");
// 启动sequence,指定sequencer
seq.start(m_env.sequencer);
endfunction
endclass
这种方式最直接。我个人习惯在test的main_phase里启动,因为可以控制启动时机。
方式二:自动启动(default_sequence机制)
在sequencer上配置一个默认sequence,仿真开始后自动运行:
class my_sequencer extends uvm_sequencer #(my_transaction);
`uvm_component_utils(my_sequencer)
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 设置默认sequence
uvm_config_db #(uvm_object_wrapper)::set(
this, "*.seq", "default_sequence",
my_sequence::type_id::get()
);
endfunction
endclass
我的经验:自动启动适合简单的测试场景。但复杂项目里,我建议手动启动。为什么?因为你可以控制sequence的生命周期,比如在某个phase结束后再启动另一个sequence。
1.3 sequence的执行机制——body方法
每个sequence都有一个body方法。这是sequence的“主函数”。启动sequence后,UVM会自动调用body。
body方法里写什么?就是写你要发送的transaction序列:
class my_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(my_sequence)
virtual task body();
my_transaction tr;
// 创建transaction
tr = my_transaction::type_id::create("tr");
// 开始发送
start_item(tr);
// 随机化数据
assert(tr.randomize());
// 完成发送
finish_item(tr);
// 再来一个
`uvm_do(tr)
// 重复100次
repeat(100) begin
`uvm_do_with(tr, {tr.addr inside {[0:255]};})
end
endtask
endclass
这里有几个关键点:
- start_item/finish_item:这是最底层的API,手动控制发送流程
- `uvm_do:宏封装,自动完成创建、随机化、发送三步
- `uvm_do_with:带约束的发送,指定某些字段的取值范围
我曾经踩过的坑:在body里用`uvm_do`时,如果不小心在循环里重复创建transaction,会导致内存泄漏。正确做法是只创建一次,然后反复随机化。或者直接用`uvm_do`宏,它会帮你管理生命周期。
1.4 握手时序的细节——别被仿真骗了
握手看起来简单,但仿真时序上有个坑。我刚开始做验证时,以为sequence发完数据,driver立刻就能拿到。其实不是这样的。
看这个时序图(用文字描述):
| 时间点 | sequence | sequencer | driver |
|---|---|---|---|
| T0 | start_item() | 收到请求 | 等待中 |
| T1 | 等待grant | 仲裁,发grant | get_next_item() |
| T2 | finish_item() | 转发数据 | 拿到数据 |
| T3 | 等待done | 等待done | 驱动数据,item_done() |
| T4 | 继续下一个 | 空闲 | 空闲 |
注意看T0到T1之间,sequence会阻塞在start_item里,直到sequencer说“你可以发了”。这个等待时间取决于sequencer的仲裁策略和当前是否有其他sequence在竞争。
1.5 实战中的小技巧
嗯,这里分享几个我常用的技巧:
- 用grab/ungrab抢断:如果某个sequence需要紧急发送数据(比如中断响应),可以用`grab()`方法直接抢断当前正在执行的sequence。我曾在一次项目中用这个处理复位后的紧急配置。
- 设置优先级:sequence可以设置优先级,sequencer会优先处理高优先级的请求。但注意,优先级只在同一次仲裁中有效,不会抢占正在执行的sequence。
- 用lock/unlock锁定:如果你想让某个sequence独占sequencer,可以用`lock()`。这个我在调试DMA传输时用过,确保连续的数据包不被其他sequence打断。
一句话总结:sequence、sequencer、driver的握手,本质上是“请求-授权-发送-完成”的四步流程。理解了这个,后面学sequence的高级玩法就轻松多了。
下一章,我会讲sequence的嵌套和层次化使用。到时候会展示怎么用subsequence来构建复杂的测试场景。敬请期待!