sequence、item、driver

新手上路

        在UVM世界,利用其核心特性,在创建了组件和顶层环境,并且完成组件之间的TLM端口连接以后,接下来就可以使得整个环境开始运转了。(generator控制仿真可以停下来,别的组件的run phase里面都是forever一直运行)
        在经过一番实践,掌握了组件之间的TLM通信方式,开辟了建筑之间的道路、桥梁和河道以后,就可以进入紧张繁忙的物流期了。运转的必要条件是组件之间需要有事务(transaction)传送,这就同管道连接好需要引入水流一样。
        在本章我们将主要围绕下面几个核心词来阐述它们的作用、分类以及之间的互动关系:
 sequence item(transaction)、sequence(generator)、sequencer、driver

        如果按交通道路的车流来打比方,sequence就是道路(创建,randomize),sequence item是道路上行驶的货车,sequencer是目的地的关卡,而driver便是最终卸货的地方(interface上做硬件信号的激励)。
        从软件实施层面来讲,这里的货车是从sequence一端出发的,经过了sequencer,最终抵达driver。经过driver的卸货,每一辆货车也就完成了它的使命。driver对每一件到站的货物,经过扫描处理,将它们分解为更小的信息量,提供给DUT。

序列组件的互动

sequence、item、driver_第1张图片

在这个过程中,不同的角色之间会进行下面这些互动:

  • sequence对象会产生目标数量的sequence item对象。借助于SV的随机化和sequence item对随机化的支持,使得产生的每个sequence item对象中的数据内容都不相同。
  • 产生的sequence item会经过sequencer再流向driver。
  • driver陆续得到每一个sequence item,经过数据解析,将数据按照与DUT的物理接口协议写入到接口上,对DUT形成有效激励。当有必要时,driver在每解析并消化完一个sequence item后,它会将最后的状态信息写回sequence item对象再返回给sequencer,最终抵达sequence对象一侧。

        这么做的目的在于,有时sequence需要得知driver与DUT互动的状态,这就需要driver有一个回路将更新过的sequence item对象写回至sequence一侧。

        sequence item是driver与DUT每一次互动的最小粒度内容。

        例如DUT如果是一个slave端,driver扮演master去访问DUT的寄存器,那么sequence item需要定义的数据信息至少包括访问地址、命令码、数据和状态值,这样的信息在driver取得后,会通过时序方式在interface—侧发起激励送至DUT。
        按照一般总线做寄存器访问的习惯,这种访问在时序上大致会保持几个时钟周期,直至数据传送完毕,而由driver再准备发起下一次操作。
        用户除了可以在声明sequence item时添加必要的成员变量,也可以添加对这些成员变量进行操作的成员方法。这些添加了的成员变量,需要充分考虑在通过sequencer传递到driver前是否需要随机化。

        对于一个sequence而言,它会产生多个sequence item,也可以产生多个sequence。(大的sequence包含小的sequence)从产生层次来看,sequence item是最小粒度,它可以由sequence生成,而相关sequence也可以进一步组织继而实现层次化,最终由更上层的sequence进行调度。这么看来,sequence可以看做是产生激励内容的载体
        在sequence与driver之间起到桥梁作用的是sequencer。由于sequencer与driver均component组件,它们之间的通信也是通过TLM端口实现的。
        driver与sequencer之间的TLM通信参数即sequence item类。由于这一限制,使得sequencer到driver的传输数据类型不能改变,同时与sequencer挂接的sequence创建的sequence item类型也
应为指定类型。(sequence、sequencer、driver三者传输的transaction类型一致)

        激励驱动链的最后一道关卡是driver。
        对于常见用法,driver往往是一个sequence item消化完,报告给sequencer和sequence,同时再请求消化下一个sequence item。
        driver看起来永远喂不饱,同时还又对食物很挑剔。在消化每一个sequence item之前,该item中的数据是已经随机化好的,所以每个item内容一般都是各不相同的
        driver自己并不会轻易修改item中的值,它会把item中的数据按照与DUT的物理协议时序关系驱动到接口上面。
        例如对于一个标准的写操作,driver不但需要按照时序依次驱动地址总线、命令码总线和数据总线,还应该等待从端的返回信号和状态值,这样才算完成了一次数据写传输

