芯片漫游指南(4) -- UVM序列
1 新手上路
1.1 概述
-
在UVM世界,利用其核心特性,在创建了组件和顶层环境,并且完成组件之间的TLM端口连接之后,接下来就可以使得整个环境开始运转了。
-
在经过一番时间,掌握了组件之间的TLM通信方式,开辟了建筑之间的道路,桥梁和河道以后,就可以进入紧张繁忙的物流期了。
-
运转的必要条件是组件之间需要有事务(transaction)传送,这就同管道连接好需要水流一样。
-
如果城市没有交通,那么显然不会有多热闹。
-
在本章我们将主要围绕下面几个核心词来阐述它们的作用、分类以及之间的互动关系:
- sequence item
- sequence
- sequencer
- driver
-
如果按照交通道路的车流来打比方,sequence就是道路,sequence item是道路上行驶的货车,sequencer是目的地的关卡,而driver便是最终卸货的地方。
-
从软件层面来讲,这里的货车是从sequence一端出发的,经过了sequencer,最终抵达driver。
-
经过driver的卸货,每一辆货车也就完成了它的使命。
-
driver对每一件到站的货物,经过扫描处理,将它们分解为更小的信息量,提供给DUT。
1.2 序列组件的互动
- 在这个过程中,不同 的角色之间会进行下面的互动:
- sequence对象会产生目标数量的sequence item对象。借助于SV的随机化和sequence item对随机化的支持,使得产生的每个sequence item对象中的数据内容都不相同。
- 产生的sequence item会经过sequencer再流向driver。
- dirver陆续得到每一个sequence item,经过数据解析,将数据按照与DUT的物理接口协议写入到接口上,对DUT形成有效激励。
- 当有必要是,driver在每解析并消化完一个sequence item后,它会将最后的状态信息写会sequence item对象再返回给sequencer,最终抵达sequence对象一侧。
- 这么做目的在于,有时sequence需要得到driver于DUT互动的状态,这就需要driver有一个回路将更新过的sequence item对象写会至seqence一侧。
- sequence item是driver与DUT没一次互动的最小颗粒度内容。
- 例如DUT如果是一个slave端,driver扮演master去访问DUT的寄存器,那么sequence item需要定义的数据信息至少包括访问地址、命令码、数值和状态值,这样的信息在driver取得后,会通过时序方式在interface一侧发起激励发送至DUT。
- 按照一般总线做寄存器访问的习惯,这种访问的时序上大致会保持几个时钟周期,直至数据传送完毕,而由driver再准备发起下一次操作。
- 用户除了可以在声明sequence item时添加必要的成员变量,也可以添加对这些成员变量进行操作的成员方法。这些添加了的成员变量,需要冲锋考虑在通过sequencer传递到driver前是否需要随机化。
- 激励驱动链的最后一道关卡是driver。
- 对于常见的用法,driver往往是一个sequence item消化完,报告给sequencer和sequence,同时再请求消化下一个sequence item。
- driver看起来永远喂不饱,同时还有对事物很挑剔。在消化每一个sequence item之前,该item种的数据是已经随机化好的,所以每个item内容一般都是各不相同的。
- driver自己并不会轻易修改item中的值,它会把item中的数据按照与DUT的物理协议时序关系驱动到接口上面。
- 例如对于一个标准的写操作,driver不但需要按照时序一次驱动地址总线,命令码总线和数据总线,还应该等待从的返回信号和状态值,这样才算完成一个数据写传输。
1.3 继承关系
- uvm_sequence_item和uvm_sequence都是基于uvm_object,它们不同于uvm_component只应当在build阶段作为UVM环境的“不动产”进行创建和配置,而是可以在任何阶段创建。
- 这种类的继承带来的UVM应用区别在于:
- 由于无法判定环境在run阶段什么时间点会创建sequence和将其挂载(attach)到sequencer上面,是有意无法通过UVM环境结构或者phase机制来识别sequencer和item。
- 考虑到uvm_sequence和uvm_sequence_item并不处于UVM结构当中,所以顶层在做配置时,无法按照层次关系直接配置到sequence中。
- sequence一旦活动起来,它必须挂载到一个sequencer上,这样sequence可以依赖于sequencer的结构关系,间接通过seeuencer来获取顶层的配置和更多信息。
1.4 提示
- 从常规的认知方式来看,用户可能更愿意将时序控制的权利赋予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的测试场景。
1.5 总结
- 数据传送机制采用的是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。
2 Sequence和item
2.1 概述
- 无论是自驾item,穿过sequencer交通站通往终点driver,还是坐上sequencer大巴,一路沿途观光,最终跟随导游停靠到风景点driver。
- 在接受如何驾驶item和sequence,准守什么交规,最终可以有序穿过sequencer抵达之前,有必要首先认识sequence与item之间的关系。
- 这里的sequence指的是uvm_sequence类,而item指的是uvm_sequence_item类,我们简称其为sequence和item。
- 对于激励生成的场景控制,是由sequence来编制的,而对于激励所需要的具体数据和控制要求,则是从item的成员数据得来的。
- item是基于uvm_object类,这表明了它具备UVM核心基类所必要的数据操作方法,例如copy()、clone()、compare()、record()等。
- item通常应该具备有什么类型的数据成员呢?我们将它们划分为如下基类:
- 控制类。譬如总线协议上的读写类型、数据长度、传送模式等。
- 负载类。一般指数据总线上的数据包。
- 配置类。用来控制driver的驱动行为,例如命令driver的发送间隔或者无错误插入。
- 调试类。用来标记一些额外信息方便调试,例如该对象的实例序号、创建时间,被driver解析的时间始末等。
2.2 Sequence和Item示例
class bus_trans extends uvm_sequence_item;
rand bit write;
rand int data;
rand int addr;
rand int delay;
static int id_unm;
`uvm_object_utils_begin(bus_trans)
`uvm_field_int...
`uvm_object_utils_end
...
endclass
class test1 extends uvm_test;
`uvm_component_utils(test1)
...
task run_phase(uvm_phase phase);
bus_trans t1,t2;
phase.raise t1,t2;
phase.raise_objection(phase);
#100ns
t1 = new("t1");
t1.print();
#200ns;
t2 = new("t2");
void'(t2.randomize());
t2.print();
phase.drop_objection(phase);
endtask
endclass
- item使用时的特点:
- 如果数据域属于需要用来做驱动,那么用户应考虑定义为rand类型,同时按照驱动协议给出合适的constranint。
- 由于item本身的数据属性,为了充分利用UVM域声明的特性,我们建议将必要的数据成员都通过`uvm_fielf_xxx宏来声明,以便日后uvm_object的基本数据方法自动实现,例如上面的print()函数。
- t1没有被随机化而t2被随机化了,这种差别在item通往sequence之前是很明显的。UVM要求item的创建和随机化都应该发生在sequence的body()任务中,而不是在sequencer或者driver中。
- 按照item对象的声明周期来区分,它的生命应该开始与sequence的body()方法,而后经理了随机化并穿越sequencer最终到达drivver,直到driver消化之后,它的生命一般来讲才会结束。之所有要突出这一点,是因为一些用户在使用中会不恰当地直接操作item对象,直接修改其中的数据,或者将他的句柄发送给其他组件使用,这回无形中修改item的数据基因,或者延长一个item对象的寿命。这种不合适的对象操作方式是需要注意的,可以取代的方式则是合理利用copy()和clone()等数据方法。
2.3 item与sequence的关系
- 一个sequence可以包含一些有序组织起来的item实例,考虑到item在创建后需要被随机化,sequence在声明时需要预留一些可供外部随机化的变量,这些随机变量一部分是通过层次传递约束来最终控制item对象的随机变量,一部分是用来item对象之间加以组织和时序控制的。
- 为了区分几种常见的sequence定义方式,我们介绍sequence之前首先将其分类为:
- 扁平类(flat sequence)。这一类往往只用来组织更细小的粒度,即item实例构成的组织。
- 层次类(hierarchical sequence)。这一类是由更高层的sequence用来组织底层的sequence,进而让这些sequence或者按照顺序的方式,或者按照并行方式,挂载到同一个sequencer上。
- 虚拟类(virtual sequence)。这一类则是最终控制整个测试场景的方式,鉴于整个环境中往往存在不同种类的sequencer和其他对应的sequence,我们需要一个虚拟的sequence来协调顶层的测试场景。之所以称这个方式为virtual sequence,是因为该序列本身并不会固定挂载于某一个sequencer类型上,而是将其内部不同类型sequence最终挂载到不用的目标sequencer上面,这也是virtual sequence不同于hierarchical sequence的最大一点。
2.4 Flat Sequence介绍
- 一个flat sequence往往由细小的sequence item群落构成,在此之上sequence还有更多的信息来玩呗它需要实现的激励场景。
- 一般对于flat sequence而言,它包含的信息有:
- sequence item以及相关的constraint用来关联生成的item之间的关系,从而完善出一个flat sequence的时序形态。
- 除了限制sequence item的内容,各个item之间的时序信息也需要由flat sequence给定,例如何时生成下一个item并且发送给driver。
- 对于需要与driver握手的情况(例如读操作),或者等待mointor事件从而做出反应(例如slave的memory response数据响应操作),都需要sequence在收到另外一侧组件的状态后,再决定下一步操作,即响应具体事件从而创建对应的item并且发送出去。
2.5 示例一
class flat_seq extends uvm_sequence;
rand int length;
rand int addr;
rand int data[];
rand bit write;
rand int delay;
constraint cstr{
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)
...
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
calss bus_trans extends uvm_sequence_item;
rand bit write;
rand int data;
rand int addr;
rand int delay;
static int id_num;
`uvm_oject_utils_begin(bus_trans)
`uvm_filed_int...
`uvm_object_utils_end
...
endclass
class 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 ==2;};
seq.body();
phase.drop_objection(phase);
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_transk理应容纳更多的时序内容,而不应该只作为一次数据传输。作为数据传送的最小颗粒,用户们有权将它们扩展到更大的数据和时间范围,从而间接减小数据通信的成本,提高整体运行效率。
2.6 示例2
class bus_trans extends uvm_sequence_item;
rand bit write;
rand int data[];
rand int length;
rand int addr;
rand int delay;
static int id_num;
constraint cstr{
data.size() == length;
foreach(data[i]) soft data[i] ==i;
soft addr == 'h100;
soft write == 1;
delay inside {[1:5]};}
`uvm_object_utils_begin(bus_trans)
`uvm_filed_...
`uvm_object_utils_end
...
endclass
class flat_seq extends uvm_sequence;
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 == local::addr;};
tmp.print();
endtask
endclass
class test1 extends uvm_test;
`uvm_component_utils(test)
...
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(phase);
endtask
endclass
- 我们可以将一端完整发生在数据传输中的、更长的数据都“收编”在一个bus_trans类中,提高这个item粒度的抽象层次,让它变得更有“气质”。而一旦拥有了更成熟的、更合适切割的item,上层的flat sequence在使用过程中就会更顺手一些了。
- flat_seq类不再操本不属于自己的闲心去考虑数据内容,而只应该考虑这个数据包的长度、地址等信息,因为扩充随机数据的责任一般由item负责就足够了,使用flat_seq的用户无需考虑多余的数据约束。
2.7 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 hire_seq extends uvm_sequence;
`uvm_object_utils(hier_seq)
function new(string name = "hire_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
- 从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/item,最后来创建期望的场景。
- 在示例中既有传销的激励关系,也有并行的激励关系,而在更复杂的场景中,用户可以考虑加入事件同步,或者一定的延迟关系来构成sequence/item之间的时序关系。
3 Sequencer和Driver
3.1 概述
- driver同sequencer之间的TLM通信采取了get模式,即由driver发起请求,从sequencer一端获得item,再由sequencer将其传递至driver。
- 作为driver,它往往是一个“永动机”,胃口很大的家伙,永远停不下来,只要它可以从sequencer获取item,它玖穿着红舞鞋一直跳下去。
- sequencer和item只应该在合适的时间点产生需要的数据,而至于怎么处理,则回由driver来实现。
- 为了便于item传输,UVM专门定义了匹配的TLM端口供sequencer和driver使用:
- uvm_sequ_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
- 而sequencer一侧则为请求的响应端,在sequencer一侧例化了对应的两种端口:
- uvm_seq_item_pull_imp #(REQ,RSP,this_type)seq_item_export
- uvm_analysis_export #(RSP)rsp_export
- 通常情况下,用户可以通过匹配的第一对TLM端口完成item的完整传送,即driver::seq_item_port.connect(sequencer::seq_item_export)完成。
- 这一类端口功能主要用来实现driver与sequencer的request获取和response返回。
- 这一种类型的TLM端口支持如下方法:
- task get_next_item(output REQ req_arg):采取blocking的方式等待从sequence获取下一个item。
- task try_next_item(output REQ req_arg):采取nonblocking的方式从sequencer获取item,如果立即返回的结果req_arg为null,则表示sequence还没有准备好。
- function void item_done(input REQ req_arg=null):用来通知sequence当前的sequence item已经消化完毕,可以选择性地传递RSP参数,返回状态值。
- task wait_for_sequences():等待当前的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实际上都是参数化的类:
- 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_driver::rsp_export端口来完成,方法为uvm_driver::rsp_port::write(RSP)。
3.2事务传输实例
class bus_trans extends uvm_sequence_item;
rand int data;
`uvm_objetc_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;
tmp = create_item(bus_trans::get_type(),m_sequencer,"req");
void'($cast(req,tmp));
start_item(req);
req.randmize with {data == 10;};
`uvm_info("SEQ",$sformatf("sent a item \n %s",req.sprint());UVM_LOW)
finish_item(req);
get_response(tmp);
void'($cast(rsp,tmp));
`uvm_info("SEQ",$sformatf("got a item \n %s",rsp.sprlit()),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(temp);
void'($cast(req,tmp));
`uvm_info("DRV",$sformatf("got a item \n %s",req.sprint()),UVM_LOW)
void'($cast(rsp,req.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);
sqr = sequencer::type_id::create("sqr",this);
drv = driver::type_id::type_id::create("drv",this);
endfunction
function void connect_phase(uvm_phase 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);
e = env::type_id::create("e",this);
endfunction
task run_phase(uvm_phase phase);
flat_seq seq;
phase.raise_objection(phase);
seq = new();
seq.start(e.sqr);
phase.drop_onjection(phase);
endtask
endclass
输出结果:
UVM_INFO @ 0:uvm_test_top.e.sqr@@flat_seq [SEQ] sent a item
...
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item
...
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] sent a item
...
UVM_INFO @ 0:uvm_test_top.e.sqr@@flat_seq [SEQ] got a item
...
-
展示了从item定义,到sequence定义,最后到sequencer与driver的连接,即sequencer和driver之间的item传输过程,帮助理解传输的七点、各个节点以及终点。
-
一旦理解了这中期的朴素原理,那么这两个组件之间的握手也就不再那么神秘了。
-
对于理解driver从sequencer获取item,经过时序处理再返回给sequence的握手过程很有帮助。
-
在定义sequencer时,默认了REQ类型为uvm_sequence_item类型,这与稍后定义driver采取默认REQ类型保持一致。
-
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)这种成对的操作,是可选的而不是必须的,即用户可以选择uvm_driver不返回response item,同时sequence也无需获取response item。
3.3 通信时序
- 无论是sequence还是driver,它们通话的对象都是sequencer,当多个sequence试图要挂载到同一个sequencer上时,设计sequencer的仲裁功能。
- 重点分析sequencer作为sequence与driver之间握手的桥梁,是如何扮演好这一角色的。
- 我们将抽取去这三个类的主要方法,利用时间箭头演示出完整的TLM通信过程。
- 对于sequence而言,无论是flat sequence还是hierarchical sequence,进一步切分的话,流向sequencer的都是sequece item,所以每个item的“成长周期”来看,它起始于create_item(),继而通过start_item()尝试从sequencer获取可以通过的权限。
- 对于seqencer的仲裁机制和使用方法我们暂且略过,而driver一侧将一直处于“吃不饱” 的状态,如果它没有了item可以使用,将调用get_next_item()来尝试从sequencer一侧获取item。
- 在sequencer将通过权限交给某一个底层的sequence前,目标sequence中的item应该完成随机化,继而在获取sequence的通过权限后,执行finish_item()。
- 在多个sequence同时向sequencer发送item时,就需要有ID信息表明该item从那个sequence来,ID信息在sequence创建item时就赋值了。
- 在到达driver之后,这个ID也可以用来跟踪它的sequence信息,使得运输的使用更加安全,sequencer可以根据ID信息来分发这些response item返回值正确的sequence源头。
- 建议用户driver中,通过clone()方式单独创建response item,保证response item和response item两个对象的独立性。
- 也许有的用户为了“简便”,在使用了resuest item之后,就直接修改它的数据并作为要返回给sequence的response item之后,就直接修改它的数据作为返回给sequence的response item,这么做看来似乎节能环保,但实际上殊不知可能埋下隐患,一方面它盐城了本来应该进垃圾桶的request item的寿命,同时也无法再对request item原始生成的数据做出有效记录。
- 为了统一起见,用户可以在不定义sequence或者driver时指定sequence item类型,使用默认类型REQ = uvm_sequence_item,但是用户需要主要在driver一侧的类型转换,例如对get_next_item(REQ)的返回值REQ句柄做出动态类型转换,待得到正确类型之后再进行接下来的操作。
- 有的时候如果要复用一些验证IP,用户需要修改原有的底层sequece item。从处于验证复用的角度,我们建议通过继承原有的sequence item的方式定义新的Item子类,同时在顶层通过factory override的方式用心的item类型替换原有的item类型。
4. Sequencer和Sequence
4.1概述
- 了解了sequence与driver之间传递sequence item的握手过程,同时也掌握了sequence与item之间的关系。
- 接下来需要就sequence挂载到sequencer的常用方法做出总结,大家抗原通过对这些常用方法的宏的介绍,了解到它们不同的使用场景。
- 面对多个sequence如果需要同时挂载到sequence时,那就面临着仲裁的需要,uvm_sequencer自带有仲裁特性,结合sequence的优先级设定,最终可以实现想要的效果。
sequence宏概述
- 对于UVM初学者,我们往往给出的建议是,如果可以正确区别方法start()和宏`uvm_do,那就拿下了sequence发送和嵌套的半壁江山。
- 然而考虑到读者对技艺的高要求,我们这里会系统性地阐述各种方法和宏之间的关系,以及讨论什么时候可以使用方法,什么时候可以使用宏。
- 对于已经习惯于sequence宏使用的用户而言,当它们再切回到sequence方法、或者调试这些方法时,会有一种不适感,但是如果你想要对sequence发送做出更准确的控制,我们还须正本清源,首先熟悉sequence的方法。
4.2 sequence和item发送实例
class bus_trans extends uvm_sequence_item;
rand int data;
`uvm_object_utils_begin(bus_trans)
`uvm_field_int(data,UVM_ALL_ON)
`uvm_objection_utils_end
...
endclass
class child_seq extends uvm_sequence;
`uvm_object_utils(child_seq)
...
task body();
uvm_sequence_item tmp;
bus_trans req;
tmp = create_item(bus_trans::get_type(),m_sequencer,"req");
void'($cast(req,tmp));
start_item(req);
req.randomize with {data == 10;};
finsih_item(req);
endtask
endclass
class top_seq extends uvm_sequence;
`uvm_object_utils(top_seq)
...
task body();
uvm_sequence_item tmp;
child_seq cseq;
bus_trans req;
//create child sequence anda items
cseq = child_seq::type_id::create("cseq");
tmp = create_item(bus_trans::get_type(),m_sequencer,"req");
//send child sequence via start()
cseq.start(m_sequence,this);
//send sequence item
void'($cast(req,tmp));
start_item(req);
req.randomize with {data==20;};
finish_item(req);
endtask
endclass
class sequence 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;
forever begin
seq_item_port.get_next_item(tmp);
void'($cast(req,tmp));
`uvm_info("DRV",$sformatf("got a item \n %s",req.split()),UVM_LOW)
seq_item_port.item_done();
end
endtask
endclass
class enc enxtends uvm_enc;
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_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfuction
endclass
输出结果为:
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item
...
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item
...
- 在这段例码中,主要使用了两种方法,第一个方法时针对将sequence挂载到sequencer上的应用。
- uvm_sequence::start(uvm_sequence_base sequencer,
uvm_sequence_base parnet_sequence = null
int this_priority = -1,bit call_pre_post = 1) - 在使用该方法的过程中,用户首先应该指明sequencer的句柄,如果该sequence是顶部sequence,即没有更上层的sequence嵌套它,则它可以省略对第二个参数parent_sequence的指定。
- 第三个参数的默认值-1会使得该sequence如果有parent_sequence会继承其优先级值,如果他是顶部(root)sequence,则其优先级会被自动设定为100,用户也可以自己指定优先级数值
- 第四个参数建议使用默认值,这样的话uvm_sequence::pre_body()和uvm_sequence::post_body()两个方法会在uvm_sequence::body()的前后执行。
- 在上面的例子中,child_seq被嵌套到top_seq中,继而在挂载是需要指定parent_sequence;而在test一层调用top_seq时,由于它是root sequence,则不需要再指定parent sequence,这一点用户需要主要。另外,在调用挂载sequence时,需要对这些sequence进行例化。
- 第二种发送方法时针对将Item挂载到sequencer上的应用。
uvm_sequence::start_item(uvm_sequence_item item,int set_priority = -1,
uvm_sequencer_base sequencer = null);
uvm_sequence::finish_item(uvm_sequence_item item,int set_priority = -1); - 对于start_item()的使用,第三个参数用户需要注意的是否需要将item挂载到"非当前parent sequence挂载的sequencer"上面,有点绕口是吗?简单来说,如果你想将item和其parent sequence挂载到不同的sequencer上面,你就需要指定这个参数。
- 在使用这一方法时,用户除了需要记得创建item,例如通过uvm_object::create()或者uvm_sequence::create_item(),还需要在它们之间完成item的随机化处理。
- 从这一点建议来看,需要读者了解到,对于一个item的完整传送sequence要在sequencer一侧获得通过权限,才可以顺利将item发送至driver。我们可以通过拆解这些步骤得到更多的细节:
- 创建item。
- 通过start_item()方法等待获得sequencer的授权许可,其后执行parent sequence的方法pre_do()。
- 对item进行随机化处理。
- 通过finish_item()方法在对item进行了随机化处理之后,执行parent sequence的mid_do(),以及调用uvm_sequencer::send_request()和uvm_sequencer::wait_for_item_done()来讲item发送至sequencer再完成与driver之间的握手。最后,执行了parent_sequence的post_do()。
- 这些完成的细节有两个部分需要注意。
- 第一,sequence和item自身的优先级,可以决定什么时候可以获得sequencer的授权。
- 第二,读者需要意识到,parent sequence的虚方法pre_do()、mid_do()和post_do()会发生在发送item的过程中间。
- 如果对比start()方法和start_item()/finish_item(),读者首先要分清它们面向的挂载对象是不同的。
- 在执行start()过程中,默认情况下会执行sequence的pre_body()和post_body(),但是如果start()的参数call_pre_post = 0,那么就不会这样执行,所以在一些场景中,UVM用户会奇怪为什么pre_body()和post_body()没有被执行。
- pre_body()和post_body()并不是一定会被执行的,这一点同UVM的phase顺序执行是有区别的。
- 下面一段代码是对start()方法执行过程的自然代码描述(资料引用),读者可以看到它们执行的顺序关系和条件:
- 对于pre_do()、mid_do()、post_do()而言,子一级的sequence/item在被发现过程中会间接调用parent sequence的pre_do()等方法。
- 只要在参数传递过程中,确保子一级sequence/item与parent sequence的联系,那么这些执行过程是会按照上面的描述依次执行的。
- 下面我们也给出一段start_item()/finish_item()的自然代码描述,来表示执行发送item时的相关方法执行顺序:
4.3 发送序列的相关宏
- 正是通过几个sequence/item宏来打天下的方式,用户可以通过`uvm_do/ `uvm_do_with来发送无论是sequence还是item。这种不区分对象时sequence还是item的方式,带来了不少便捷,但也容易引起verifier们的惰性。所以在使用它们之前,需要先了解它们背后的sequence和item各自发送的方法。
- 不同的宏,可能会包含创建对象的过程也可能不会创建对象。例如`uvm_do/`uvm_do_with会创建对象,而`uvm_send则不会创建对象,也不会将对象随机处理,因此要了解它们各自包含的执行内容和顺序。
- 此外还有其它的宏,例如,将优先级作为参数传递的`uvm_do_pri/`uvm_do_on_prio等,还有专门针对sequence的`uvm_create_seq/`uvm_do_seq/`uvm_do_seq_with等宏。
4.4 sequencer的仲裁特性介绍
- uvm_sequencer类自建了仲裁机制来保证多个sequence在勇士挂载到sequencer时,可以按照仲裁规则运行特定sequence中的item优先通过。
- 在实际使用中,我们可以通过uvm_sequencer::set_arbitrantion(UVM_SEQ_ARB_TYPE val)函数来设置仲裁模式,这里的仲裁模式UVM_SEQ_ARB_TYPE有下面几种值可以选择:
- UVM_SEQ_ARB_FIFO:默认模式。来自于sequences的发送请求,按照FIFO先进先出的方式被依次授权,和优先级没有关系。
- UVM_SEQ_ARB_WEIGHTED:不同的sequence的发送请求,将按照它们的优先级权重随机授权。
- UVM_SEQ_ARB_RANDOM:不同的请求会被随机授权,而无视它们的抵达顺序和优先级。
- UVM_SEQ_ARB_STRICT_FIFO:不同的请求,会按照它们的最高优先级随机授权,与抵达时间无关。
- UVM_SEQ_ARB_USER:用户可以自定义仲裁方法user_priority_arbitration()来裁定那个sequence请求被随机授权。
- 在上面的仲裁模式中,与priority有关的模式有UVM_SEQ_ARB_WEIGHTED、UVM_SEQ_ARB_STRICT_FIFO和UVM_SEQ_ARB_STRICT_RANDOM。
- 这三种模式区别在于,UVM_SEQ_ARB_WEIGHTED的授权可能会落到各个优先级sequence的请求上面,而UVM_SEQ_ARB_STRICT_RANDOM则只会将授权随机安排待最高优先级的请求上面,UVM_SEQ_ARB_STRICT_FIFO则不会随机授权,而是严格按照优先级以及抵达顺序来依次授权。
- 没有特别的要求,用户不需要再额外自定义授权机制,依次使用UVM_SEQ_ARB_USER这一模式的情况不多见,其他模式可以满足绝大多数的仲裁请求。
- 鉴于sequence传送的优先级可以影响sequencer的仲裁授权,我们有必要结合sequencer的仲裁模式和sequence的优先级给出一段例码。
4.5 Sequencer的仲裁示例
class bus_trans extends uvm_sequence_item;
rand int data;
...
endclass
class child_seq extends uvm_sequence;
rand int base;
...
task body();
bus_trans req;
repeat(2) `uvm_do_with(req,{data inside {[base:base+9]};})
endtask
endclass
class top_seq extends uvm_sequence;
...
task body();
child_seq seq1,seq2,seq3;
m_sequencer_set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
fork
`uvm_do_pri_with(seq1,500,(base == 10;))
`uvm_do_pri_with(seq2,500,{base == 20;})
`uvm_do_pri_with(seq3,300,{base == 30;})
join
endtask
endclass
class sequencer extends uvm_sequencer;
...
endclass
class driver extends uvm_driver;
...
task run_phase(uvm_phase phase);
REQ tmp;
bus_trans req;
forever begin
seq_item_port.get_next_item(tmp);
void'($cast(req,tmp));
`uvm_info("DRV",$sformatf("got a item %0d from parent sequence %s",req.data,req.get_parent_sequence().get_name()),UVM_LOW)
seq_item_port.item_done();
end
endtask
endclass
class env extends uvm_env;
sequencer sqr;
driver drv;
...
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_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
class test1 extends uvm_test;
env e;
...
task run_phase(uvm_phase phase);
top_seq seq;
phase.raise_objection(phase);
seq = new();
seq.start(e.sqr);
phase.drop_objection(phase);
endtask
endclass
输出结果为:
UVM_INFO @ 0:umv_test_top.e.drv [DRV] got a item 16 from parent sequence seq1
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item 22 from parent sequence seq2
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item 19 from parent sequence seq1
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item 23 from parent sequence seq2
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item 33 from parent sequence seq3
UVM_INFO @ 0:uvm_test_top.e.drv [DRV] got a item 32 from parent sequence seq3
- 上面的例码中,seq1、seq2、seq3在同一时刻发起传送请求,通过`uvm_do_pri_with的宏,在发送sequence时可以发传递优先级参数。
- 由于将seq1和seq2设置为同样的高优先级,而seq3设置为较低的优先级,这样在随后的UVM_SEQ_ARB_STRICT_FIFO仲裁模式下,可以从输出结果看到,按照优先级高低和传送请求的顺序,先将seq1和seq1中的item发送完毕,随后将seq3发送完。
- 除了sequence遵循仲裁机制,在一些特殊情况下,有一些sequence需要有更高权限取得sequencer的授权来访问driver。例如在需要响应中断的情况下,用于处理中断的sequence应该有更高的权限来获取sequencer的授权。
4.6 uvm_sequencer锁定机制
- uvm_sequencer提供了两种锁定机制,分别通过lock()和grab()方法实现,这两种方法的区分在于:
- lock()与unlock()这一对方法可以为sequence提供排外的访问权限,但前提条件是,该sequence首先需要按照sequencer的仲裁机制获得授权。而一旦sequence获得授权,则无需担心权限被收回,只有该sequence主动解锁(unlock)它的sequencer,才可以释放这一锁定的权限。lock()是一种阻塞任务,只有获得来了权限,它才会返回。
- grab()与ungrab()也可以为sequence提供排外的访问权限,而且它只需要在sequencer下一次授权周期时就可以无条件获得授权。与lock方法相比,grab方法无视同一时刻内发起传送请求的其它sequence,而唯一可以阻止它的只有已经预先获得授权的其他lock或者grab的sequence。
- 这里需要注意的是,由于“解铃还须系铃人”,如果sequence使用了lock()或者grab()方法,必须在sequence结束前调用unlock()或者ungrab()方法来释放权限,否则sequencer会进入死锁状态而无法继续为其余sequence授权。
sequencer锁定示例
class bus_trans extends uvm_sequence_item;
...
endclass
class child_seq extends uvm_sequence;
...
endclass
class lock_seq extends uvm_sequence;
...
task body();
bus_trans req;
#10ns;
m_sequencer.lock(this);
`uvm_info("LOCK","get exclusive access by lock()",UVM_LOW)
repeat(3) #10ns `uvm_do_with(req,{data inside{[100:110]};})
m_sequencer.unlock(this);
endtask
endclass
class grab_seq extends uvm_sequence;
...
task body();
bus_trans req;
#20ns;
m_sequencer.grab(this);
`uvm_info("GRAB","get exclusive access by grab()",UVM_LOW)
repeat(3) #10ns `uvm_do_with(req,{data inside{[200:210]};})
m_sequencer.ungrab(this);
endtask
endclass
class top_seq extends uvm_sequence;
...
task body();
child_seq seq1, seq2, seq3;
lock_seq locks;
grob_seq grabs;
m_sequencer.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
fork
`uvm_do_pri_with(seq1,500,{base == 10;})
`uvm_do_pri_with(seq2,500,{base == 20;})
`uvm_do_pri_with(seq3,300,{base == 30;})
`uvm_do_pri(locks,300)
`uvm_do(grabs)
join
endtask
endclass
- 集合例码和输出结果,我们从中可以发现如下几点:
- 对于sequence locks,在10ns时它跟其他几个sequence一同向sequencer发起请求,按照仲裁模式,sequencer先后授权给seq1、seq2、seq3,最后才授权给locks。
- 而locks在获得授权之后。就可以一直享有权限而无需担心权限被sequencer收回,locks结束前,用户需要通过unlock()方法返还权限。
- 对于sequence grabs,尽管他在20ns发起了请求权限(实际上seq1、seq2、seq3也在同一时刻发起了请求权限),而由于权限已经被locks占用,所以它无权收回权限。
- 因此只有当locks在40ns结束时,grabs才可以在sequencer没有被锁定的状态下获取权限,而grabs在条件下获取权限是无视同一时刻发起请求的其它sequence的。
- 同样的,在grabs结束前,也应当通过ungrab()方法释放权限,防止sequencer的死锁行为。
4.7 sequence的层次化
概述
- 伴随着对sequence/item发送方式的了解,读者也需要从之前4位初出茅庐的verifier梅、尤、娄、董他们的角度来看,如何完成验证的水平复用和垂直复用。
- 就水平复用而言,在MCDF各个子模块的验证语境中,它值得是如何利用已有的资源,完成高效的激励场景创建。
- 就垂直复用来看,它指的是在MCDF子系统验证中,可以完成结构复用和激励场景复用两个方面。这节的垂直复用主要关注于激励场景复用。
- 无论是水平复用还是垂直复用,激励场景的复用很大程度上取决于如何设计sequence,使得底层的sequence实现合理的粒度,帮助完成水平复用,进一步依托于底层激励场景,最终可以实现底层到高层的垂直复用。
- 本节就MCDF的实际验证场景出发,引申出以下概念来完善sequence的层次化:
- hierarchical sequence
- virtual sequence
- layering sequence
- 通过对这三个与sequence层次化有关的概念解读和实际场景分析,我们希望读者本节后可以就不同sequence场景复用设计出适合自己验证场景的sequence结构,三者在特定情况下可有机组合,为整体的验证复用提供良好的支持。