跳至主要內容

6、UVM机制(一)

DarentTheYang...大约 30 分钟数字验证数字验证

这一节需要先讲一些杂散的东西,然后才继续集中研究。首先会介绍一下UVM类、factory机制、field机制、configuration机制、phase机制、UVM内嵌的信息服务机制。以后还会有一个TLM信息传输机制需要讲,这是后话。刚才提到的这些机制是基础,需要花一些时间来学,但并不会占用太久的时间(可能还是有点久的,内容不算少)。==本章章内相关位置及末尾会给出查看示例代码的网站,文章末尾的网站仅为汇总。注意,这些示例代码可能会和内容展示的代码不太一样,因为这些代码是在这章写完后才编写的。代码内容仅供参考,可以随意转载,转载请带上作者博客链接。这些代码存在于EDAPlayground中。如果需要在线运行,需要注册。==

6、UVM机制(一)

6.1 UVM类

记住这个结构就行,如果需要知道详情,可以去看张强《UVM实战》这本书,真的强烈推荐。

UVM类
UVM类

6.2 factory机制

什么是factory机制

UVM factory机制可以使用户在不更改代码的情况下实现不同对象的替换。

factory是UVM中一种数据结构。它的作用范围是整个平台空间,它有且仅有一个实例化对象(即单实例类)。它是一个多态构造器,可以仅仅使用一个函数让用户实例化很多不同类型的对象。

为了使用一个函数而可以返回多种对象,这些对象必须从一个基类扩展而来

例如,从my_transaction扩展出my_transaction_ext,创造不同的输入数据、输入数据范围等,从而达到不同的验证效果。

factory机制中的两个查找表

factory机制的核心是两张查找表,一张叫注册表,一张叫替换表

首先是注册表。在诸如driver、env这些component中, 都有这样一段代码:

`uvm_component_utils(class_name)

而transaction(sequence也是,只是它同时用了field机制才需要begin、end)这样的object中,有这样一段代码:

`uvm_object_utils(class_name)

这两个都是用于向factory注册的宏。当component或者object调用注册宏后,就向注册表中加入了这些类。在实例化对象时,使用类似于这样的语句:

// 在build_phase中实例化
m_env = my_env::type_id::create("m_env", this);

才能使factory机制生效,而不是直接使用构造函数new。另外,注册还有一个好处,就是可以在函数中调用一些已经定义好的静态函数(例如get_type),而不用自己定义。

其次是替换表

在实例化对象时,UVM会扫描替换表中的类,查看正在实例化的类是否已经被其他类替换。如果没有,就使用当前类例化。如果查找到被替换了,就用替换的类实例化。

注册表注册需要用以上两个宏,向替换表中写入内容也需要由相应的宏来完成,这就是override宏。

override机制

factory和override机制演示代码open in new window

使用factory机制能提升验证平台的复用性,其中一个原因在于factory机制中的override机制。

override能够将通过重写或者继承原来的类创造的新类覆盖掉原来的类,从而实现验证平台的复用。

override语句主要有两个:

// 这一句是整体替换,会把整个验证平台中的目标类被替换成新的类。
set_type_override_by_type(original_class_name::get_type(),
                          target_class_name::get_type());
// 这一句是部分替换,只会把其中一个对象替换成新类的对象。
set_inst_override_by_type("original_inst_path",
                          original_class_name::get_type(),
                          target_class_name::get_type());

这两个函数都只存在于component中,也就是说仅有component才能调用这两个函数去替换类,而且一般需要在build_phase中调用它们。

override用法

假设我现在要替换的是transaction A,要将它替换成transaction B,替换前的transaction情况:

替换前的情况
替换前的情况

首先看set_type_override_by_type,看一下这句语句的示意图:

整个验证平台的替换
整个验证平台的替换

可以看到,整体被替换成了transaction B,实现方法如下:

// 1. 将transaction A扩展为transaction B
class transaction_B extends transaction_A;
    // 需要重新注册
    `uvm_object_utils(transaction_B)
    
    // 重写构造函数
    function new(string name = "");
        super.new(name);
    endfunction
    
    // 2. 如果是要修改限制条件,需要重写修改的部分,不需要写未修改的部分。
    // 注意,新的限制条件一定要在旧的限制条件之内,就是子集,不能比原来的限制条件更宽。
    constraint Limit_new{
        xxxx;
    }
endclass

// 3. 在组件中调用set_type_override_by_type替换,
// 例如可以用原来的testcase扩展为新的testcase,
// 这里示意性地把testcase_A扩展为testcase_B,直接从uvm_test扩展也是可以的
class testcase_B extends testcase_A;
    // 需要重新注册
    `uvm_component(testcase_B)
    
    // 重写构造函数
    function new(string name = "", uvm_component parent);
        super.new(name, parent);
    endfunction
    
    // 4. 在build_phase中,写入替换信息
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        set_type_override_by_type(transaction_A::get_type(),
                                  transaction_B::get_type());
    endfunction
    
    // 可以通过一些打印信息来确定是否确实替换成功了
    virtual function void report_phase(uvm_phase phase);
        super.report_phase(phase);
        factory.print();
    endfunction
endclass


// 最后,在top中启动run_test时,填入testcase_B,就能正确启动了。

然后再看看set_inst_override_by_type。这是替换后的示意图:

