UVM基础-Sequence、Sequencer(一)

目录

Sequence、Sequencer、Driver大局观

Sequence和item

item与sequence的关系

flat sequence

hierarchical sequence

sequence与driver的关系

事务传输实例

实例

flat_seq

事务传输过程分析


        对于IC验证的新晋工程师进入到一个公司,大概率不会让你上来就写验证环境,以及验证环境的修改。但是会让你一上来就写一些sequence,以及一些test。因此,sequence这一部分是很重要的。

Sequence、Sequencer、Driver大局观

整个序列组件之间的数据传输可以如下描述:

        ① 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互动的状态(如果有需要的话)。


        sequence item是driver与DUT每一次互动的最小粒度内容。例如DUT如果是一个slave端,driver扮演一个master去访问DUT的寄存器,那么sequence item需要定义的数据信息至少包括访问地址、命令码、数据和状态值,这样的信息在driver取得后,会通过时序方式在interface一侧发起激励送至DUT。        

        用户除了可以在声明sequence item时添加必要的成员变量,也可以添加对这些成员变量进行操作的成员方法。这些添加了的成员变量,需要充分考虑在通过sequence传递到driver前是否需要随机化。

        对于一个sequence而言,它会产生多个sequence item,也可以产生多个sequence。从产生层次上来看,sequence item是最小粒度,它可以由sequence生成,而相关sequence也可以进一步组织继而实现层次化,最终由更上层的sequence进行调度。

        sequence与driver之间起到桥梁作用的是sequence。由于sequence和driver均是component组件,它们之间的通信也是通过TLM端口实现的。TLM端口是实现组件和组件之间的通信,driver和sequencer之间的TLM通信参数是sequence item类。由于这一限制,使得sequencer到driver的传输数据类型不能改变,同时与sequencer连接的sequence创建的sequence item类型也应该为指定类型。 也就是说,sequencer从sequence拿到的item的类型,和sequencer发送给drver,以及driver接收到的数据类型,必须是严格一致的。

        driver不应该轻易修改item中的值,它会把item中的数据按照与DUT的物理协议时序关系驱动到接口上面。

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

        由于无法判定环境在run阶段什么时间点会创建sequence和将其产生的sequence item 挂载(attach)到sequencer上面,所以无法通过UVM环境结构或者phase机制来识别sequence的运行阶段。也正是因为uvm_object是独立于build阶段之外的,所以用户可以有选择地动态地在合适时间点挂载所需要的sequence和item

        uvm_sequence和uvm_sequence_item不是组件,所以无法通过config_db按照层次关系对其进行配置。因此要用一个trick:sequence一旦活动起来,它必须挂载到一个sequencer上(发送item),也就是sequence能够获取sequencer的句柄,通过句柄来访问sequencer中的成员变量或等信息,那么这样sequence可以依赖于sequencer的结构关系,间接通过sequencer来获取顶层的配置和更多信息。

        明确划分模块职责的话,sequence应该只负责生成item的内容, 而不应该控制item的时序,而驱动激励时序的任务应当由driver完成。

sequencer之所以作为一个组件,设立在sequence和driver之间,主要有两个原因:

        ① sequencer作为一个组件,它可以通过TLM端口与driver传送driver对象。

        ② sequencer在面向多个并行sequence时,它有充分的仲裁机制来合理分配和传送item,继而实现并行item数据传送至driver的测试场景。

数据传送机制:(数据传输,get还是put)

        数据传送采用的是get模式不是put模式。如果是put模式,那么应该是sequence将数据put至driver,而如果是get模式,那么应该是driver从sequencer获取item。

选择get模式的原因:

        ① 如果是get模式,当item从sequence产生,穿过sequencer到达driver时,我们就可以结束该传输。如果是put模式,则必须是sequencer将item传送至driver,同时必须收到返回值才可以发起下一次传输,从效率上看,两者具有差别。

        ② 如果需要让sequencer具有仲裁特性,可以使得多个sequence同时挂载到sequencer上面,那么get模式更符合设计。这是因为driver作为initiator,一旦发出get请求,它会先通过sequencer,然后获得仲裁后的item


Sequence和item

        sequence指的是uvm_sequence类,而item指的是uvm_sequence_item类,简称为sequence和item。item是基于uvm_object类,这表明了它具备UVM核心基类所必要的数据操作方法,例如copy()、clone()、compare()、record()等。

item通常应该具备一下数据成员:

        ① 控制类:总线协议上的读写类型、数据长度、传送模式等。

        ② 负载类:一般指数据总线上的数据包。

        ③ 配置类:用来控制driver的驱动行为,例如命令driver的发送间隔或者有无错误插入。

        ④ 调试类:用来标记一些额外信息方便调试,例如对象的实例序号,创建时间等。

