3. UVM基础组件:从uvm_object到uvm_test

各位同学,今天我们来聊聊UVM里最基础、也最绕不开的几个组件。说实话,我刚学UVM那会儿,被uvm_component和uvm_object的区别搞得晕头转向。后来在项目里踩了几次坑,才算真正搞明白。

嗯,咱们就从最底层的uvm_object说起。

3.1 uvm_object:UVM世界的"细胞"

uvm_object是UVM所有数据类的基类。说白了,只要你想在UVM环境里传递数据、做配置、搞事务,都得从它派生。

我个人习惯把uvm_object理解成"轻量级对象"。它没有生命周期概念,不参与phase机制,也不会被自动构建。你什么时候想用,就什么时候new一个出来。

核心特点:

  • 没有phase机制
  • 不参与UVM树形结构
  • 可以随时创建和销毁
  • 支持copy、compare、print等通用方法

举个例子,我们常用的transaction、sequence_item,都是从uvm_sequence_item派生,而uvm_sequence_item又继承自uvm_object。

class my_transaction extends uvm_sequence_item;
    rand bit [31:0] addr;
    rand bit [31:0] data;
    
    `uvm_object_utils_begin(my_transaction)
        `uvm_field_int(addr, UVM_ALL_ON)
        `uvm_field_int(data, UVM_ALL_ON)
    `uvm_object_utils_end
    
    function new(string name = "my_transaction");
        super.new(name);
    endfunction
endclass

这里要注意,uvm_object的构造函数只有一个name参数。我在项目中见过有人试图传parent进去,结果编译报错——嗯,那是uvm_component才有的待遇。

3.2 uvm_component:UVM世界的"器官"

uvm_component就不一样了。它是有生命周期的,参与phase调度,并且会挂载到UVM的树形结构里。

你可以这么理解:uvm_object是数据,uvm_component是处理数据的模块。数据可以随处飘,但模块必须待在树上。

我的经验: 判断一个类该继承哪个,就问自己一个问题——这个东西需要参与phase吗?需要被自动构建吗?如果答案是"是",就用uvm_component;否则用uvm_object。

class my_driver extends uvm_driver #(my_transaction);
    `uvm_component_utils(my_driver)
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        // 在这里创建子组件
    endfunction
endclass

看到没?构造函数有两个参数:name和parent。这个parent就是用来构建树形结构的。我曾经在重构代码时忘了传parent,结果UVM报了个"component not in tree"的警告——虽然能跑,但很多功能会失效。

3.3 uvm_agent:验证组件的"集装箱"

uvm_agent是个很有意思的组件。它本质上是一个容器,用来封装driver、monitor、sequencer这些"干活"的组件。

为什么需要agent?你想想看,一个接口协议可能有多个信号,每个信号都需要driver去驱动、monitor去采样。如果把这些组件散落在env里,管理起来会很混乱。agent就是用来做这件事的——把属于同一个接口的组件打包在一起。

agent的两种模式:

模式 is_active 包含组件
主动模式 UVM_ACTIVE driver + monitor + sequencer
被动模式 UVM_PASSIVE monitor(只观察,不驱动)
class my_agent extends uvm_agent;
    my_driver    drv;
    my_monitor   mon;
    my_sequencer seqr;
    
    `uvm_component_utils(my_agent)
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mon = my_monitor::type_id::create("mon", this);
        if(get_is_active() == UVM_ACTIVE) begin
            drv  = my_driver::type_id::create("drv", this);
            seqr = my_sequencer::type_id::create("seqr", this);
        end
    endfunction
    
    function void connect_phase(uvm_phase phase);
        if(get_is_active() == UVM_ACTIVE) begin
            drv.seq_item_port.connect(seqr.seq_item_export);
        end
    endfunction
endclass

这里有个细节:get_is_active()是uvm_agent自带的方法。我建议在顶层配置里设置agent的模式,而不是在agent内部硬编码。这样同一个agent可以复用在不同的场景里。

3.4 uvm_env:验证平台的"总装车间"

uvm_env是验证平台的顶层容器。它负责实例化所有的agent、reference model、scoreboard等组件,并完成它们之间的连接。

说白了,env就是整个验证环境的"总装车间"。你在env里把各个模块拼起来,连好线,然后test就去调用这个env。

class my_env extends uvm_env;
    my_agent    agt;
    my_model    mdl;
    my_scoreboard scb;
    
    `uvm_component_utils(my_env)
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        agt = my_agent::type_id::create("agt", this);
        mdl = my_model::type_id::create("mdl", this);
        scb = my_scoreboard::type_id::create("scb", this);
    endfunction
    
    function void connect_phase(uvm_phase phase);
        agt.mon.ap.connect(mdl.analysis_export);
        mdl.ap.connect(scb.analysis_export);
    endfunction
endclass

避坑指南: 我曾经在env里忘了调用super.build_phase(),结果所有子组件都没创建出来。UVM的phase机制要求父类必须调用super方法,否则子类的行为会异常。这个坑我踩过一次就再也没忘过。

3.5 uvm_test:验证的"总指挥"

uvm_test是验证测试的入口。它负责创建env,配置参数,启动sequence。每个测试用例都是一个uvm_test的派生类。

我个人习惯把test做得尽量轻量——只做配置和启动,具体的验证逻辑交给sequence和env去处理。这样换测试用例时,只需要改test里的配置参数就行。

class my_test extends uvm_test;
    my_env env;
    
    `uvm_component_utils(my_test)
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = my_env::type_id::create("env", this);
    endfunction
    
    task run_phase(uvm_phase phase);
        my_sequence seq;
        phase.raise_objection(this);
        seq = my_sequence::type_id::create("seq");
        seq.start(env.agt.seqr);
        #100ns;
        phase.drop_objection(this);
    endtask
endclass

注意raise_objection和drop_objection。这是UVM控制仿真结束的机制。如果不raise objection,仿真可能在你还没开始跑sequence就结束了。我记得有次调试一个bug,折腾了半天才发现是objection没处理好。

3.6 组件关系图

说了这么多,咱们用一张图来总结这些组件的关系:

uvm_test uvm_env uvm_agent uvm_driver uvm_monitor uvm_sequencer uvm_component 继承关系树 uvm_object 是数据基类,不参与树形结构

从这张图可以看得很清楚:uvm_test包含uvm_env,uvm_env包含uvm_agent,而uvm_agent又包含driver、monitor、sequencer。这些组件都继承自uvm_component,构成了UVM的树形结构。

至于uvm_object,它不在这个树里。它更像是"数据包",在组件之间传递。比如driver从sequencer拿到transaction(继承自uvm_object),然后驱动到接口上。

总结一下:

  • uvm_object:数据载体,轻量级,无phase
  • uvm_component:功能模块,有phase,在树中
  • uvm_agent:接口级容器,封装driver/monitor/sequencer
  • uvm_env:平台级容器,封装agent/model/scoreboard
  • uvm_test:测试入口,配置env并启动sequence

嗯,这些组件搞清楚了,UVM的骨架也就搭起来了。后面咱们再往里面填充血肉——sequence、config_db、callback这些,就水到渠成了。

可可精品资料库