云计算百科
云计算领域专业知识百科平台

UVM virtual sequencer 与 virtual sequence:从“各自为战”到“统一调度”

1. 当多个 Agent 需要“步调一致”

在模块级验证中,通常只有一个接口 Agent,比如一个 AHB Master Agent 驱动总线,其内部的 sequencer 独自产生激励,完全够用。但来到 SOC 级验证,场景复杂很多:CPU 要通过总线配置 DMA 寄存器、DMA 发起搬移操作、同时 IO 设备持续产生中断和数据流。此时测试用例往往需要在同一时间段内协调 CPU、DMA、IO 等多路激励,让它们按特定时序、顺序或并行关系运行。

每个 Agent 自带的 sequencer 只能管理自己的 sequence,无法知道其他 Agent 在做什么,更谈不上协同。显然,我们需要一个“总指挥”——它能同时看到所有 Agent 的 sequencer,并像乐队的指挥一样,把节奏和时机准确地分给每一个声部。

这就是引入 uvm_virtual_sequencer 和 uvm_virtual_sequence 的动机。

2. 核心概念:调度席、调度员与生产线

先厘清三个角色:

  • 生产线 —— Real Sequencer
    每个 Agent 内部真实的 uvm_sequencer,它负责将 sequence_item 发送给对应的 driver,真正产生接口波形。它只能被动的接收 sequence 来启动。

  • 调度席 —— Virtual Sequencer
    它本身也是一个 uvm_sequencer(通常直接从 uvm_sequencer 扩展),但不生产任何数据,也不向任何 driver 发送 item。它的唯一作用是聚合其他 real sequencer 的句柄,成为一个“通讯录”,让使用者通过它找到所有需要调度的生产线。

    典型的 virtual sequencer 代码如下:

    class my_vseqr extends uvm_sequencer;
    cpu_sequencer m_cpu_sqr;
    dma_sequencer m_dma_sqr;
    io_sequencer m_io_sqr;

    `uvm_component_utils(my_vseqr)

    function new(string name, uvm_component parent);
    super.new(name, parent);
    endfunction
    endclass

    这里不需要也不应该写 uvm_declare_p_sequencer 宏,该宏是给 sequence 用的。

  • 调度员 —— Virtual Sequence
    它是一个普通的 uvm_sequence,但在 body 中通过 p_sequencer 宏获得调度席的句柄,从而可以访问所有 real sequencer,并在其上启动对应的 sequence。调度员制定“剧本”(sequence 的启动顺序、并行/串行关系),指挥每一条生产线何时开工。

有了这三个角色的配合,激励调度就从“各自为战”变成了“统一指挥”。

3. 关键代码:打通任督二脉

下面给出一个完整的标准实现模板。假设环境里已经存在 cpu_sequencer、dma_sequencer 以及对应的 cpu_base_seq、dma_base_seq。

第一步:定义 virtual sequencer(调度席)

class my_vseqr extends uvm_sequencer;
cpu_sequencer m_cpu_sqr;
dma_sequencer m_dma_sqr;

`uvm_component_utils(my_vseqr)

function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass

在实际环境中,我们需要在某个更高的层次(如 env 或 test)将这些句柄指向真实的 sequencer:

my_vseqr vseqr;
// 在 connect_phase 或 build_phase 中进行赋值
vseqr.m_cpu_sqr = env.cpu_agt.sqr;
vseqr.m_dma_sqr = env.dma_agt.sqr;

第二步:定义 virtual sequence(调度员),并通过 uvm_declare_p_sequencer 获得调度席句柄

这是最关键的一步。宏 `uvm_declare_p_sequencer(my_vseqr) 会声明一个 p_sequencer 变量,并将其类型指定为 my_vseqr,从而可以直接访问其中的 m_cpu_sqr、m_dma_sqr。

class my_vseq extends uvm_sequence#(uvm_sequence_item);
`uvm_object_utils(my_vseq)
`uvm_declare_p_sequencer(my_vseqr) // 必须!让 p_sequencer 变为 my_vseqr 类型

function new(string name="my_vseq");
super.new(name);
endfunction

task body();
cpu_base_seq cseq;
dma_base_seq dseq;

// raise objection 以保证仿真不提前结束
if (starting_phase != null)
starting_phase.raise_objection(this);

`uvm_info("VSEQ", "Starting coordinated sequences", UVM_MEDIUM)

fork
// 分别在对应的 real sequencer 上启动 sequence
cseq.start(p_sequencer.m_cpu_sqr);
dseq.start(p_sequencer.m_dma_sqr);
join

// 两条 sequence 都完成后,drop objection
if (starting_phase != null)
starting_phase.drop_objection(this);