继承关系

sequence、item、driver_第2张图片

        uvm_sequence_item和uvm_sequence都是基于uvm_object,它们不同于uvm_component只应当在build阶段作为UVM环境的“不动产”进行创建和配置,而是可以在任何阶段创建。

这种类的继承带来的UVM应用区别在于:

  • 由于无法判定环境在run阶段什么时间点会创建sequence和将其挂载(attach)到sequencer上面,所以无法通过UVM环境结构或者phase机制来识别sequence的运行阶段。
  • 由于uvm_object独立于build阶段之外,这使得用户可以有选择地、动态地在合适时间点挂载所需要的sequence和item。
  • 考虑到uvm_sequence和uvm_sequence_item并不处于UVM结构当中,所以顶层在做配置时,无法按照层次关系直接配置到sequence中。(组件才有字符串的层次关系)
  • 如果sequence一旦活动起来,它必须挂载到一个sequencer上,这样sequence可以依赖于sequencer的结构关系,间接通过sequencer来获取顶层的配置和更多信息

sequence和sequencer如何通信(对象和组件)(用uvm_event?)、sequence如何配置(无法用config_db)(sequence通过sequencer获取配置)?

提示

        从常规的认知方式来看,用户可能更愿意将时序控制的权利赋予sequence。这种从sequence产生item,继而将item通过sequencer推送给driver的方式,实际上有一些“越俎代庖”的嫌疑。
        如果明确划分责任的话,sequence应该只负责生成item的内容,而不应该控制item消化的方式和时序,而驱动激励时序的任务应当由driver来完成
        item的生成和传送,并不表示最终的接口驱动时序,决定这一点的还包括sequencer和driver。sequencer之所以作为一个“路由”管道,设立在sequence和driver之间,我们看重的是它的两个特点:

  • sequencer作为一个组件,它可以通过TLM端口与driver传送item对象。
  • sequencer在面向多个并行sequence时,它有充分的仲裁机制来合理分配和传送item,继而实现并行item数据传送至driver的测试场暑

总结

        数据传送机制采用的是get模式而不是put模式。我们在TLM传输中为读者介绍过两种典型的数据传送场景,如果是put模式,那么应该是sequencer将数据put至driver,而如果是get模式,那么应当是driver从sequencer获取item。
        之所以选择get模式,UVM是基于下面的考虑:

  • 如果是get模式,那么当item从sequence产生,穿过sequencer到达driver时,我们就可以结束该传输(假如不需要返回值的话)。而如果是put模式则必须是sequencer将item传送至driver,同时必须收到返回值才可以发起下一次的传输。这从效率上看,是有差别的。
  • 如果需要让sequencer拥有仲裁特性,可以使得多个sequence同时挂载到sequencer上面,那么get模式更符合“工学设计”。这是因为driver作为initiator,一旦发出get请求,它会先通过sequencer,继而获得仲裁后的item。

(initiator、put和get,具体使用差别优势?)

sequence和item

        无论是自驾item,穿过sequencer交通站通往终点driver,还是坐上sequence大巴,一路沿途观光,最终跟随导游停靠到风景点drivero
        在介绍如何驾驶item和sequence,遵守什么交规,最终可以有序穿过sequencer抵达driver之前,有必要首先认识sequence与item之间的关系。
        这里的sequence指的是uvm_sequence类,而item指的是uvm_sequence_item类,我们简称其为sequence和item。                                                                                                                            对于激励生成和场景控制,是由sequence来编织的,而对于激励所需要的具体数据和控制要求,则是从item的成员数据得来的。(即使只有一个item,也不能直接将item挂载到sequencer上。)

        item是基于uvm_object类,这表明了它具备UVM核心基类所必要的数据操作方法,例如copy()、clone()、compare()、record()等。
        item通常应该具备有什么类型的数据成员呢?我们将它们划分为如下几类:

  • 控制类。譬如总线协议上的读写类型、数据长度、传送模式等。(寄存器访问)
  • 负载类。一般指数据总线上的数据包。(channel写入)
  • 配置类。用来控制driver的驱动行为,例如命令driver的发送间隔或者有无错误插入。(控制formatter行为)
  • 调试类。用来标记一些额外信息方便调试,例如该对象的实例序号、创建时间、被driver解析的时间始末等。

