4. sequence的仲裁机制:sequencer的优先级仲裁、lock与grab操作,sequence的锁定与抢占
好,咱们今天聊点硬核的。sequence怎么抢着发数据?谁先谁后?
说白了,sequencer就是个交通警察。多个sequence同时想发transaction,谁有优先权?谁可以插队?谁又能把路给封了?
我在项目中遇到过好几次,因为仲裁没处理好,导致DUT收到了乱序的激励,仿真结果一塌糊涂。嗯,今天就把这块彻底讲清楚。
4.1 优先级仲裁:谁更重要?
UVM的sequencer默认支持优先级仲裁。每个sequence在启动时,可以指定一个优先级值。
数值越大,优先级越高。这个不难理解吧?
核心规则:
- 优先级范围:-1 到 999
- 默认优先级:-1(最低)
- 数值越大,越优先被选中
看个例子:
class high_priority_seq extends uvm_sequence #(my_transaction);
`uvm_object_utils(high_priority_seq)
task body();
// 启动时指定优先级为100
start_item(req, 100); // 第二个参数就是优先级
finish_item(req);
endtask
endclass
class low_priority_seq extends uvm_sequence #(my_transaction);
`uvm_object_utils(low_priority_seq)
task body();
// 使用默认优先级 -1
start_item(req); // 最低优先级
finish_item(req);
endtask
endclass
我个人习惯,在测试用例里把关键配置sequence设成高优先级。比如复位序列、初始化序列,这些必须优先执行。
小技巧:优先级仲裁只在多个sequence同时请求时才生效。如果只有一个sequence在跑,优先级再低也没人跟它抢。
4.2 仲裁算法:sequencer怎么选?
UVM提供了几种内置的仲裁算法。你可以在sequencer上设置:
| 算法 | 枚举值 | 行为描述 |
|---|---|---|
| FIFO | SEQ_ARB_FIFO | 先来先服务,不考虑优先级 |
| 加权轮询 | SEQ_ARB_WEIGHTED | 按优先级权重分配机会 |
| 严格优先级 | SEQ_ARB_STRICT_FIFO | 高优先级永远优先,同优先级按FIFO |
| 随机 | SEQ_ARB_RANDOM | 随机选一个,不管优先级 |
| 用户自定义 | SEQ_ARB_USER | 你自己实现仲裁逻辑 |
设置方法很简单:
class my_sequencer extends uvm_sequencer #(my_transaction);
function new(string name, uvm_component parent);
super.new(name, parent);
// 设置为严格优先级模式
set_arbitration(SEQ_ARB_STRICT_FIFO);
endfunction
endclass
我曾经踩过一个坑:默认仲裁模式是SEQ_ARB_FIFO,优先级设了等于没设。查了半天才发现,原来仲裁模式没改。你想想看,优先级设了100,结果仲裁器根本不看,那多冤?
4.3 lock操作:把路占住
lock是什么?就是sequence把sequencer给锁住了。锁住之后,其他sequence别想发transaction,直到你解锁。
用法:
class lock_seq extends uvm_sequence #(my_transaction);
`uvm_object_utils(lock_seq)
task body();
// 先获取锁
lock();
// 锁住之后,连续发多个transaction
repeat(10) begin
`uvm_do(req)
end
// 用完了,解锁
unlock();
endtask
endclass
注意:lock()是阻塞调用。如果锁已经被别人拿了,你会一直等,直到锁释放。
警告:lock()和unlock()必须成对出现。我曾经见过有人lock了忘记unlock,结果整个仿真卡死。嗯,debug了一整天。
4.4 grab操作:强行插队
grab比lock更霸道。lock是排队等锁,grab是直接插队到最前面。
什么意思?
- lock:如果锁被占着,你乖乖排队
- grab:不管谁占着锁,我直接抢过来
看代码:
class grab_seq extends uvm_sequence #(my_transaction);
`uvm_object_utils(grab_seq)
task body();
// 直接抢锁,不等
grab();
// 抢到后发一个紧急transaction
`uvm_do(req)
// 释放
ungrab();
endtask
endclass
我个人建议:grab操作要慎用。你想想看,如果两个sequence都用了grab,那就会互相抢来抢去,谁也别想好好干活。
4.5 lock vs grab:到底用哪个?
我整理了一个对比表,方便你理解:
| 特性 | lock | grab |
|---|---|---|
| 等待行为 | 排队等待 | 直接插队 |
| 优先级影响 | 受优先级影响 | 无视优先级 |
| 使用场景 | 需要连续发多个包 | 紧急中断,必须立即执行 |
| 风险 | 可能死锁 | 可能引起饥饿 |
我在项目中遇到过这样一个场景:DUT有个紧急复位信号,必须立刻响应。这时候我就用了grab,让复位sequence直接抢锁,发完复位包再释放。其他sequence等复位完了再继续。
4.6 锁定与抢占的实际案例
来,看一个完整的例子:
class normal_seq extends uvm_sequence #(my_transaction);
`uvm_object_utils(normal_seq)
task body();
`uvm_do(req)
`uvm_do(req)
`uvm_do(req)
endtask
endclass
class urgent_seq extends uvm_sequence #(my_transaction);
`uvm_object_utils(urgent_seq)
task body();
// 紧急事务,直接抢锁
grab();
`uvm_do(req)
ungrab();
endtask
endclass
// 在test中启动
class my_test extends uvm_test;
`uvm_component_utils(my_test)
task run_phase(uvm_phase phase);
normal_seq n_seq;
urgent_seq u_seq;
n_seq = normal_seq::type_id::create("n_seq");
u_seq = urgent_seq::type_id::create("u_seq");
fork
// 同时启动两个sequence
n_seq.start(m_sequencer);
u_seq.start(m_sequencer);
join
endtask
endclass
运行结果会怎样?
- urgent_seq用grab抢到了锁
- 它先发了一个紧急transaction
- 然后释放锁
- normal_seq才开始发它的三个包
避坑指南:我曾经在项目中同时用了多个grab sequence,结果仿真行为完全不可预测。后来我改成只用lock,配合优先级,行为就稳定多了。记住:grab是核武器,能不用就别用。
4.7 总结一下
sequence的仲裁机制,说白了就是解决「谁先发」的问题。
- 优先级仲裁:数值大的优先,但要选对仲裁模式
- lock:排队等锁,适合连续发包
- grab:直接抢锁,适合紧急中断
我个人经验是:能用优先级解决的问题,就别用lock。能用lock解决的问题,就别用grab。越简单的机制,越不容易出bug。
嗯,今天就聊到这儿。下一章我们讲讲sequence的层次化组织,怎么把多个sequence组合成一个大的场景。到时候见。