`uvm_info("VSEQ", "Coordinated sequences done", UVM_MEDIUM)
endtask
endclass

第三步:在测试用例中将 virtual sequence 挂载到 virtual sequencer 上启动

class my_test extends uvm_test;
my_vseqr vseqr;

task run_phase(uvm_phase phase);
my_vseq vseq = my_vseq::type_id::create("vseq");
phase.raise_objection(this);
vseq.start(vseqr); // 把调度员放到调度席上执行
phase.drop_objection(this);
endtask
endclass

注意这里 start(vseqr) 传入的是 virtual sequencer,虽然 virtual sequencer 不会发送 item,但通过它 p_sequencer 才能被正确设置,整个调度链路才走得通。

4. 实战场景:该用才用,忌滥用

SOC 验证 —— 必备利器

  • 启动流程:CPU 先配置系统寄存器,DMA 搬运启动数据,IO 控制器复位,然后 CPU 等待中断……所有操作必须按时间线编排,一个 vseq 就能描述整个场景。
  • 并行压力测试:CPU 循环读写、DMA 连续传输、以太网收发帧同时进行,各个 real sequencer 跑各自的 sequence,通过 fork/join 在 vseq 里统一管理。
  • 低功耗场景:按照特定顺序关钟、关电源、唤醒,涉及多个模块,vseq 负责编排。

IP 级验证 —— 一般不需要

单个 IP 通常只有一两个标准接口(例如 AXI-Slave + Register IF),用一个普通的 sequence 就能处理所有激励。强行引入 virtual sequencer 只会增加层次复杂度,没有实质收益。记住原则:只在需要跨 Agent 协调时使用 virtual 机制。

5. 易踩坑:避开这些地雷

  • 把 virtual sequencer 当成 real sequencer 用
    在 vseq.start(vseqr) 之后,如果你在 virtual sequencer 内部试图发送 item,会发现根本没有 driver 在接收它。一定要清楚:vseqr 只是张“名片夹”,不参与实际传输。

  • 漏写 `uvm_declare_p_sequencer
    如果没有这个宏,p_sequencer 的类型是 uvm_sequencer_base,无法访问 m_cpu_sqr 等成员,编译或运行时会报出空句柄或类型错误。更糟的是,如果你声明了一个同名的本地 p_sequencer,很可能引起更隐蔽的问题。
    该宏必须写在 virtual sequence 类内部,不是 virtual sequencer 内部。

  • p_sequencer 为 null
    常见原因:

    • 在启动 virtual sequence 时传入的是 null 或者错误的 sequencer。
    • 忘记在环境层将 real sequencer 的句柄赋值给 vseqr 的成员。
    • 在 uvm_declare_p_sequencer 之后,没有通过正常 start() 流程执行 sequence,导致宏背后的自动化查找失败。
      一定要确保启动 vseq 时执行 vseq.start(vseqr)。
  • fork/join 错配导致时序失控
    如果使用 fork … join_none 启动多条 sequence,但不等待它们完成就退出 body,并且 objection 管理混乱,可能导致仿真过早结束,或者 sequence 之间出现竞争。推荐做法:

    • 需要所有 sequence 都完成的场景用 fork…join。
    • 需要部分 sequence 永远在后台运行的,用 fork…join_any + 明确的终止条件。
    • 使用 uvm_sequence_base::wait_for_sequence_state 或自定义同步事件来管理完成状态。
  • 多个 virtual sequence 并行时 objection 管理混乱
    当有多个 vseq 同时在各个子 sequencer 上跑时,如果每个 vseq 内部都 raise/drop objection,且没有统一的退出策略,很容易出现某个 vseq drop objection 导致整个仿真提前结束,而其他 vseq 还没跑完。建议:

    • 在最高层测试用例中统一 raise objection,在所有 vseq 都完成后才 drop。
    • 或者使用 phase 机制,令 vseq 在 main_phase 等 task phase 中自然结束,由 phase 的 objection 自动管理。
  • 6. 经验总结:角色分明,协同有序

    再强调一下三个核心角色的本质,作为“口诀”记忆:

    • Virtual Sequencer —— 调度席
      只负责持有 real sequencer 的句柄,不生产 item,不运行 sequence。

    • Virtual Sequence —— 调度员
      在 body 里通过 p_sequencer 拿到调度席上所有生产线的引用,编排它们在时间上的启动、等待、并行与串行关系。

    • Real Sequencer —— 生产线
      真实干活,将 item 交给 driver 产生波形。

    掌握这组概念并牢记它们的分工,可以让复杂的多接口激励场景变得干净、可维护。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » UVM virtual sequencer 与 virtual sequence:从“各自为战”到“统一调度”
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!