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组合成一个大的场景。到时候见。