IP验证学习之agent编写

发布于:2025-09-13 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

前言

interface:

sequence item :

driver:

monitor:

sequence:

注释:

sequencer:

agent:

agent_config : 

agent_packet:

总结


前言

这篇文章主要讲述agent中各个组件的模板的通用写法,具体的环境还是要依靠DUT的环境。读者可以细致阅读代码注释以体会其中的设计思想。

以编写一个agent为例,简单的描述一下agent的内容,其基本概念已经在 UVM入门基础 文章里做了简单介绍,这里不在赘述了。

若其中含有错误还恳请读者指正。

参考:绿皮书,验证公众号


一个成熟的agent里面,势必包含了很多可重用性组件,不过这些组件通常也是照搬其他环境的组件然后自己做改动的。

下面做一些简单的组件编写介绍。

interface:

接口的编写很简单,通常是直接看DUT的接口,然后进行分类后就可以写出来,因为很简单,可以先写接口。

interface op_in_if#(
    DATA_WIDTH = 4                //参数申明
) (input clk, input rst_n);

    logic    start;
    logic    ed;

    logic[DATA_WIDTH-1 : 0]    data1;
    logic[DATA_WIDTH-1 : 0]    data2;
    logic                      data_en;   

parameter     setup_time    = 0.1ns;
parameter     hold_time     = 0.1ns;


clocking drv_cb  @(posedge clk);            //为了能从波形上看出延时
    default input #setup_time    output #hold_time;
    output    start;
    output    ed;                    //接口方向是从driver视角看的
    output    data1;
    output    data2;
    output    data_en;
    input     rst_n;

endclocking: drv_cb

clocking mon_cb  @(posedge clk);
    default input #setup_time    output #hold_time;
    input    start;
    input    ed;                    //接口方向是从monitor视角看的
    input    data1;
    input    data2;
    input    data_en;
    input    rst_n;

endclocking: mon_cb

modoport drv(clocking drv_cb);
modoport mon(clocking mon_cb);

endinterface: op_in_if


sequence item :

在编写该组件时,通常会排除掉interface里的控制信号以及使能信号,只是单纯的提取数据和配置接口。这样做的原因是DUT的控制信号以及使能信号在整个环境中流通其实没有太大意义,这些控制信号可以在tb里直接配置,或者是由其他VIP控制的。

如果当前的transacion还需要用于对比,那就需要注意将其注册变量,以及添加一些相关约束,(如果不需要对比则可以将变量加入 UVM_NOCOMPARE 内)。

class op_in_seq_item#(
    DATA_WIDTH = 4
) extends uvm_sequence_item;

//-------------Data Member--------------

    rand bit[DATA_WIDTH-1:0]    data1;
    rand bit[DATA_WIDTH-1:0]    data2;            //monitor采样,采样到数据后存放到队列中

    rand bit[DATA_WIDTH-1:0]    data1_q[$];
    rand bit[DATA_WIDTH-1:0]    data2_q[$];        //用于临时存储数据

    rand bit    data_num;                        //用于计数数据有多少个,通常用来控制数据要发多少


    `uvm_object_param_utils_begin(op_in_seq_item`PARAM_LIST)
        `uvm_field_int(data1 , UVM_ALL_ON)
        `uvm_field_int(data2 , UVM_ALL_ON)
        `uvm_field_queue_int(data1_q , UVM_ALL_ON)
        `uvm_field_queue_int(data1_q , UVM_ALL_ON)
        `uvm_field_int(data_num , UVM_ALL_ON)
    `uvm_object_utils_end                        //存在生命周期的用object注册

//-------------------------------------------------

            //Methods

//-------------------------------------------------

    extern function new(string name = "op_in_seq_item");

endclass:op_in_seq_item

function op_in_seq_item::new(string name = "op_in_seq_item");    //在类的外面申明函数以供类调用
    super.new(name);
    `uvm_info("TRACE",$sformatf(%m), UVM_HIGH)

endfunction:new

driver:

编写driver通常是根据已有的时序图编写,这个时序图来自设计文档,也可以是自己对DUT的理解,通常在main_phase里执行发送流程,而具体的数据内容则是自己定义函数编写实现。

`define DRV_VIF    vif.drv_cb        //为了方面书写使用宏定义来替代接口的始终块
typedef class op_in_driver;

