在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需要得知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不但需要按照时序依次驱动地址总线、命令码总线和数据总线,还应该等待从端的返回信号和状态值,这样才算完成了一次数据写传输。
uvm_sequence_item和uvm_sequence都是基于uvm_object,它们不同于uvm_component只应当在build阶段作为UVM环境的“不动产”进行创建和配置,而是可以在任何阶段创建。
这种类的继承带来的UVM应用区别在于:
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之间,我们看重的是它的两个特点:
数据传送机制采用的是get模式而不是put模式。我们在TLM传输中为读者介绍过两种典型的数据传送场景,如果是put模式,那么应该是sequencer将数据put至driver,而如果是get模式,那么应当是driver从sequencer获取item。
之所以选择get模式,UVM是基于下面的考虑:
(initiator、put和get,具体使用差别优势?)
无论是自驾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通常应该具备有什么类型的数据成员呢?我们将它们划分为如下几类:
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
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
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宏, 这个宏完成了三个
步骤:
区别于之前的例码,这个例码通过`uvm_do_with宏帮助理解,所谓的sequence复用就是通过高层的sequence来嵌套底层的sequence/item,最后来创建期望的场景。在示例中既有串行的激励关系,也有并行的激励关系,而在更复杂的场景中,用户还可以考虑加入事件同步,或者一定的延迟关系来构成sequence/item之间的时序关系。
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()做 了如下的几件事情:
在定义driver时,它的主任务driver::run_phase()也应通常做出如下处理:
对于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的挂载。
(arbitration的触发依靠driver的get_next_item)