部分替换示意图
部分替换示意图

下面是实现方法,可以看出和set_type_override_by_type非常类似:

// 1. 同样地将transaction A扩展为transaction B
class transaction_B extends transaction_A;
    // 需要重新注册
    `uvm_object_utils(transaction_B)
    
    // 重写构造函数
    function new(string name="");
        super.new(name);
    endfunction
    
    // 2. 同样使用限制条件作为不同点
    constraint Limit_new{
        xxxx;
    }
endclass

// 3. 同样,在组件中调用set_inst_override_by_type替换,
// 例如可以用原来的testcase扩展为新的testcase,
// 这里示意性地把testcase_A扩展为testcase_C,
// 直接从uvm_test扩展也是可以的。这边只替换sequencer中的transaction。
class testcase_C extends testcase_A;
    // 需要重新注册
    `uvm_component(testcase_C)
    
    // 重写构造函数
    function new(string name = "", uvm_component parent);
        super.new(name, parent);
    endfunction
    
    // 4. 在build_phase中,写入替换信息,需要注意的是这个路径,起点是自身,所以不用写,
    // 终点是被替换对象所在的component或者它的父对象,比如这里的索引就到m_seqr了。
    // 如果路径只写到m_env,就会把m_env中所有的transaction_A对象都替换掉,这不一定符合预期。
    // 所以要搞清楚替换对象到底在哪里。
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        set_inst_override_by_type("m_env.i_agent.m_seqr.*",
                                  transaction_A::get_type(),
                                  transaction_B::get_type());
    endfunction
    
    // 可以通过一些打印信息来确定是否确实替换成功了
    virtual function void report_phase(uvm_phase phase);
        super.report_phase(phase);
        factory.print();
    endfunction
endclass

// 最后,在top中启动run_test时,填入testcase_C,就能正确启动了。

再次强调:

  1. 只能在component中调用override,但被替换的类可以是component或者object。
  2. 替换的类和被替换的类必须有一定的继承关系,比如新类继承自旧类。

如果不在EDAplayground使用,没有把这所有代码都放在一个design.sv的文件里的话,需要调用include把新增的代码文件都包含进来。

这里只是做了一个transaction激励替换的例子,用这个方法还可以替换平台组件,改变测试平台组件的行为。以后有时间了也写下来。

6.3 field机制

field机制的应用

下面是my_transaction代码:

class my_transaction extends uvm_sequence_item;
    rand bit [3:0] rxd;
    rand bit rxv;
    
    // 重点关注这段代码!
    `uvm_object_utils_begin(my_transaction)
    	`uvm_field_int(rxd, UVM_ALL_ON)
    	`uvm_field_int(rxv, UVM_ALL_ON)
    `uvm_object_utils_end
    // 我是说上面这段!

    constraint Limit{
        rxd inside {[0:15]};
    }
    
    function new(string name = "");
        super.new(name);
    endfunction
    
endclass

可以看到这段代码中有一段调用了`uvm_object_utils_begin和end的代码块。这段代码有两个作用:

  1. 向factory注册my_transaction类,这点和普通的不带begin和end的代码是一样的。
  2. 为begin和end中包围的这些变量(比如这段里写的是rxd和rxv)在类中创建一些实用的方法,例如打印、复制、对比、打包、解压、记录等,这就是field automation机制。简单来说,就是可以用UVM的内建函数处理数据,而不用自己手动编写具有这些功能的方法(函数)。

要让变量具有这些方法,需要用这几个关键语句:

`uvm_field_int()
`uvm_field_queue()

这些宏必须被包括在uvm_object_utils_begin和end之中才能使用。component也具有相同的宏,只需要把这些语句包含在uvm_component_utils_begin和end里就行。

UVM field automation机制的内建方法

方法名功能
clone深度复制,如果被复制的目标成员中包含了其他对象,就调用该对象的clone也复制一份,这种方法是会创造新对象的。该方法仅限于使用了UVM field automation机制的成员。
copy普通复制,如果被复制的目标成员中包含了其他对象,不会调用该对象的copy方法,仅对句柄进行复制,而没有创造新的对象。该方法仅限于使用了UVM field automation机制的成员。
print按照给定格式打印出对象的成员,打印的内容仅限于使用了UVM field automation机制的成员。
sprint与print类似,但返回的是一个字符串
compare深度对比,对比的内容仅限于使用了UVM field automation机制的成员。
pack将成员按一定格式打包成一个数据流,打包的内容仅限于使用了UVM field automation机制的成员。
unpack按成员的规格对数据流进行分解,参与分解的成员仅限于使用了UVM field automation机制的成员。
record对成员做记录,参与记录的成员仅限于使用了UVM field automation机制的成员。

如果在类中有某一个成员没有赋予field automation机制,那么这些方法都对它不起作用。

成员变量的注册方法

大部分变量注册的格式都为:

`uvm_field_*(ARG, FLAG)

其中ARG是变量名,FLAG指屏蔽类型(等一下讲),这里的*一般填入数据类型,比如int,或者int_queue,也就是队列,等等。

但有一种类型的数据例外:枚举类型enum。

