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启动后,到底怎么跑的?我画个简单的流程给你看:

  1. pre_start():准备工作,比如打印日志、设置参数
  2. body():核心任务,里面写start_item()finish_item()
  3. 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的激励生成你就掌握了八成。剩下的,就是多写多练了。