item使用时的特点:

  • 如果数据域属于需要用来做驱动,那么用户应考虑定义为rand类型,同时按照驱动协议给出合适的constraint。
  • 由于item本身的数据属性,为了充分利用UVM域声明的特性,我们建议将必要的数据成员都通过‘uvm_field_xxx宏来声明,以便日后uvm_ object的基本数据方法自动实现,例如上面的print()函数。
  • t1没有被随机化而t2被随机化了,这种差别在item通往sequencer之前是很明显的。UVM要求item的创建和随机化都应该发生在sequence的body()任务中,而不是在sequencer或者driver中。(body类似组件的run phase一样)
  • 按照item对象的生命周期来区分,它的生命应该开始于sequence的body()方法,而后经历了随机化并穿越sequencer最终到达driver,直到被driver消化之后,它的生命一般来讲才会结束。(没有句柄指向它(item))之所以要突出这一点,是因为一些用户在使用中会不恰当地直接操作item对象,直接修改其中的数据,或者将它的句柄发送给其它组件使用,这会无形中修改item的数据基因,或者延长一个item对象的寿命。这种不合适的对象操作方式是需要注意的,可以取代的方式则是合理利用copy()和clone()等数据方法。

Item与Sequence的关系

  • 一个sequence可以包含一些有序组织起来的item实例,考虑到item在创建后需要被随机化,sequence在声明时也需要预留一些可供外部随机化的变量,这些随机变量一部分是用来通过层级传递约束来最终控制item对象的随机变量,一部分是用来对item对象之间加以组织和时序控制的。
  • 为了区分几种常见的sequence定义方式,我们在介绍sequence之前首先将其分类为:
  1. 扁平类(flat sequence)。这一类往往只用来组织更细小的粒度,即item实例构成的组织
  2. 层次类(hierarchical sequence)。这一类是由更高层的sequence用来组织底层的sequence,进而让这些sequence或者按照顺序方式,或者按照并行方式,挂载到同一个sequencer上。
  3. 虚拟类(virtual sequence)。这一类则是最终控制整个测试场景的方式,鉴于整个环境中往往存在不同种类的sequencer和其对应的sequence,我们需要一个虚拟的sequence来协调顶层的测试场景。之所以称这个方式为virtual sequence,是因为该序列本身并不会固定挂载于某一种sequencer类型上,而是将其内部不同类型sequence最终挂载到不同的目标sequencer上面。这也是virtual sequence不同于hierarchical sequence的最大一点。

Flat Sequence介绍

  • 一个flat sequence往往由细小的sequence item群落构成,在此之上sequence还有更多的信息来完备它需要实现的激励场景。
  • 一般对于flat sequence而言,它包含的信息有:
  1. sequence item以及相关的constraint用来关联生成的item之间的关系,从而完善出一个flat sequence的时序形态。
  2. 除了限制sequence item的内容,各个item之间的时序信息也需要由flat sequence给定,例如何时生成下一个item并且发送至driver。
  3. 对于需要与driver握手的情况(例如读操作),或者等待monitor事件从而做出反应(例如slave的memory response数据响应操作), 都需要sequence在收到另外一侧组件的状态后,再决定下一步操作,即响应具体事件从而创建对应的item并且发送出去。

(一旦把sequence挂载到sequencer上,body就会自动运行,就像run一样,不需要手动调用)