`uvm_field_enum(T, ARG, FLAG)

T为枚举类型的名字,ARG为变量名,FLAG为屏蔽类型。

这里记录一些常用的宏,需要的时候可以来这里查找,不需要特别记忆:

`uvm_field_int(ARG, FLAG)
`uvm_field_real(ARG, FLAG)
`uvm_field_enum(T, ARG, FLAG)
`uvm_field_object(ARG, FLAG)
`uvm_field_string(ARG, FLAG)
`uvm_field_array_enum(ARG, FLAG)
`uvm_field_array_int(ARG, FLAG)
`uvm_field_array_string(ARG, FLAG)
`uvm_field_queue_int(ARG, FLAG)
`uvm_field_queue_string(ARG, FLAG)
`uvm_field_aa_int_string(ARG, FLAG)
`uvm_field_aa_string_string(ARG, FLAG)

关键词:UVM field automation成员对象注册方法表

FLAG标识符

类中的每个成员都可以设置它们是否参与某个操作,比如rxd不参与复制,但rxv参与复制等。用来设置这些行为的就是FLAG标识符。

FLAG本质上是一个15位的向量,可以手动地定义这样的向量,然后放在FLAG的位置。还可以用UVM中已经定义好的一些宏,去规定哪些功能开启,哪些功能关闭:

UVM_DEFAULT

// 将所有机制都打开,该成员参与所有方法,一般都用这个就可以
UVM_ALL_ON 

// 是否参与复制
UVM_COPY
UVM_NOCOPY

// 是否参与比较
UVM_COMPARE
UVM_NOCOMPARE

// 是否参与打印
UVM_PRINT
UVM_NOPRINT

// 是否参与记录
UVM_RECORD
UVM_NORECORD

// 是否参与打包
UVM_PACK
UVM_NOPACK

在调用打印的时候,可以指定特殊的打印格式,打印格式有如下几种:

UVM_BIN
UVM_DEC
UVM_UNSIGNED
UVM_OCT
UVM_HEX
UVM_STRING
UVM_TIME
UVM_REAL

使用方法:

`uvm_field_int(field, UVM_ALL_ON | UVM_BIN)

加上这个按位或就行。

6.4 configuration机制

configuration机制个人感觉是应用难度比较大,却能极大地增加验证平台重用性的机制。作为验证平台属性配置的机制,它的强大主要体现在它所能传递的数据类型上:

  1. 它能传递一般的值,可以是整型、实数、队列等,用于配置一些散布在验证平台中的可配置参数。
  2. 传递对象。传递对象最经常的用途是拿它来配置整个平台。
  3. 传递interface,接口。class中不能直接使用interface创建新的接口,只能在内部定义一个虚接口virtual interface,然后由外部传入。

configuration机制的优点

  1. 可以简单地配置某个组件中变量的值,避免外部使用全局变量带来的风险
  2. 高层组件可以在不改变代码的情况下改变子组件的变量
  3. 各个层次都可以使用configuration机制,限制小
  4. 支持通配符和正则表达式
  5. 支持用户自定义的数据类型
  6. 可以在仿真运行的过程中进行配置

configuration原理

UVM configuration实现分为两部分:

设置配置资源(set,可以理解为发送):

调用相关代码后,会把配置内容写入UVM配置资源池中,这类似于一种表格,也类似于factory两张表中的替换表,但他需要的信息更多。

使用静态函数设置:

uvm_config_db#(type)::set(
							uvm_component cntxt, // 资源来源,必须是component
    						string instance_name, // 资源去向,描述这个资源所属的组件,是一个字符串,可以用通配符或者正则表达式
    						string field_name, // 资源的ID,相当于通道名,是其他组件获取这个资源的重要凭证
    						T value // 资源值,可以是值、对象或者接口
);

获取配置资源(get,可以理解为接收):

设置完资源后,尽管UVM资源池内已经记录了数据来源、数据类型、数据去向、资源的ID、资源值这么5项内容,但不代表它已经配置成功了。要想配置成功,还必须在适当的位置获取这些资源。

使用静态函数获取:

uvm_config_db#(type)::get(
							uvm_component cntxt, // 获取配置资源的源组件,在何处获取资源
    						string instance_name, // 被配置的目标对象所在的组件,是个字符串,可用通配符和正则表达式
    						string field_name, // 资源的ID,要和设置时填入的ID一致
    						inout T variable // 
);

传递值例子:为sequence配置资源

获取配置资源

一般来说,sequence中有控制产生随机transaction个数的量。可以让这个量设置成一个变量,然后使用configuration机制从外部去配置,从而实现产生transaction数量的控制。

config配置演示_配置单个整数open in new window

展示修改后的sequence代码:

class my_sequence extends uvm_sequence#(my_transaction);
    `uvm_object_utils(my_sequence)
    
    // 用于控制产生transaction数量的变量,默认值还是填上比较好
    int item_num = 10;
    
    function new(string name="");
        super.new(name);
    endfunction
    
    // pre_randomize函数是systemverilog的内容,
    // 总之它会在随机化前调用,不由phase控制。
    // 用静态函数get接收数据
    function void pre_randomize();
        uvm_config_db#(int)::get(
            // config只能发送东西给平台组件component,而sequence并不是component,
            // 只能发送给它的父对象m_seqr。
            // sequence父对象类是my_sequencer,注意这里写得是父对象的类而不是父对象本身
        	my_sequencer, 
            // 这里是空的,因为就是取到本地
            "",
            // 第三个是资源ID,需要和set时的参数相同,可以用变量名作为ID,方便记
            "item_num",
            // 配置的目标对象,这里就把数据交给item_num就可以
            item_num
        )
    endfunction
    
    virtual task body();
        if(starting_phase != null) begin
            starting_phase.raise_objection(this);
            // 替换掉这里的数
            repeat(item_num) begin
            	`uvm_do(req)
            end
            #100;
            if(starting phase != null) begin
                starting_phase.drop_objection(this); 
            end
        end
    endtask
