1. UVM序列基础回顾:sequence、sequencer、driver的握手关系,sequence的启动与执行机制

1.1 三个核心组件的“三角恋”关系

做UVM验证,说白了就是搞明白三个东西怎么配合:sequence、sequencer、driver。这三者的关系,我习惯叫它“验证界的三角恋”。

sequence是“造数据”的,它知道要发什么激励。driver是“干活”的,它负责把数据打到接口上。sequencer呢?它就是个“中间人”,负责调度和传递。

握手流程其实很简单:

  1. sequence发起请求:sequence通过`uvm_do`或`start_item`告诉sequencer:“我要发数据了!”
  2. sequencer仲裁转发:sequencer拿到请求,看看有没有其他sequence也在抢。如果有,就按优先级仲裁。仲裁通过后,把请求转给driver。
  3. driver申请数据:driver通过`get_next_item`从sequencer那里拿到请求,然后说:“给我数据吧!”
  4. sequence发送数据:sequence把具体的数据内容(transaction)通过`finish_item`发给driver。
  5. 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来构建复杂的测试场景。敬请期待!