item使用注意事项:

        ※ 如果数据域属于需要用来做驱动,那么用户应考虑定义为rand类型,同时按照驱动协议给出合适的constraint。

        ※ 由于item本身的数据属性,为了充分利用UVM域声明的特性,建议将必要的数据成员都通过`uvm_field_automation机制来声明,以便后续uvm_object基本数据方法的自动实现。

        ※ UVM要求item的创建和随机化都应该发生在sequence的body()任务中,而不是在sequencer和driver中。

        ※ 按照item的周期来说,它应该始于sequence的body()方法,而后经过随机化,穿越sequencer到达driver,直到被driver吸收,到此就结束了。如果要对item进行修改数据,不应当直接进行修改,这会无形的增加item的寿命,正确做法是利用copy或者clone函数来复制一份再做处理。

item与sequence的关系

        一个sequence可以包含一些有序组织起来的item实例,考虑到item在创建后需要被随机化,sequence在声明时也需要预留一些可供外部随机化的变量。

sequence可以被区分为常见的三类:

        扁平类(flat sequence):这一类往往只用来组织更小的粒度,即item实例构成的组织。

        层次类(hierarchical sequence):这一类往往是由更高层的sequence用来组织底层的sequence,进而让这些sequence或者按照顺序方式,或者按照并行方式,挂载到同一个sequencer上。

        虚拟类(virtual sequence):这个类是最重要的,它是最终控制整个测试场景的方式,由于整个环境中往往存在不用种类的sequencer和其对应的sequence,我们需要一个虚拟的sequence来协调顶层的测试场景。之所以称这个方式为virtual sequence,是因为该序列本身并不会固定挂载于某一种sequencer类型上,而是将其内部不同类型的sequence最终挂载到不同的目标sequencer上面。

flat sequence

一般对于flat sequence而言,它包含的信息有:

        ※ sequence item以及相关的constraint用来关联生成的item之间的关系,从而完善出一个flat sequence的时序形态。

        ※ 除了限制sequence item的内容,各个item之间的时序信息也需要由flat sequence给定,例如何时生成下一个item并且发送至driver。

        ※ 对于需要与driver握手的情况(读写操作),或者等待monitor事件从而做出反应。都需要相应具体事件,从而创建对应的item并且发送出去。

class flat_seq extends uvm_sequence;
  rand int length;  //随机变量
  rand int addr;
  rand int data[];
  rand bit write;
  rand int delay;  
  constraint cstr{  //随机变量constraint
    data.size() == length;
  foreach(data[i]) soft data[i] == i;
  soft addr == 'h100;
  soft write == 1;
  delay inside {[1:5]};
  };
  `uvm_object_utils(flat_seq) //factory注册
  ...                         //省略的new函数
  task body();
    bus_trans tmp;
    foreach(data[i]) begin
      tmp = new();
      tmp.randomize() with {
        data == local::data[i];
        addr == local::addr + i<<2;
        write == local::write;
        delay == local::delay;
      };
      tmp.print();
    end
  endtask
endclass

class bus_trans extends uvm_sequence_item;
  rand bit write;
  rand int 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

        在uvm_sequence中写的task body,当sequence挂载到sequencer之后,body任务会自动运行,就像组件里面的run一样。

        上面的代码,是在flat_sequence中,不断去new一个bus_trans,然后给他赋值data、addr、write和delay,事实上这样做的话,flat_sequence不仅要考虑数据包的长度和地址,还要考虑数据包的内容,要做的事情太多了。 

        我们可以将一段完整发生在数据传输中的、更长的数据都“收编”在一个bus_trans类中(item 中),提高这个item粒度的抽象层次,一旦有了更成熟的、更合适切割的item,上层的flat sequence在使用过程中也更加方便。

hierarchical sequence

        Hierarchical sequence区别于flat sequence的地方在于,它可以使用其他sequence,还有item,这么做是为了创建更丰富的激励场景。

        通过层次嵌套关系,可以让hierarchical sequence使用其他hierarchical sequence、flat sequence和sequence item,如果底层的sequence和item粒度合适,那么就可以充分复用他们,来实现更为丰富的激励场景。