endclass

设置配置资源

因为sequence中配置了它将从m_seqr中获取数据,所以这边需要发送数据给到m_seqr。记住资源的ID,就是第三个参数,要和获取的一致才行。这里将从顶层去配置这个在sequence里的item_num。

class my_test extends uvm_test;
    // 注册
    `uvm_component_utils(my_test)

    my_env m_env;
    
    // 构造
    function new(string name="", uvm_component parent);
        super.new(name, parent);
    endfunction

    // build
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        m_env = my_env::type_id::create("m_env", this);
        
        uvm_config_db#(uvm_object_wrapper)::set(this, "*.m_seqr.run_phase", "default_sequence", my_sequence::get_type());
        
        // 在这里设置item_num资源值
        uvm_config_db#(int)::set(
        	// 资源源,是谁发送的数据,写this就行,从这里发送过去
            this,
            // 配置对象所属的component,是sequencer,即m_seqr,顺着向下找到它
            "*.m_seqr",
            // 资源ID,刚才说了是用变量名来做资源ID,便于记忆
            item_num,
            // 资源值,直接给个int类型的数就行,这里给20
            20
        );
    endfunction

    virtual function void start_of_simulation_phase(uvm_phase phase);
        super.start_of_simulation_phase(phase);
        uvm_top.print_topology(uvm_default_tree_printer);
    endfunction
endclass

这样就完成了资源配置,把my_sequence的值设置成了20。

进一步理解config db的参数

这里还需要进一步理解config_db的参数。对于uvm_config_db#(type)::set()的四个参数:

uvm_config_db#(type)::set(
							uvm_component cntxt,
    						string instance_name,
    						string field_name,
    						T value
);

cntxt,指的是context,是个UVM组件,而instance name是实例名的意思,它们放在一起将组合成可见范围。例如,如果填上这节所讲的参数,变成:

uvm_config_db#(int)::set(
            this, // 这个this来自于my_test
            "*.m_seqr",
            item_num,
            20
        );

那么,可见范围就是my_test中的m_seqr及以下的部分。即,这条配置信息只对sequencer和它的子对象可见。当然, 第一个参数也可以填上null,那么在第二个参数中补齐可见范围即可:

uvm_config_db#(int)::set(
            null,
            "uvm_test_top.m_env.m_seqr",
            item_num,
            20
        );

可见范围还是同样的,m_seqr和它的子对象。

为什么是uvm_test_top呢?因为不论启动怎么样的testcase(这里是my_test),UVM都会自动地把它的实例命名为uvm_test_top

传递interface例子

interface,接口,是driver与DUT、monitor与DUT直接连接的部分,如图所示:

interface的连接情况
interface的连接情况

刚才已经说过,interface不能直接用于class中,所以需要建立一个虚接口virtual interface的句柄,这也是缩写vif的由来。虚接口不能直接例化,需要从外部接收,这就需要用到UVM configuration。

config配置演示_配置接口open in new window

先看一下用于第二章所述DUT的interface的代码,这里不作详解:

interface dut_interface(
  input bit clk
);
  
  logic rst_n;
  logic [3:0] rxd;
  logic rx_v;
  logic [3:0] txd;
  logic txv;
  
  clocking imonitor_cb@(posedge clk);
    default input #1 output #0;
    input rxd;
    input rxv;
  endclocking
  
  
  clocking driver_cb@(posedge clk);
    default input #1 output #0;
    output rst_n;
    output rxd;
    output rxv;
  endclocking
  
  
  clocking omonitor_cb@(posedge clk);
    default input #1 output #0;
    input txd;
    input txv;
  endclocking
  
  modport imonitor(clocking imonitor_cb);
  modport driver(clocking driver_cb, output rst_n);
  modport omonitor(clocking omonitor_cb);
  
endinterface

在driver中接收interface(在monitor中接收也是类似的):

class my_driver extends uvm_driver#(my_transaction);
    `uvm_component_utils(my_driver)
    
    // 声明一个虚接口句柄,用已经定义好的接口,在前面加上virtual即可
    virtual dut_interface m_vif;
    
    function new(string name="", uvm_component parent);
        super.new(name, parent);
    endfunction
        
    // 接收接口到vif
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        // 在build_phase中接收
        uvm_config_db#(virtual dut_interface)::get(this, "", "vif", m_vif);
    endfunction
    
    // 驱动DUT的方法
    virtual function void pre_reset_phase(uvm_phase phase);
        super.pre_reset_phase(phase);
        // 这是12个runtime phase之一,而且驱动DUT是需要消耗时间的,
        // 所以需要提起objection,详见下一节。
        // 在pre_reset_phase中,先把modport中属于driver的那部分给上不定态。
        phase.raise_objection(this);
        m_vif.driver_cb.rxd <= 4'bx;
        m_vif.driver_cb.rxv <= 1'bx
        // 不要忘记drop。
        phase.drop_objection(this);
    endfunction
    
    virtual function void reset_phase(uvm_phase phase);
        super.reset_phase(phase);
        phase.raise_objection(this);
        // 可以开始赋初值,这里给上reset后的值
        m_vif.driver_cb.rxd <= 4'b0;
        m_vif.driver_cb.rxv <= 1'b0;
        phase.drop_objection(this);
    endfunction
    
    virtual task run_phase(uvm_phase phase);
        // run phase不属于runtime phase,不需要继承,也不需要提起objection
        // 根据接口协议,驱动DUT。这里的DUT只是一个简单的数据转发模块,所以只是简单地把数据发送到接口就行
        // 接收transaction分解后发送给DUT
        forever begin
            seq_item_port.get_next_item(req);
            // 打印一下收到的数据
            `uvm_info("DRIV_RUNPHASE", req.sprint(), UVM_MEDIUM)
            m_vif.driver_cb.rxd <= req.rxd;
            m_vif.driver_cb.rxv <= req.rxv;
            
            // 等待一个周期,打印已经驱动的信息后结束这组数据的仿真
            @m_vif.driver_cb;
            `uvm_info("DRIV_RUNPHASE", "Data has driven.", UVM_MEDIUM)
            seq_item_port.item_done();
        end
    endtask