class bus_trans extends uvm_sequence_item;
    rand bit write;
    rand bir data;
    rand int addr;
    rand int delay;
    static int id_num;
    `uvm_object_utils_begin(bus_trans)
      `uvm_field_int...
    `uvm_object_utils_end
endclass
class flat_seq extends uvm_sequence;
    rand int length;
    rand int addr;
    rand int data[]; //这是数组,有多个数据不同于transaction,层次化的关系。item只包含一个data
    rand bit write;
    rand int delay;
    constranint cstr {
        data.size() == length;
        foreach(data[i]) soft data[i]==i;
        soft addr == 'h100;
        soft write == 1;
        delat inside {[1:5]};
    };
    `uvm_object_utils(flat_seq)
    ...
    task body();
      bus_trans tmp;
      foreach(data[i]) begin
        tmp =new();  //new在foreach中,创建很多个item
        tmp.randomize() with {data == local::data[i];  //sequence里面的随机变量进一步影响
                              addr == local::addr + i<<2; //item里面的随机变量,在外部例化  
                              write == local::write;  //会有进一步的传递过程
                              delay == local::delay;};
        tmp.print();
      end
    endtask
class test1 extends uvm_test;
    `uvm_component_utils(tests1)
    ...
    task run_phase(uvm_phase phase);
        flat_seq seq;
        phase.raise_objection(phase);
        seq=new();
        seq.randomize() with {addr == 'h200;length == };
        seq.body();  //通过start函数挂载到sequencer上才会自动执行
        phase.drop_objection();
    endtask
endclass
  • 我们暂时没有使用sequence的宏或者其它发送item的宏来实现sequence/item.与sequencer之间的传送,而是用更直白的方式来描述这种层次关系。
  • flat_seq类可以看作是一个更长的数据包,数据包的具体内容、长度、地址等信息都包含在flat_seq中。在生成item过程中,通过将自身随机变量作为constraint内容来限定item随机变量,这是flat sequence的大致处理方法
  • 上面例码没有给出例如`uvm_do/`uvm_do_with/`uvm_ create等宏是为了首先认清sequence与item之间的关系。因此该例也只给出在flat_ seq::body()任务中创建和随机化item,而省略了发送item。
  • 实际上bus_trans理应容纳更多的时序内容,而不应该只作为一次数据传输。作为数据传送的最小粒度,用户们有权将它们扩展到更大的数据和时间范围,从而间接减小数据通信和处理的成本,提高整体运行效率。
class bus_trans extends uvm_sequence_item;
    rand bit write;
    rand int data[];  //改transaction包含多个数据,颗粒度更大
    rand int length;
    rand int addr;
    rand int delay;
    static int id_num;
    constranint cstr {
        data.size() == length;
        foreach(data[i]) soft data[i] = i;
        soft addr == 'h100;
        soft write == 1;
        delat inside {[1:5]};
    };
    `uvm_object_utils_begin(bus_trans)
        `uvm_field_...
    `uvm_object_utils_end
    ...
endclass
class flat_seq extends uvm_sequenc;
    rand int length;
    rand int addr;
    `uvm_object_utils(flat_seq)
    ...
    task body();
        bus_trans tmp;
        tmp=new();
        tmp.randomize() with {length == local::length; 
                              addr == locl::addr};
        tmp.prnt();
    endtask

clas test1 extends uvm_test;    
    `uvm_component_utils(test1)
    ...
    task run_phase(uvm_phase phase);
        flat_seq seq;
        phase.raise_objection(phase);
        seq=new();
        seq.randomize() with {addr == 'h200;length == 3;};
        seq.body();
        phase.drop_objection();
    endtask
endclass
  • 我们可以将一段完整发生在数据传输中的、更长的数据都“收编”在一个bus_ _trans类中,提高这个item粒度的抽象层次,让它变得更有“气质”。而一旦拥有了更成熟的、更合适切割的item,上层的flat sequence在使用过程中就会更顺手一些了。
  • flat_ seq类不再操本不属于自己的闲心去考虑数据内容,而只应该考虑这个数据包的长度、地址等信息,因为扩充随机数据的责任一般由item负责就足够了,使用flat_ seq的用户无需考虑多余的数据约束。

Hierarchical Sequence介绍

  • Hierarchical sequence区别于flat sequence的地方在于,它可以使用其他sequence,当然还有item,这么做是为了创建更丰富的激励场景。
  • 通过层次嵌套关系,可以让hierarchical sequence使用其它hierarchical sequence、flat sequence和sequence item,这也就意味着,如果底层的sequence item和flat sequence的粒度得当,那么用户就可以充分复用这些sequence/item来构成形式更加多样的hierarchical sequence。
  • 接下来就之前定义的bus_ _trans和flat_ seq给出一个简单的hier_seq类,帮助理解这些sequence/item类之间的联系。
class hier_seq extends uvm_sequence;
    `uvm_object_utils(hier_seq)
    function new(string name = "hier_seq");
        super.new();
    endfunction
    task body();
        bus_trans t1,t2;
        flat_seq s1,s2;
        `uvm_do_wiht(t1,{length == 2;}) //该宏进行三个步骤:先创建(),之后随机化,最后传送
        fork
          `uvm_do_with(s1,{length == 5;})
          `uvm_do_with(s2,{length == 8;})
        join
        `uvm_do_with(t2,{length == 3;})
    endtask