class op_in_driver_callback#(
    DATA_WIDTH = 4
)extends uvm_callback;            //为了在driver中不修改原值的情况下进行额外的逻辑操作,通常使用callback机制

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

    virtual task pre_send(op_in_driver`PARAM_LIST    drv, op_in_seq_item`PARAM_LIST    tr);        
    endtask    //定义具体的回调方法
    
    virtual task post_send(op_in_driver`PARAM_LIST    drv, op_in_seq_item`PARAM_LIST    tr);        
    endtask    //同样是在回调基类中定义了具体的回调方法

endclass    //定义了回调基类,也就是定义了回调函数的入口,在里面可以找到具体的回调方法

class op_in_driver_example_callback#(
    DATA_WIDTH = 4
)extends op_in_driver_callback`PARAM_LIST;    //继承上一个callback类,这只是个具体的使用示例

      virtual task pre_send(op_in_driver`PARAM_LIST    drv, op_in_seq_item`PARAM_LIST    tr);    
        `uvm_info("OP_IN_DRIVER_CALLBACK", "callback test", UVM_MEDIUM)

    endtask
endclass    //这里演示的只是一个追踪环境功能,读者可以自行修改逻辑。

class op_in_driver#(
    DATA_WIDTH = 4
) extends uvm_driver #(op_in_seq_item`PARAM_LIST);


    `uvm_component_param_utils(op_in_driver`PARAM_LIST)
    `uvm_register_cb(op_in_driver,    op_in_driver_callback)

    virtual op_in_if `PARAM_LIST    vif ;

//------------------------------------------------------------
    
                    //Methods

//------------------------------------------------------------

    extern function new(string name,uvm_component parent);
    extern function void build_phase(uvm_phase phase);
    extern virtual function void start_of_simulation_phase(uvm_phase phase);
    extern virtual task main_phase(uvm_phase phase);
    extern virtual task pre_reset_phase(uvm_phase phase);
    extern virtual task reset_phase(uvm_phase phase);