endclass

接口要在顶层实例化。由于program是不能实例化DUT的,所以需要在module中实例化。

"include 所有代码"

module top;
    bit sys_clk;
    
    dut_interface m_inf(sys_clk);
    
    dut dut_inst(
        .clk(m_inf.clk),
        .rst_n(m_inf.rst_n),
        
        .rxd(m_inf.rxd),
        .rxv(m_inf.rxv),
        
        .txd(m_inf.txd),
        .txv(m_inf.txv)
    );
    
    // 时钟
    initial begin
       sys_clk = 1'b0;
        forever #10 sys_clk = ~sys_clk;
    end
    
    // 给driver传递inf,这个接口还要传递给monitor,所以直接传给agent,
    // 让monitor和driver都可见,再让它们分别从agent中取得就行
    initial begin
        uvm_config_db#(virtual dut_interface)::set(null, "*.m_agent.*", "vif", inf); 
        run_test("testcase");
    end
    
    // 产生波形文件
    initial begin
        // $wlfdumpvars(); 
        // 在edaplayground中改用以下这两句:
        $dumpfile("dump.vcd");
    	$dumpvars;
    end
    
endmodule

传递对象例子

这是configuration机制中最难的例子。他需要新建一些类,用于配置整个验证平台。这是一种比较高级的用法。

暂时不给出这个例子的代码。一来验证平台到这里还不是那么复杂,其次,reference model和scoreboard还没有加入,没有这么多需要配置的类。

configuration机制可以传递用户自定义的类。当验证平台中需要配置的参数太多、太散的时候,对他们一个个单独使用set和get其实非常麻烦。

可以把这些所有需要配置的参数封装成一个配置类,让平台组件顶层先获取这个类,取出自己需要的数值,然后把余下的部分传递给子对象让他们配置,然后再传递给子对象的子对象取出数据完成配置,这样一层层地往下传,直到配置完整个平台。

那么,首先需要确定有哪些变量需要配置,这里举出几个例子:

  1. env中:

    is_coverage 是否需要进行覆盖率统计

    is_check 是否需要加入scoreboard

  2. agent中:

    is_active agent是否为active模式

  3. sequencer中:

    item_num sequence产生的个数,其实不是sequencer本身需要,而是要传给sequence的,但只能由sequencer来接收

  4. driver中:

    vif 虚接口

  5. monitor中:

    vif 虚接口

假设就有这么些数据需要配置,而且在参数类往下传的过程中,先经过env,后经过agent,所以需要创建两个类,一个类名叫env_cfg,配置env,一个类名叫agent_cfg,配置agent,而且agent_cfg应该是env_cfg的子对象。

而driver、monitor因为已经是终端了,所以不需要再向下传,所以不需要再单独建立一个类。

// 配置文件一般扩展自uvm_object,而不是component
class agent_cfg extends uvm_object;
    // agent 本身的配置数据,这些数据最好都给上初值,下同
    is_active = UVM_ACTIVE;
    
    // 给sequencer
    item_num = 20;
    
    // 给driver和monitor
    virtual dut_interface m_vif;
    
    // 因为配置数据需要复制,所以用field automation机制提供这些方法
    `uvm_object_utils_begin(agent_cfg)
    	// 一种特殊的数据类型
    	`uvm_field_enum(uvm_active_passive_enum, is_active, UVM_ALL_ON)
    	`uvm_field_int(item_num, UVM_ALL_ON)
    `uvm_object_utils_end
    
    // 构造函数
    function new(string name="");
        super.new(name);
    endfunction
    
endclass

然后再写env的配置文件:

class env_cfg extends uvm_object;
    // env本身的配置数据,这些数据最好都给上初值
    is_coverage = 0;
    is_check = 0;
    
    // 需要包含agent_cfg,先声明句柄
    agent_cfg m_agent_cfg;
    
    `uvm_object_utils_begin(env_cfg)
    	`uvm_field_int(is_coverage, UVM_ALL_ON)
    	`uvm_field_int(is_check, UVM_ALL_ON)
    	// 要把子对象的类也加进来
    	`uvm_field_object(m_agent_cfg, UVM_ALL_ON)
    `uvm_object_utils_end
    
    // 构造函数
    function new(string name="");
        super.new(name);
        // 这里不用factory创建子对象,直接构造就行
        m_agent_cfg = new("m_agent_cfg");
    endfunction
    
endclass

在这一整个类和需要它的地方都需要声明一个句柄,用以接收config_db传递来的信息。

而在testcase这样的高层模块,则需要调用它的构造函数,而且需要配置这个配置类。

class my_test extends uvm_test;
    `uvm_component_utils(my_test)
    
    my_env m_env;
    
    // 声明配置类句柄
    env_cfg m_env_cfg;
    
    function new(string name = "", uvm_component parent);
        super.new(name, parent);
        // 实例化配置类
        m_env_cfg = new("m_env_cfg");
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        m_env = my_env::type_id::create("m_env", this);
        uvm_config_db#(uvm_object_wrapper)::set(
            this, "*.m_seqr.run_phase", "default_sequence", my_sequence::get_type()
        );
        
        // 给env_cfg和agent_cfg里的数值赋值
        m_env_cfg.is_coverage = 0;
        m_env_cfg.is_check = 0;
        m_env_cfg.m_agent_cfg.item_num = 40;
        
        // 需要传递的虚接口,需要从top获得,传递给配置类的m_vif,直接用句点就能表示它。
        if(!uvm_config_db#(virtual dut_interface)::get(this, "", "top_vif", m_env_cfg.m_agent_cfg.m_vif)) begin
            // 如果没有成功得到vif,就没必要继续了
            `uvm_fatal("CONFIG_ERROR", "无法获得顶层传来的接口!")
        end
        
        // 传递配置类给env,接下来转到env去解包并继续向下传递
        uvm_config_db#(env_cfg)::set(
            this, "*.m_env", "env_cfg", m_env_cfg
        );
    endfunction
    
endclass

下面展示env类中,如何接收、解包配置自身,并把agent的配置信息传下去:

class my_env extends uvm_env;
    `uvm_component_utils(my_env)
    
    int is_coverage = 0;
    int is_check = 0;
    
    // 注意我这里用了m_agent,而不是之前的i_agent
    my_agent m_agent;
    
    // 需要声明接收配置类的句柄,不需要初始化
    env_cfg m_env_cfg;
    
    function new(string name = "", uvm_component parent);
        super.new(name, parent);
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        
        m_agent = my_agent::type_id::create("m_agent", this);
        
        // 接收来自testcase的数据,并保存到env_cfg里
        if(!uvm_config_db#(this, "", "env_cfg", m_env_cfg)) begin
            `uvm_fatal("CONFIG_ERROR", "无法获得来自testcase的接口!")
        end
        
        // 然后再把中间的agent_cfg传递给agent
        uvm_config_db#(agent_cfg)::set(this, "*.m_agent", "m_agent_cfg", m_env_cfg.m_agent_cfg);    
        // 提示现在的覆盖率收集设置为开启
        if(m_env_cfg.is_coverage) begin
            `uvm_info("COVEARGE_ENABLED", "覆盖率收集已开启!", UVM_MEDIUM)
        end
        
        // 提示scoreboard开启
        if(m_env_cfg.is_check) begin
            `uvm_info("SCOREBOARD_ENABLED", "计分板已开启", UVM_MEDIUM)
        end
            
    endfunction
endclass

现在,env已经把env_cfg中的agent_cfg单独取出并发送给agent了,需要在agent处获取,并配置。

class my_agent extends uvm_agent;
    `uvm_component_utils(my_agent)
    
    my_sequencer m_seqr;
    my_driver m_driv;
    my_monitor m_mon;
    
    // 声明agent_cfg的句柄
    agent_cfg m_agent_cfg;
    
    function new(string name = "", uvm_component parent);
        super.new(name, parent);
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        // 这里是master agent,就不写判断条件了,最好是写上
        m_seqr = my_sequencer::type_id::create("m_seqr", this);
        m_driv = my_driver::type_id::create("m_driv", this);
        m_mon = my_monitor::type_id::create("m_mon", this);
        
        // 接收agent_cfg
        if(!uvm_config_db#(agent_cfg)::get(this, "", "m_agent_cfg", m_agent_cfg);) begin
            `uvm_fatal("AGENT_CONFIG_ERROR", "没有接收到来自ENV的配置类!") 
        end
        
        // 继续往下传!
        uvm_config_db#(virtual dut_interface)::set(
        	this, "m_driv", "driv_vif", m_agent_cfg.m_vif
        );
        uvm_config_db#(virtual dut_interface)::set(
        	this, "m_mon", "mon_vif", m_agent_cfg.m_vif
        );
        // 还有这个item_num要传给sequence
        uvm_config_db#(int)::set(
        	this, "my_seqr", "item_num", agent_cfg.item_num
        );
    endfunction
    
    
endclass

传递vif这里只写driver的了,monitor的同理

class my_driver extends uvm_driver#(my_transaction);
    `uvm_component_utils(my_driver)
    
    virtual dut_interface m_vif;
    
    function new(string name = "", uvm_component parent);
        super.new(name, parent);
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        // 接收vif
        uvm_config_db#(virtual dut_interface)::set(
        	this, "", "driv_vif", m_vif
        );
    endfunction
    
    // 后面的代码就和之前没有区别了。
    
endclass

接下来是接收item_num的sequence,其实也和以前没什么大的区别:

class my_sequence extends uvm_sequence#(my_transaction);
    `uvm_object_utils(my_sequence)
    
    int item_num = 10;
    
    function new(string name="");
        super.new(name);
    endfunction

    virtual function void pre_randomize();
        uvm_config_db#(int)::get(
        	my_sequencer, "","item_num",item_num
        )
    endfunction
    
    virtual task body();
        if(starting_phase != null) begin
            starting_phase.raise_objection(this);
            // 替换掉这里的数
            repeat(item_num) begin
            	`uvm_do(req)
            end
            #100;
            if(starting phase != null) begin
                starting_phase.drop_objection(this); 
            end
        end
    endtask
endclass

而在顶层,需要在initial块中,配置好interface,并把它开始往下传,这点千万不要忘记。

initial begin
    uvm_config_db#(virtual dut_interface)::set(null, "uvm_test_top", "vif", inf); 
    run_test("testcase");
end

最后强调一下,不论是set还是get,它的第一个参数都需要是一个component,千万不要忘记。

这一小节是整个这一章最困难的地方,后面的那些机制都没有这个难。但其实它并没有那么难,只是配置起来比较繁琐。工程中用到这种配置方法的机会可能也不是很多。

6.5 phase机制

UVM phase

UVM为平台组件定义了一套phase流程来控制仿真平台的执行过程,phase是uvm_component的属性。

下面是UVM phase的运行流程:

UVM phase运行流程
UVM phase运行流程

后面的描述需要看着这张图来理解。

为什么需要phase?

从验证平台的构建、配置到连接,再到运行,最后停止,需要有一定的先后顺序

例如,在构建平台过程中,需要先实例化对象,配置对象,然后再连接各个组件。如果先连接组件,再实例化对象,就会出错。如果把实例化和配置打包成一个函数,再把连接打包成另一个函数,就可以确保按顺序执行。

每一个phase阶段都有一个任务或函数与其对应,例如实例化对应的是build_phase,等等。

同一个phase有两种顺序:

  1. 自顶向下(构造阶段):从树根root开始,自顶向下运行,直到每一个树叶。按照我的理解,这句话的意思是,在诸如build的阶段,需要不断地调用构造函数,必须先构造出env这样的容器类,才能继续向下构造agent这样的子对象;必须先构造出agent,才能继续构造sequencer、driver、monitor这样的子对象,所以是从树根到树叶。
  2. 自下向上(执行阶段):从树叶开始,一直到树根root。不同于构造连接阶段,执行阶段会从树叶开始,一直到树根。

task phase和function phase

task phase是会消耗仿真时间的,而function phase不消耗仿真时间。在上图中可以看出,只有执行阶段的run phase和12个runtime phase是task phase,其他的都是function phase。

task phase执行顺序

在准备阶段的start_of_simulation phase执行完成后,仿真会同时进入run_phase和pre_reset phase。也就是说,run phase和runtime phase是并行执行的。它们同时存在,可以共同使用。

而每一个组件的同名phase执行是会同步的。只有当每一个组件的同名phase全部执行完毕后,才会一起执行下一个phase,就算是function phase也一样

未定义的phase

有些组件在某个phase中定义了功能,而其他组件没有在这个phase定义功能,不代表这些组件就不执行这个phase了。这些phase仍会执行,只是不具有任何功能。

phase的启动

只需要在顶层模块的initial块中执行代码run_test()启动验证平台,就可以自动启动phase了。只要将想要的功能对应写入这些phase,就会执行。

UVM task phase objection

objection是phase的属性,用来控制task phase的开始与结束。在先前的sequence代码中有这这样一对代码:

starting_phase.raise_objection(this);
starting_phase.drop_objection(this);

它们能控制task phase的开始和终止,同步components的同名task phase。objection可以在component或object中提起。每一次调用raise_objection都需要一个drop_objection与其对应。

使用时要注意:

  1. objection对task phase才有意义,控制task phase的开始和终止。
  2. 必须在执行消耗仿真时间的语句之前raise objection,否则整个phase都会失效。
  3. 在task phase结束时,需要调用drop objection,否则仿真会卡在这里,永不结束。
  4. 在某个component的phase中调用的raise_objection会启动其他的同名phase,drop同理。因此,在某个component中的phase中没有调用raise_objection并不一定导致它不启动,它可能会被其他component的同名phase启动。但是,如果在他运行完毕之前,提起raise_objection的phase已经调用了drop_objection,那么它会在没被执行完就终止,导致bug。所以原则上要求每一个需要运行的component都独立地调用一次raise_objection,而不是由其他component启动
  5. 还有一点, objection只针对runtime phase,12个runtime phase只要有任何一个提起了objection,那么run phase也会跟着执行,不需要raise objection
未调用raise_objection时一种可能发生的运行错误
未调用raise_objection时一种可能发生的运行错误

objection 使用方法总结

  1. 尽量在每个task phase都调用raise_objection和drop_objection
  2. 不要在无限循环的task phase中使用raise_objection和drop_objection
  3. 一定要成对使用raise_objection和drop_objection
  4. 每个task phase中尽量只使用一次
  5. raise_objection要在第一个消耗仿真时间的语句前使用

12个runtime phase的记忆方法

显然runtime phase可以分为4个阶段,reset、configure、main、shutdown,每个阶段又有pre、本身、post这三个阶段,很容易记得。

6.6 UVM信息服务机制

为了替代不方便使用的display函数,UVM提供了一系列信息打印服务。

优势:

  1. 可以显示打印信息在平台中的位置
  2. 通过层次、等级、时间等对打印信息进行过滤

信息安全等级

UVM的信息安全等级分为4个,FATAL、ERROR、WARNING、INFO。这四个等级对应不同的行为,每个安全等级都有默认的行为。

安全等级/仿真行为UVM_EXITUVM_COUNTUVM_DISPLAYUVM_LOGUVM_CALL_HOOKUVM_NO_ACTION安全等级描述
UVM_FATAL默认行为2默认行为1不能容忍的错误,安全等级最严重
UVM_ERROR默认行为2默认行为1仿真本身的错误,例如scoreboard检查数据有误,无关平台
UVM_WARNING默认行为警告信息,可能会影响仿真
UVM_INFO默认行为一般的报告信息,安全等级最低

UVM_EXIT:立即终止仿真

UVM_COUNT:记录报告数量,达到设定的上限后自动停止仿真。上线设定方法:set_report_max_quit_count()

UVM_DISPLAY:在终端打印出相关信息

UVM_LOG:将相关信息写入指定文件中

UVM_CALL_HOOK:调用相关的回调函数

UVM_NO_ACTION:不会执行任何动作

四种安全等级的宏

`uvm_fatal("ID", "Message")
`uvm_error("ID", "Message")
`uvm_warning("ID", "Message")
`uvm_info("ID", "Message", verbosity)

除了info之外,其他的一定是会打印出来的,所以不需要冗余度。

uvm_info

`uvm_info("信息的ID", "打印的信息", 可视化等级)

可视化等级也被称为信息冗余度,可以在后面规定需要打印出来的最高等级,默认时,只有UVM_MEDIUM和UVM_LOW是会被打印的。

假如在一次测试中,增加了很多uvm_info,而后续的测试不需要这些info,可以调整冗余度可视程度,屏蔽掉这些信息。所以,需要提前搞清楚这些info的使用情况,安排一个统一的冗余度,免得需要一个一个注释。

可视化等级共有5种:UVM_LOW、UVM_MEDIUM、UVM_HIGH、UVM_FULL、UVM_DEBUG。一般只用前三个,后两个很少用。在UVM代码中,有这样一段代码:

typedef enum{
    UVM_NONE = 0,
    UVM_LOW = 100,
    UVM_MEDIUM = 200,
    UVM_HIGH = 300,
    UVM_FULL = 400,
    UVM_DEBUG = 500
} uvm_verbosity

运行仿真的时候可以可以指定可视化等级,在仿真命令里加入:+UVM_VERBOSITY=UVM_*

这是一条标准线,在这以上(不包括这个等级)的信息就会被屏蔽。

不止可以在仿真命令里进行全局限制,也可以用函数在component中设置:

// 只能设置当前组件info的可视化等级
set_report_verbosity_level(verbosity);
// 用来设置当前组件及当前组件的子组件info的可视化等级
set_report_verbosity_level_hier(verbosity);

自定义信息的默认行为

刚才的表格给出了每一种信息的默认行为,这些行为是可以修改的。

在testcase中的start_of_simulation_phase中可以用以下语句修改:

// 这句话是根据安全等级来修改行为的,覆盖面广,但优先级低,
// 可以被另外两种优先级更高的设置语句覆盖。
set_report_serverity_action(severity, action);

// 通过信息的ID改变行为。
set_report_id_action(ID, action);

// 根据安全等级+信息ID修改行为,它的覆盖范围最小,但却是优先级最高的行为修改语句。
set_report_severity_id_action(severity, ID, action);

看下面几条修改的例子:

// 这条语句会让INFO不再有任何操作,甚至不打印出信息
set_report_severity_action(UVM_INFO, UVM_NO_ACTION);

// 这条语句让ID为"DRV_RUN_PHASE"的信息只打印信息,没有其他后续行为,
// 不论他是否是FATAL。但如果它是INFO,则会覆盖上一条语句。
// 即使它是被设置为UVM_NO_ACTION的INFO,他也会照样打印出信息。
set_report_id_action("DRV_RUN_PHASE", UVM_DISPLAY);

// 这条语句会让类型为INFO的,具有"MON_RUN_PAHSE"的ID的信息触发时,直接退出仿真。
set_report_severity_id_action(UVM_INFO, "MON_RUN_PHASE", UVM_EXIT);

// 如果这样设置,那么不满足第二条和第三条条件的INFO语句,
//都不会有任何行为(因为最前面对UVM_INFO设置了UVM_NO_ACTION)

还有些其他的设置方法,但并不常用,工作中大概也不用这么复杂的设置,就不讲了。

示例代码

  1. factory和override机制演示代码open in new window
  2. config配置演示_配置单个整数open in new window
  3. config配置演示_配置接口open in new window
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.5