endclass

从hier_seq::body()来看 ,它包含有bus_trans t1, t2和flat_seq s1,s2,而它的层次关系就体现在了对于各个sequence/item的协调上面。例码中使用了`uvm_do_with宏, 这个宏完成了三个
步骤:

  • sequence或者item的创建
  • sequence或者item的随机化
  • sequence或者item的传送

区别于之前的例码,这个例码通过`uvm_do_with宏帮助理解,所谓的sequence复用就是通过高层的sequence来嵌套底层的sequence/item,最后来创建期望的场景。在示例中既有串行的激励关系,也有并行的激励关系,而在更复杂的场景中,用户还可以考虑加入事件同步,或者一定的延迟关系来构成sequence/item之间的时序关系

Sequencer和Driver

sequence、item、driver_第3张图片

driver同sequencer之间的TLM通信采取了get模式,(这里为双向端口,看可以get,也可以put response)即由driver发起请求,从sequencer-端获得item,再由sequencer将其传递至driver。作为driver,它往往是一个“永动机”,胃口很大的家伙,永远停不下来,只要它可以从sequencer获取item,它就穿着红舞鞋一直跳下去。sequencer和item只应该在合适的时间点产生需要的数据,而至于怎么处理数据,则会由driver来实现。

端口和方法

为了便于item传输,UVM专门定义了匹配的TLM端口供sequencer和driver使用: 
●uvm_seq_item_pull_port #(type REQ=int, type RSP=REQ)
●uvm_seq_item_pull_ export #(type REQ=int, type RSP=REQ)
●uvm_seq_item_pull_imp #(type REQ=int, type RSP=REQ, type imp=int)
由于driver是请求发起端,所以在driver一侧例化了下面两种端口:
uvm_seq_item_pull_port #(REP, RSP) seq_item_ port
●uvm_analysis_port #(RSP) rsp_port  (广播模式的端口,如monitor。)
而sequencer一侧则为请求的响应端,在sequencer一侧例化了对应的两种端口:
uvm_seq_item_pull_imp #(REQ, RSP, this_type) seq_item_export
●uvm_analysis_export #(RSP) rsp_export  (这里为中间阶段的export是因为sequencer里面有tlm fifo,tlm fifo里面有import端口。sequencer存放的就是response的fifo。注意在sequencer中没有存放request的fifo,request是一个一个拉起后直接到driver)
通常情况下,用户可以通过匹配的第一对TLM端口完成item的完整传送,即drier::seq_item_port和sequencer::seq_item_export。这一对端口在连接时同其它端口连接方式一样,即通过driver::seq_item_port.connect(sequencer::seq_item_export)完成。
这一类端口功能主要用来实现driver与sequencer的request获取和response返回。