class hier_seq extends uvm_sequence;
  `uvm_object_utils(hier_seq)
  function new(string name = "hier_seq");
    super.new(name);
  endfunction
  task body();
    bus_trans t1,t2;
    flat_seq s1,s2;
    `uvm_do_with(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

        这里用了uvm_do_with宏,这个宏定义出来,主要做了三件事:① 创建sequence或者item;② 对里面的成员变量进行随机化; ③ 传送数据到sequencer上。

sequence与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   // 专门广播response的,一对多端口

而sequencer一侧则为请求的响应端,在sequencer一侧例化了对应的两种端口:

        ※ uvm_seq_item_pull_imp #(REQ, RSP, this_type) seq_item_export

        ※ uvm_analysis_export #(RSP) rsp_export

        对于第三种sequence部分的端口需要详解一下,虽然sequence_item_export叫做export,但是它是被定义为imp,也就是TLM传输的终点。而sequence作为TLM通信数据传输的终点,为什么要给uvm_analysis端口定义为export呢?因为在我们的理解中,export是TLM通信数据传输的中间节点而不是终点。这是因为uvm_analysis端口内置了一个存储RSP的FIFO而FIFO上是有imp端口的,因此uvm_analysis端口的export接到内置的FIFO的imp端口上就形成了终点。这也是为什么我们成sequencer就是TLM通信传输的终点的原因。

        显然,从上面我们可以看到,sequence和driver的各自的两个端口其实是和对方的端口成对的。uvm_seq_item_pull_port #(REP, RSP ) seq_item_port 和 uvm_seq_item_pull_imp #(REQ, RSP, this_type) seq_item_export成对;uvm_analysis_port #(RSP) rsp_port 和 uvm_analysis_export #(RSP) rsp_export成对。因此需要在connect_phase中将端口进行连接。

        通常情况下,只需要连接 driver::seq_item_portsequence::seq_item_export 就行了。即在connect_phase中通过:driver::seq_item_port.connect(sequencer::seq_item_export) 完成。这一对端口功能主要用来实现driver与sequence的request获取和response返回。

seq_item_port可以调用很多方法:

        ※ task get_next_item(output REQ req_arg): 采取blocking的方式等待从sequenne获取下一个item。

        ※ task try_next_item(output REQ reg_arg): 采取nonblocking的方式从sequence获取item,如果立即返回的结果req_arg为null,则表示sequence还没有准备好。

        ※ function void item_done(input RSP rsp_arg=null): 用来通知sequence当前的sequence item已经消化完毕,可以选择性的传递RSP参数,返回状态值。

        ※ task wait_for_sequence(): 等待当前的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。

        平时用的比较多的任务和方法,就是上面加粗的任务和方法。

        driver消化完当前的request后,可以通过item_done(input RSP rsp_arg=null)方法来告知sequence 此次传输已经结束,参数中的RSP可以选择填入,返回相应的状态值。

事务传输实例

实例


//bus transaction item definition
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();
    //创建了一个sequence item tmp
    uvm_sequence_item tmp;
    bus_trans req,rsp;
    //调用函数 create_item, 接下来生成的item, 挂载到m_sequence上
    tmp = create_item(bus_trans::get_type(), m_sequencer,"req");
    //create_item返回的是一个父类的句柄,需要通过cast转换成子类
    void'($cast(req, tmp));
    //若在创建的时候没有上面声明item挂载的sequence,则在start_item阶段,默认挂载至sequence所在的sequencer下
    start_item(req);
    //倘若没有做子类父类的转化,那么不能访问子类对象且不说,在父类进行randomize的时候,由于randomize是虚方法,父类调用该方法同时也会对子类的成员变量进行随机化。
    req.randomize with {data == 10;};
    `uvm_info("SEQ", $sformat("sent a item \n %s", req.sprint()), UVM_LOW)
    finish_item(req);
    //ret_response方法原型中,get的变量是父类句柄,所以为了能访问子类方法sprint(),下面还需要做父类到子类的转换
    get_response(tmp); //driver消化完item,发送一个item_done的response,若item_done(rsp)有句柄送到sequence的fifo中就可以通过get_response来获取句柄,否则不能获取(会被get_response blocking住)。
    void'($cast(rsp, tmp));
    `uvm_info("SEQ", $sformat("got a item \n %s", rsp.sprint()), UVM_LOW)
  endtask
endclass

class sequencer extends uvm_sequence;
  `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);
    //拿到父类的句柄,需要转化成子类的对象
    void'($cast(req, tmp));
    `uvm_info("DRV", $sformatf("got a item \n %s", req.sprint()), UVM_LOW)
    void'(cast(rsp, req.clone()));//子类方法的clone,克隆完返回的还是父类的句柄,需要转化成子类
    rsp.set_sequence_id(req.get_sequence_id());
    rsp.data += 100;
    seq_item_port.item_done(rsp);
    `uvm_info("DRV", $sformatf("sent a item \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);
    //在build_phase创建
    sqr = sequencer::type_id::create("sqr", this);
    drv = driver::type_id::create("drv", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    //在connect_phase连接,连接第一对端口,第二对端口可以不连
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

class test1 extends uvm_test;
  env e;
  `uvm_component_utils(test1)
  ...
  function void build_phase(uvm_phase phase);
    //例化env
    e = env::type_id::create("e", this); 
  endfunction 
  task run_phase(uvm_phase phase);
    flat_seq seq;
    phase.raise_objection(phase);
    seq = new();  //创建一个sequence
    seq.start(e.sqr); //将sequence挂载到env中的sequencer上
    //sequence挂载到sequencer上之后,就会自动运行其中的body任务了
    phase.drop_objection(phase);
  endtask
endclass

        在sequence发送item的时候,就会给item记录一个sequence_id,表示该item是由哪个sequence发送的,这样在一个sequencer同时收到两个不同的sequence发送的item,然后发送给driver,driver再返回这两个item的response给sequencer,让sequencer把对应的response送给对应的sequence,就需要利用这个sequence_id。rsp.set_sequence_id(req.get_sequence_id())这个就是通过获取request的id,给response,来保证发送对应request的sequence能得到对应自己的response。

        sequence_id不做域的自动化的话,在使用clone函数的时候是不会进行clone的,默认为0。

注意一下,在声明drvier和sequence时,没有给他们下类型的定义,所以默认是sequence_item类型属于父类的句柄,因此在get_next_item之后,需要将父类的句柄转换成子类的句柄,这步可以通过在声明class时,定义类型#(RSP)来改变。但是req.clone()获得的句柄默认是uvm_object类型,因此必须通过父类到子类的转换。

flat_seq

class flat_seq extends uvm_sequence;
  `uvm_object_utils(flat_seq)
  ...
  task body();
    //创建了一个sequence item tmp
    uvm_sequence_item tmp;
    bus_trans req,rsp;
    //调用函数 create_item, 接下来生成的item, 挂载到m_sequence上
    tmp = create_item(bus_trans::get_type(), m_sequencer,"req");
    //create_item返回的是一个父类的句柄,需要通过cast转换成子类
    void'($cast(req, tmp));
    //若在创建的时候没有上面声明item挂载的sequence,则在start_item阶段,默认挂载至sequence所在的sequencer下
    start_item(req);
    //倘若没有做子类父类的转化,那么不能访问子类对象且不说,在父类进行randomize的时候,由于randomize是虚方法,父类调用该方法同时也会对子类的成员变量进行随机化。
    req.randomize with {data == 10;};
    `uvm_info("SEQ", $sformat("sent a item \n %s", req.sprint()), UVM_LOW)
    finish_item(req);
    //ret_response方法原型中,get的变量是父类句柄,所以为了能访问子类方法sprint(),下面还需要做父类到子类的转换
    get_response(tmp); //driver消化完item,发送一个item_done的response,若item_done(rsp)有句柄送到sequence的fifo中就可以通过get_response来获取句柄,否则不能获取(会被get_response blocking住)。
    void'($cast(rsp, tmp));
    `uvm_info("SEQ", $sformat("got a item \n %s", rsp.sprint()), UVM_LOW)
  endtask
endclass

flat_seq作为动态创建的数据生成载体,它的主任务flat_seq::body()做了如下的几件事情:

        ※ 通过方法create_item()创建了request item对象;

        ※ 调用start_item()准备发送item;

        ※ 在完成发送item之前对item进行随机处理;

        ※ 调用finish_item()完成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)这种成对的操作,是可选的,可以选择获取或者不获取,但是要成对出现。

        在高层环境中,应该在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防止仿真在run_phase的时候提前退出。

        使用uvm_sequence::start(SEQUENCER)来完成sequence到sequencer的挂载操作。当多个sequence试图挂载到同一个sequencer时,需要在sequencer上添加仲裁功能


        在sequence创建item之前,首先需要将sequence挂载到sequencer上,上面整个sequence从create_item到最后和driver握手成功的get_response的运作都在sequence的body()中。Driver的get_next_item到item_done都在driver的run_phase()中。

        sequencer做仲裁,是在driver发起get_next_item()时,才开始仲裁选择哪个item。如果只有一个sequence挂载到sequencer上,那直接用就行了。在sequencer将通过的权限交给某一个底层的sequence之前,目标sequence中的item应该完成随机化,继而在获取sequencer的通过权限后,执行finish_item()。

        finish_item()会等到driver发挥item_done()才会结束。

对每个item而言,它起始于create_item(),继而通过start_item()尝试从sequencer获取可以通过的权限。如果driver没有了item可用,将调用get_next_item()来尝试从sequencer一侧获取item。

        为了统一起见,用户可以不在定义sequencer或者driver时指定sequence item类型, 使用默认的REQ = uvm_sequence_item,但是用户需要注意在driver一侧的类型转换,例如对get_next_item(REQ)的返回值REQ句柄做出动态类型转换,等到正确类型之后再进行接下来的操作。

  • 7
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不吃葱的酸菜鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值