task shutdown_phase(uvm_phase phase);    //为了将最后一笔数据发出去,这里就是构造一个脉冲发出去
    super.shutdown_phase(phase);
    phase.raise_objection(this);
    @(`DRV_VIF)
    `DRV_VIF.ed <= 1;
    @(`DRV_VIF)
    `DRV_VIF.ed <= 0;
    phase.drop_objection(this);
endtask

 //用户自定义的任务
    extern virtual task send(op_in_seq_item`PARAM_LIST    tr);

endclass: op_in_driver


function op_in_driver::new(string name, uvm_component parent)
    super.new(name , parent);
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)
endfunction : new

function void op_in_driver::build_phase(uvm_phase phase);    //也可以在build_phase里面实例化
    super.build_phase(phase)
     `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)
endfunction: build_phase

function void op_in_driver::start_of_simulation_phase(uvm_phase phase);
    super.start_of_simulation_phase(phase);
     `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)
endfunction: start_of_simulation_phase

 //在main_phase里正常的发驱动即可,上面的phase主要是用来追踪环境的,可以写也可以不写
task op_in_driver::main_phase(uvm_phase phase);
    op_in_seq_item`PARAM_LIST    req ;
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)

    forever begin
        seq_item_port.get_next_item(req) ;
        `uvm_info("OP_IN_DRIVER_TRANS", req.sprint(), UVM_MEDIUM )

        `uvm_do_callbacks(op_in_driver`PARAM_LIST, op_in_driver_callback`PARAM_LIST, pre_send(this,req));    //调用回调方法,也就是调用之前定义的回调函数pre_send,将回调函数里的值赋值给当前的对象req。
        send(req);
        repeat(20) @(`DRV_VIF);
        
        `uvm_do_callbacks(op_in_driver`PARAM_LIST, op_in_driver_callback`PARAM_LIST, pre_send(this,req));

        seq_item_port.item_done();
    end

endtask : main_phase


 //模拟reset之前的信号输入X态
task op_in_driver::pre_reset_phase(uvm_phase phase);
    super.pre_reset_phase(phase)
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)
    phase.raise_objection(this);

        `DRV_IF.start   <= 'bx;
        `DRV_IF.data_en <= 'bx;
        `DRV_IF.data1   <= 'bx;
        `DRV_IF.data2   <= 'bx;
        `DRV_IF.ed      <= 'bx;

    phase.drop_objection(this);
endtask: pre_reset_phase

 //等待复位后X态变成复位值
task op_in_driver::reset_phase(uvm_phase phase);
    super.reset_phase(phase);
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)
    
    phase.raise_objection(this);

        wait(vif.rst_n = 1) ;            //等待事件触发
        
        `DRV_IF.start   <=   'b0;
        `DRV_IF.data_en <=   'b0;
        `DRV_IF.data1   <=   'b0;
        `DRV_IF.data2   <=   'b0;
        `DRV_IF.ed      <=   'b0;
        
        wait(vif.rst_n = 0) ;
        while(!vif.rst_n) begin
            @(`DRV_VIF);
        end
    
    phase.drop_objection(this) ;
endtask : reset_phase


 //根据设计文档中的时序图逻辑编写即可。
task op_in_driver:: send(op_in_seq_item`PARAM_LIST    tr);
    @(`DRV_VIF);
    `DRV_VIF.start    <=  1;
    @(`DRV_VIF);
    `DRV_VIF.start    <=  0;

    //repeat(2) @(`DRV_VIF)
    for(int i=0 ; i< tr.data_num; i++)    begin
        `DRV_VIF.data1    <=  tr.data1_q[i] ;
        `DRV_VIF.data2    <=  tr.data2_q[i] ;
         @(`DRV_VIF);
    end
    `DRV_VIF.data_en    <=  'b0;
    `DRV_VIF.ed         <=   1;
    @(`DRV_VIF);
    `DRV_VIF.ed         <=   0;

endtask :send

`undef    DRV_VIF
    

上述的driver描写的各个phase算得上详细了,实际上的driver编写通常也懒得追加环境,读者阅读代码时注意后面的注释语句就好。

monitor:

编写时要对应着driver的驱动时序,同样是根据已有的时序图来编写,不过相对而言会更复杂一点,因为它需要使用forever语句一直保持采样,原因在于monitor不知道何时采样才能停止。

`define MON_VIF    vif.mon_cb ;    //同driver一样,定义宏代替时钟块

class op_in_monitor#(
    DATA_WIDTH = 4
)extends uvm_monitor;

    `uvm_component_param_utils(op_in_monitor`PARAM_LIST)    //组件注册

    virtual op_in_if`PARAM_LIST    vif;                    //声明接口句柄

    uvm_analysis_port #(op_in_seq_item`PARAM_LIST)     analysis_port;    //端口声明

//-----------------------------------------------------------

                //Methods

//-----------------------------------------------------------


//standard UVM methods
extern function new(string name, uvm_component parent);
extern function build_phase(uvm_phase phase);
extern virtual task run_phase(uvm_phase phase);

//用户自定义函数

endclass: op_in_monitor


function op_in_monitor::new(string name, uvm_component parent)
    super.new(name, parent) ;
    `uvm_info("TRACE", $sformatf("%m") , UVM_HIGH)
endfunction: new

function void op_in_monitor::build_phase(uvm_phase phase)
    super.build_phase(phase) ;
     `uvm_info("TRACE", $sformatf("%m") , UVM_HIGH)

    analysis_port = new("analysis_port", this );
endfunction:build_phase

//为了向读者展示另一种常见的写法,这里就不像driver一样细分很多phase,而是统一使用run_phasse写

task op_in_monitor::run_phase(uvm_phase phase);
    op_in_seq_item`PARAM_LIST    tr_clone ;        //采样后赋值
    op_in_seq_item`PARAM_LIST    tr ;
    
    bit    mon_flag = 0  ;    //采样标志
     `uvm_info("TRACE", $sformatf("%m") , UVM_HIGH)
    
    tr = op_in_seq_item`PARAM_LIST::type_id::create("tr", this);
    fork
        forever begin
            while(`MON_VIF.rst_n !== 1) begin
                @(`MON_VIF);
            end
        @(`MON_VIF);
            if(`MON_VIF.start) begin
                tr.begin_tr();    //记录事务的开始时间 ,begin_tr() 和 end_tr() 是sequence类自带的方法
                mon_flag = 1;
            end
            else if(`MON_VIF.ed) begin
                tr.end_tr();    //记录事务的结束时间
                mon_flag = 0;

                if(tr.data1_q.size()!=0) begin 
                    `uvm_info("MONITOR_TRANS" ,{"\n", tr.sprint()} , UVM_MEDIUM)
                    $cast(tr_clone,tr.clone);     //采样赋值后类型转换
                    tr_clone.begin_tr(tr.get_begin_time);
                    tr_clone.end_tr(tr.get_end_time);        //获取事务的持续时间,用于分析时序
        
                    analysis_port.write(tr_clone);
                    tr = op_in_seq_item`PARAM_LIST::type_id::create("tr", this);   //发出去后更新数值
                end
            end
        end

        forever begin
            if(mon_flag && ~`MON_VIF.start && `MON_VIF.data_en) begin
                tr.data1 = `MON_VIF.data1 ;                  //采样到数据
                tr.data2 = `MON_VIF.data2;
                tr.data1_q.push_back(`MON_VIF.data1);        //压入队列
                tr.data2_q.push_back(`MON_VIF.data2);
        
                @(`MON_VIF);
            end
            else begin
                @(`MON_VIF);
            end
        end
    join
endtask :  run_phase

`undef MON_VIF
    

sequence:

sequence可以当作是sequence_item(transaction) 的延伸,transaction 是只包含事务性信息的包,而sequence是对包的具体事务做划分,规定了其变量要做什么动作,以及怎样划分等。

在编写sequence通常是对应着要操作的那个transaction,对transaction内的信号进行一些列的操作,在将数值赋值给transaction。

不过有些代码设计时也会先编写一个base_seq出来,所有其他的seq都从这个base_seq扩展出来,共用一套基层代码,类似于base_teset一样。

class op_in_seq_base#(
    DATA_WIDTH = 4
) extends uvm_sequence #(op_in_seq_item`PARAM_LIST);

    `uvm_object_param_utils_begin(op_in_seq_base`PARAM_LIST)
        // ......
    `uvm_object_utils_end


//constrains
    constrain vaild{
        //data inside {[8'h0: 8'hf0]};
};

// ----------------------------------------------------------

        // Methods

//-----------------------------------------------------------


//standard UVM Methods
extern function new(string name = "op_in_seq_base");
extern function void pre_randomize();
extern task pre_body();        //uvm_sequence自带的任务 , 用于控制sequence的行为
extern task post_body();       //uvm_sequence自带的任务 ,用于控制sequence的行为

endclass: op_in_seq_base

function op_in_seq_base::new(string name = "op_in_seq_base");
    super.new(name);
    `uvm_info("TRACE", $sformatf("%m") , UVM_HIGH ) 
endfunction

function void op_in_seq_base::pre_randomize();
    
    //......空载

endfunction

task op_in_seq_base::pre_body();
    `ifdef    UVM_VERSION_1_1
        if(get_parent_sequence() == null && starting_phase != null) begin    //如果是顶层序列并且是在某个phase里运行
            uvm_objection objection = starting_phase.get_objection();    //获取当前序列启动phase的objection
            objection.set_drain_time(this,25ns);    //设置objection的排水时间为25ns
            starting_phase.raise_objection(this);
        end
    `endif
endtask

task op_in_seq_base::post_body();
    `ifdef UVM_VERSION_1_1
        if(get_parent_sequence() == null && starting_phase != null) begin
            starting_phase.drop_objection(this);
        end
    `endif
endtask
    


class op_in_seq#(
    DATA_WIDTH = 4
) extends op_in_seq_base`PARAM_LIST;            //扩展出子sequence
    
`uvm_objection_param_utils(op_in_seq`PARAM_LIST)

//data member
    //......


//constrain
    //......

//---------------------------------------------

             //Methods
//----------------------------------------------


//standard UVM Methods
extern function new(string name = " op_in_seq");
extern virtual task body();

endclass: op_in_seq

function op_in_seq::new(string name = "op_in_seq");
    super.new(name)
    `uvm_info("TRACE", $sformatf("%m") , UVM_HIGH)
endfunction : new

task op_in_seq::body();
    op_in_seq_item`PARAM_LIST    req;
    `uvm_info("TRACE", $sformatf("%m") , UVM_HIGH)
    `uvm_do(req)
    //如果需要细分sequence,可以写成uvm_do_with(req,{data_num inside {[2:30]}; })
endtask

注释:

if (get_parent_sequence() == null && starting_phase != null) 这段代码是检查当前当前序列是否有父序列,如果为null则表明是顶层序列启动,也就是直接由sequencer启动,否则就表明是由其他序列调用,后面的是检查当前序列是否关联了UVM的phase,如果非null,则表明 它是在某个phase 里启动的。

因此这行代码的解释为:检查该序列是否为顶层启动且关联了phase。

task my_sequence::body();
    // 如果是顶层序列且在某个phase中运行
    if (get_parent_sequence() == null && starting_phase != null) begin
        `uvm_info("SEQ_START", "This is a top-level sequence running in a phase", UVM_LOW)
        // 执行仅顶层序列需要的操作(如配置DUT、启动监控等)
    end

    // 正常序列逻辑
    forever begin
        my_transaction tr;
        `uvm_create(tr)
        // ... 填充事务并发送 ...
    end
endtask

uvm_objection objection = starting_phase.get_objection();这段代码是获取该序列的对应的objection对象,简单的说,如果序列内置的starting_phase是耗时的,那么就会获取到活动的objection对象,该phase就不会结束。

task my_sequence::body();
    // 获取当前sequence中关联的phase的objection对象
    uvm_objection objection = starting_phase.get_objection();
    
    // 如果有objection对象(即当前序列关联了phase)
    if (objection != null) begin
        // 发起objection,阻止phase结束
        objection.raise_objection(this, "Sequence started");
        
        // 执行序列逻辑(例如发送多个事务)
        repeat (10) begin
            `uvm_do(req)
        end
        
        // 撤销objection,允许phase结束
        objection.drop_objection(this, "Sequence completed");
    end
endtask

sequencer:

sequencer通常不需要我们加任何东西进去,因此它和interface一样很好编写,为了避免后续还有某些需求,也可以写一个框架出来。

class op_in_sequencer#(
    DATA_WIDTH = 4
) extends uvm_sequencer #(op_in_seq_item`PARAM_LIST);

    `uvm_component_param_utils(op_in_sequencer`PARAM_LIST)

//-------------------------------------------------------
    
            //Methods

//-------------------------------------------------------

//Standard UVM Methods
    extern function new(string name = "op_in_sequencer,uvm_component parent = null);

endclass

function op_in_sequencer::new(string name = "op_in_sequencer,uvm_component parent = null);
    super.new(name,parent)
endfunction: new

agent:

agent的编写相信读者已经很熟悉了,其内部也只是单纯的例化一下driver , monitor, sequencer组件,再get一下接口配置连接一下罢了。

每个人的写法都不一定相同,个人的写法是集成和连接外面的内容都放到 agent 的uvm_config_db里。

class op_in_agent #(
    DATA_WIDTH = 4
) extends uvm_agent;

    `uvm_component_param_utils(op_in_agent`PARAM_LIST)

    //virtual op_in_if    vif;     
    //获取接口的方式是从顶层get到,不是实例化得到,因此我用了注释

//Component Members

    op_in_sequencer`PARAM_LIST    seqr;
    op_in_driver`PARAM_LIST       drv;
    op_in_monitor`PARAM_LIST      mon;
    
    uvm_analysis_port#(op_in_seq_item`PARAM_LIST)    analysis_port;

    op_in_agent_config`PARAM_LIST    agt_cfg;        //获取配置

    //-----------------------------------------------------------

                    //Methods

    //-----------------------------------------------------------

//Standard UVM Methods
    extern function new(string name,uvm_component parent);
    extern function void build_phase(uvm_phase phase);
    extern virtual function void connect_phase(uvm_phase phase);
    extern virtual function void start_of_simulation_phase(uvm_phase phase);

endclass: op_in_agent

function op_in_agent::new(string name, uvm_component parent);
    super.new(name, parent);
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)
endfunction: new

function void op_in_agent::build_phase(uvm_phase phase);
    super.build_phase(phase);
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)

    //从上级环境中获取agent_config来使用
    if(!uvm_config_db#(op_in_agent_config`PARAM_LIST)::get(this, "", "op_in_agent_config", agt_cfg))
    begin
        `uvm_error("build_phase",$sformatf("agent config not found"))
    end

   //另一种常用做法,从上一级 的env_config里面获取接口到agent_config当中
   // uvm_config_db#(virtual op_in_if)::get(this, "" ,"op_in_if" , vif);
   // if(vif == null) begin
   //   `uvm_fatal("cfg_err",$sformatf(" interface for agent config not found"))
   // end

   // agt_cfg.vif = vif;
 
    if(agt_cfg.active == UVM_ACTIVE) begin
        seqr = op_in_sequencer`PARAM_LIST::type_id::create("seqr", this);
        drv = op_in_sequencer`PARAM_LIST::type_id::create("drv", this);
        drv.vif = agt_cfg.vif;
    end

    mon = op_in_monitor`PARAM_LIST::type_id::create("mon", this);
    mon.vif = agt_cfg.vif;
    
endfunction: build_phase

function void op_in_agent::connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)

    if(agt_cfg.active == UVM_ACTIVE) begin
        drv.seq_item_port.connect(seqr.seq_item_export);
    end
    this.analysis_port = mon.analysis_port;
endfunction: connect_phase

function void op_in_agent::start_of_simulation_phase(uvm_phase phase);
    super.start_of_simulation_phase(phase);
    `uvm_info("TRACE", $sformatf("%m"), UVM_HIGH)