这一种类型的TLM端口支持如下方法:
●task get_next_ item(output REQ req_arg): 采取blocking(一直等待,如果get不到)的方式等待从sequence获取下一个item。
●task try_next_ item(output REQ req_arg): 采取nonblocking(立即返回)的方式从sequencer获取item,如果立即返回的结果req_arg为null, 则表示sequence还没有准备好。
●function void item_ done(input RSP rsp_arg=null): 用来通知sequence当前的sequence item已经消化完毕,可以选择性地传递RSP参数,返回状态值。(driver写入的话,resp会存储到sequencer里面的buffer中,此时sequence会有对应的get操作)
●task wait_for_sequences(): 等待当前的sequence直到产生下一个有效的item。(与try_next_item配合使用)
●function bit has_do_available(): 如果当前的sequence准备好而且可以获取下一个有效的item,则返回1,否则返回0。
●function void put_response(input RSP rsp_arg): 采取nonblocking方式发送response,如果成功返回1,否则返回0。(可以独立发送)
●task get(output REQ req_arg): 采用get方式获取item。
●task peek(output REQ req_arg): 采用peek方式获取item。
●task put(input RSP rsp_arg): 采取blocking方式将response发送回sequence。

读者在这里需要了解关于REQ和RSP类型的一致性,由于uvm_sequencer与uvm_ driver实际上都是参数化的类:(item的数据类型,默认父类句柄。设定传输的类型,即使传递自定义的类型,如果传输管道用缺省的父类类型,相当于子类类型先隐性转换为父类类型。发送到driver时相当于父类句柄指向子类对象,需要进行类型转换)
●uvm_sequencer #(type REQ=uvm_sequence_item, RSP=REQ)
●uvm_driver #(type REQ=uvm_ sequence_item,RSP=REQ)
●用户在自定义sequencer或者driver的时候,它们可以使用缺省类型type REQ=uvm_ sequence_ _item, 以及RSP与REQ类型保持一致。
这有一个潜在的类型转换要求,即driver得到REQ对象在进行下一步处理时,需要进行动态的类型转换,将REQ转换为uvm_sequence_item的子类型才可以从中获取有效的成员数据。另外一种可行的方式是在自定义sequencer和driver时就标明了其传递的具体item类型,这样就不用再进行额外的类型转换了。

通常情况下RSP类型与REQ类型保持一致,这么做的好处是为了便于统一处理,方便item对象的拷贝、修改等操作。
driver消化完当前的request后,可以通过item_done(input RSP rsp_arg=null)方法来告知sequence此次传输已经结束,参数中的RSP可以选择填入,返回相应的状态值。
driver也可以通过put_response()或者put()方法来单独发送response。此外发送response还可以通过成对的uvm_driver::rsp_port和uvm_sequencer::rsp_export端口来完成,方法为uvm_driver::rsp_ port::write(RSP)。 (比如三种方法写rsp:item_done、put、另一组端口广播方法write,三种方法都是写到同一个fifo)

class bus_trans extends uvm_sequence_item;
    rand int data;
    `uvm_object_utils_begin(bus_trans)
      `uvm_field_int(data,UVM_ALL_ON)
    `uvm_object_utils_end
    ...
endclass

class flat_seq extends uvm_sequence;
    `uvm_object_utils(flat_seq)
    ...
    task body();
        uvm_sequence_item tmp;
        bus_trans req,rsp;
//这里的创建利用工厂,item挂载到具体的sequencer上。(sequence和item都要挂载)。m_sequencer是
//uvm_sequence的成员变量。m_sequencer具体的值就是挂载的sequencer具体实例名称。
//使用new,create进行创建,没有指定sequencer一样可以,接下来的start_item\finish_item时候会处理
//如果item没有指定sequencer,默认指定当前所在sequence挂载的sequencer。
        tmp=create_item(bus_trans::get_type(),m_sequencer,"req"); 
        void'($cast(req,tmp)); //因为上面返回的是父类句柄,因为randomize时要访问子类的成员变量
        start_item(req);  //在sequencer侧敲门,仲裁也没有发生在这里。会立即返回。在等待,后面
 //进行随机化
        req.randomize with {data==10;}; //父类句柄随机化,tmp.randomize()可以随机化到子类的 //成员变量,因为randomize是虚方法。 但是重点是访问子类的成员变量,做限制。所以没有用这种方法。
        `uvm_info("SEQ",$formats("sent a item \n %s",req.sprint()),UVM_LOW)
        finish_item(req);  //等待仲裁  等到driver给一个item done,这个才算结束
        get_response(tmp); //这里是父类句柄,因为函数原型不是子类句柄,是一个固定的类型
//即使不发送rsp,有item done,也是一个完整的握手过程,如果发送rsp,sequence需要记得get rsp。可 //选,但一 定要成对
        void'($cast(rsp,tmp));
        `uvm_info("SEQ",$sformatf("got a item \n %s",req.sprint()),UVM_LOW)
    endtask
