4、UVM序列与序列器:uvm_sequence、uvm_sequencer、sequence的启动与仲裁

各位做验证的朋友,今天我们来聊聊UVM里最核心的“生产-调度”机制——序列与序列器。说白了,sequence就是你的测试用例脚本,sequencer就是那个负责派发任务的调度员。我刚开始学UVM时,总觉得这两兄弟的关系有点绕,后来亲手搭了几个项目,才真正摸透它们的脾气。

4.1 什么是uvm_sequence?

uvm_sequence,你可以把它理解成一个“事务生成器”。它不负责驱动信号,只负责造数据。我习惯把sequence比作“剧本”,而driver就是“演员”——剧本写好台词,演员照着演。

一个典型的sequence长这样:

class my_sequence extends uvm_sequence #(my_transaction);
    `uvm_object_utils(my_sequence)

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

    virtual task body();
        my_transaction tr;
        repeat(10) begin
            tr = my_transaction::type_id::create("tr");
            // 随机化事务
            assert(tr.randomize());
            // 发送给sequencer
            start_item(tr);
            finish_item(tr);
        end
    endtask
endclass

注意看,body()是sequence的主入口。你所有的激励生成逻辑都写在这里。我个人习惯在body()里先做一次“预热”——比如先发几个固定pattern,再进入随机化循环。这样做的好处是,仿真一开始就能看到明确的波形,方便定位问题。

核心要点:sequence不直接和DUT打交道,它只负责生成transaction,然后通过sequencer转发给driver。

4.2 uvm_sequencer:调度中枢

sequencer的角色更像一个“路由器”。它接收来自多个sequence的请求,按照仲裁规则决定谁先拿到总线使用权。

定义sequencer很简单:

class my_sequencer extends uvm_sequencer #(my_transaction);
    `uvm_component_utils(my_sequencer)

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

嗯,这里要注意:sequencer是component,而sequence是object。这个区别很重要——component有生命周期,object没有。所以sequencer需要被例化在agent里,而sequence只需要在test里创建并启动。

避坑指南:我曾经犯过一个低级错误——在sequence里直接调用get_full_name()想获取路径,结果发现返回的是空字符串。原因就是sequence不是component,没有层次路径。如果你需要路径信息,记得通过sequencer来获取。

4.3 Sequence的启动方式

启动sequence有几种常见姿势,我按使用频率排个序:

  1. 手动启动(最常用):在test的main_phase里直接调用seq.start(sequencer)
  2. 宏启动:使用`uvm_do系列宏,适合快速写简单测试
  3. 默认sequence:通过config_db设置,让sequencer在启动时自动运行

手动启动的代码示例:

class my_test extends uvm_test;
    my_sequencer sqr;
    my_sequence seq;

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        sqr = my_sequencer::type_id::create("sqr", this);
    endfunction

    virtual task main_phase(uvm_phase phase);
        phase.raise_objection(this);
        seq = my_sequence::type_id::create("seq");
        seq.start(sqr);  // 启动sequence
        phase.drop_objection(this);
    endtask
endclass

你想想看,手动启动的好处是什么?灵活。你可以在启动前修改sequence的参数,甚至动态创建多个sequence实例。我在做DMA验证时,就同时启动了三个sequence——一个发配置,一个发数据,一个发中断,全靠手动控制。

个人经验:我建议在项目初期用宏启动快速验证通路,后期全部改成手动启动。宏虽然方便,但调试时很难定位问题——你根本不知道哪个sequence在什么时候发了什么包。

4.4 Sequence的仲裁机制

当多个sequence同时向同一个sequencer发送请求时,仲裁就上场了。UVM提供了几种内置仲裁策略:

仲裁模式 说明 适用场景
SEQ_ARB_FIFO 先到先服务(默认) 大多数情况
SEQ_ARB_WEIGHTED 按优先级加权 需要控制流量比例
SEQ_ARB_RANDOM 随机选择 随机压力测试
SEQ_ARB_STRICT_FIFO 严格按请求顺序 协议要求严格顺序
SEQ_ARB_STRICT_RANDOM 严格随机 完全随机化场景

设置仲裁模式的方法:

// 在sequencer中设置
sqr.set_arbitration(SEQ_ARB_WEIGHTED);

// 或者在sequence中动态设置优先级
seq.set_priority(100);  // 数值越大优先级越高

注意:我曾经遇到过一个bug——两个sequence同时发请求,其中一个总是被饿死。查了半天才发现,默认的FIFO仲裁模式下,如果高优先级sequence持续发请求,低优先级的永远拿不到总线。解决方案是改用SEQ_ARB_STRICT_FIFO,确保每个sequence都有机会执行。

4.5 Sequence的嵌套与层次化

实际项目中,我们很少只用一层sequence。更常见的做法是用hierarchy sequence来组合多个子sequence。比如:

class top_sequence extends uvm_sequence #(my_transaction);
    `uvm_object_utils(top_sequence)

    virtual task body();
        reset_sequence reset_seq;
        config_sequence cfg_seq;
        data_sequence data_seq;

        // 先复位
        `uvm_do(reset_seq)
        // 再配置
        `uvm_do(cfg_seq)
        // 最后发数据
        `uvm_do(data_seq)
    endtask
endclass

这样做的好处是复用性极强。你只需要维护底层的原子sequence,上层组合sequence就像搭积木一样。我在做PCIe验证时,一个top_sequence里嵌套了十几个子sequence,覆盖了所有配置组合,写起来很爽。

小技巧:嵌套sequence时,记得在子sequence的body()里调用get_sequencer()来获取父sequencer的句柄。否则子sequence可能找不到正确的sequencer来发送事务。

4.6 实战中的避坑指南

最后,我总结几个实际项目中容易踩的坑:

  • objection管理:sequence启动后一定要记得raise_objection,否则仿真会提前结束。我习惯在test的main_phase里统一管理,而不是在sequence里。
  • sequence的结束条件:不要用#100ns这种硬延时来结束sequence。应该用wait_for_grant()或者检查DUT状态来动态判断。
  • 多个sequencer的同步:如果你的验证环境有多个sequencer(比如AXI的读通道和写通道),记得用uvm_eventuvm_barrier来同步它们。

嗯,关于sequence和sequencer的核心内容就这些。说白了,sequence负责“造”,sequencer负责“派”,两者配合好了,你的验证环境就成功了一半。下一章我们聊聊driver和monitor,看看数据是怎么真正送到DUT手里的。