endfunction: start_of_simulation_phase

    

agent_config : 

里面通常加入一些可配置的变量和接口,用于灵活的配置当前的agent模式和接口。

class op_in_agent_config #(
    DATA_WIDTH = 4
) extends uvm_object;

    `uvm_object_param_utils(op_in_agent_config`PARAM_LIST)

    //Virtual Interface
    virtual op_in_if`PARAM_LIST    vif ;

     //Is the agent active or  passsive
    uvm_active_passive_enum active = UVM_PASSIVE ;     //定义了一个枚举变量active,初始化配置为passive模式。

//-------------------------------------------------------

                //Methods

//--------------------------------------------------------

    //Standard UVM Methods
    extern function new(string name = "op_in_agent_config");

endclass: op_in_agent_config

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

agent_packet:

agent_packet主要是为了集成而做的,用到的地方只需要import即可。即使当前agent有新文件添加或者代码改动也不会影响所有环境的编译,因为所有的改动都存放于agent_packet里了。

//为了参数化使用,定义了参数列表
`define PARAM_LIST #(        //定义了一个宏  PARAM_LIST ,传递DATA_WIDTH
    DATA_WIDTH \            //宏定义以 \ 换行
)

package op_in_agent_pkg ;        //包定义

    import uvm_pkg::* ;            //导入基础库

    `include "op_in_agent_config.svh"
    `include "op_in_seq_item.svh"
    `include "op_in_driver.svh"
    `include "op_in_monitor.svh"
    `include "op_in_sequencer.svh"
    `include "op_in_agent.svh"

    //adapter here
    `include "op_in_seq_lib.svh" 

endpackage

`include "op_in_if.sv"

`undef PARAM_LIST        //取消宏定义,避免污染全局


总结

通用的UVM环境编写有很多种方式,但是思想不变,读者在编写agent时可以主要关注driver ,monitor 和 sequence的编写思想上。