endclass
class sequencer extends uvm_sequencer;
    `uvm_component_utils(sequencer)
    ...
endclass

class driver extends uvm_driver;
    `uvm_component_utils(driver)
    ...
    task run_phase(uvm_phase phase);
        REQ tmp;
        bus_trans req,rsp;
        seq_item_port.get_next_item(tmp);  //父类句柄,因为REQ默认sequence item,
        void'($cast(req,tmp));
        //这里一般要driver transfer到interface上
        `uvm_info("DRV",$sformatf("got a item \n %s",req.sprint()),UVM_LOW)
        void'($cast(rsp,req.clone()));  //克隆后,虽然克隆子类对象,但会返回一个object(父类 
                                        // 句柄),因为要访问子类句柄成员,需要转化为子类句柄
//访问子类句柄的成员,将req的id交给rsp。   比如不同sequence发送不同item,经过sequencer后到达 
//driver。克隆item,作为rsp返回到sequencer的fifo中,但不知道如何返回到对应sequence。每一个item
//有对应id,通过id可以返回到对应sequence。发送sequence时就有id,之所以克隆的时候没有seq_id,是 //因为该变量没有作域的自动化。克隆时只关心数据的部分,而id不是。如果不进行下面的转化,默认id为0,
//从而会导致无法返回到对应的sequence。
        rsp.set_sequence_id(req.get_sequence_id()); 
        rsp.data+=100;
        seq_item_port.item_done(rsp);  //告诉sequence的finish item。
        `uvm_info("DRC",$sformatf("sent a itme \n %s",rsp.sprint()),UVM_LOW)
    endtask
endclass
        
class env extends uvm_env;
    sequencer sqr;
    driver drv;
    `uvm_component_utils(env)
    ...
    function void build_phase(uvm_phase phase);
        sqr=sequencer::type_id::create("sqr",this);
        drv=driver::type_id::create("drv",this);
    endfunction
    function void connect_phase(uvm_phae phase);
        drv.seq_item_port.connect(sqr.seq_item_done);  //第一对端口连接
    endfunction
endclass
class test1 extends uvm_test;   
    env e;
    `uvm_component_utils(test1)
    ...
    function void build_phase(uvm_phase phase);
        e=env::type_id::create("e",this);
    endfunction
    function run_phase(uvm_phase phase);
        flat_seq seq;
        phase.raise_objection(phase);
        seq=new();
        seq.start(e.sqr);//挂载到sqr,seq就知道m_sequencer;body就会自动执行。seq本身没有随机化
        phase.drop_objection(phase);
    endfunction
endclass

事务传输过程分析

在定义fequencer时,默认了REQ类型为uvm_sequence_ item类型,这与稍后定义driver时采取默认REQ类型保持一致。
flat_ seq作为动态创建的数据生成载体,它的主任务flat_seq::body()做 了如下的几件事情:

  • 通过方法create_item()创建request item对象。
  • 调用start_item()准备发送item(仲裁的条件是driver要作get next item;sequence调用start_item)
  • 在完成发送item之前对item进行随机处理。
  • 调用finish_ item()完成item发送。(start_item立即返回,finish_item是阻塞的)
  • 有必要的情况下可以从driver那里获取response item。

在定义driver时,它的主任务driver::run_phase()也应通常做出如下处理:

  • 通过seq_item_port.get_next_item(REQ)从sequencer获取有效的request item。
  • 从request item中获取数据,进而产生数据激励。
  • 对request item进行克隆生成新的对象response item。
  • 修改response item中的数据成员,最终通过seq_ item_ port.item_ done(RSP)将response item对象返回给sequence。

对于uvm_ sequence::get_ _response(RSP)和uvm_ driver::item_ done(RSP)这种成对的操作,是可选的而不是必须的,即用户可以选择uvm_driver不返回response item,同时sequence也无需获取response item。

在高层环境中,应该在connect phase中完成driver到sequencer的TLM端口连接,比如例码在env::connect_phase()中通过drv.seq_ item_port.connect(sqr.seq_item_export)完成了driver与sequencer之间的连接。
在完成了flat_seq、sequencer、driver和env的定义之后,到了test1层,除了需要考虑挂起objection防止提前退出,便可以利用uvm_sequence类的方法uvm_sequence::start(SEQUENCER)来实现sequence到sequencer的挂载。

通信时序

  • 无论是sequence还是driver ,它们通话的对象都是sequencer。当多个sequence试图要挂载到同一个sequencer_上时,涉及sequencer的仲裁功能。
  • 重点分析sequencer作为sequence与driver之间握手的桥梁,是如何扮演好这一角色的。
  • 我们将抽取去这三个类的主要方法,利用时间箭头演示出完整的TLM通信过程。

(arbitration的触发依靠driver的get_next_item)

sequence、item、driver_第4张图片

  • 对于sequence而言,无论是flat sequence还是hierarchical sequence,进一步切分的话,流向sequencer的都是sequence item,所以就每个item的“成长周期”来看,它起始于create_ item(), 继而通过start_ item()尝试从sequencer获取可以通过的权限。
  • 对于sequencer的仲裁机制和使用方法我们暂且略过,而driver一侧将一直处于“吃不饱”的状态,如果它没有了item可以使用,将调用get_ next_item()来尝 试从sequencer一侧获取item。
  • 在sequencer将通过权限交给某一个底层的sequence前,目标sequence中的item应该完成随机化,继而在获取sequencer的通过权限后,执行finish_item()。 
  • 接下来sequence中的item将穿过sequencer到达driver一侧,这个重要节点标志着sequencer第一次充当通信桥梁的角色已经完成。
  • driver在得到新的item之后,会提取有效的数据信息,将其驱动到与DUT连接的接口上面。
  • 在完成驱动后,driver应当通过item_done()来告知sequence已经完成数据传送,而sequence在获取该消息后,则表示driver与sequence双方完成了这一次item的握手传输。
  • 在这次传递中,driver可以选择将RSP作为状态返回值传递给sequence,而sequence也可以选择调用get_response(RSP)等待从driver一侧获取返回的数据对象。

握手建议

  • 在多个sequence同时向sequencer发送item时,就需要有ID信息表明。该item从哪个sequence来,ID信息在sequence创建item时就赋值了
  • 在到达driver以后,这个ID也可以用来跟踪它的sequence信息,使得运输和使用更加安全,sequencer可以根据ID信息来分发这些response item返回至正确的sequence源头。
  • 建议用户在driver中,通过clone()方式单独创建response item,保证request item和response item两个对象的独立性。也许有的用户为了“简便” ,在使用了request item之后,就直接修改它的数据并作为要返回给sequence的response item。这么做看来似乎节能环保,但实际上殊不知可能埋下隐患,一方面它延长了本来应该丢进垃圾桶的request item寿命,同时也无法再对request item原始生成数据做出有效记录。
  • 为了统一起见,用户可以不在定义sequence或者driver时指定sequence item类型,使用默认类型REQ = uvm_sequence_item, 但是用户需要注意在driver一侧的类型转换,例如对get_next_item(REQ)的返回值REQ句柄做出动态类型转换,待得到正确类型之后再进行接下来的操作。
  • 有的时候如果要复用一些验证IP,用户需要修改原有的底层sequence item。从处于验证复用的角度,我们建议通过继承于原有sequence item的方式定义新的item子类,同时在顶层通过factory override的方式用新的item类型替换原有的item类型。

你可能感兴趣的:(uvm,uvm)