目录
对于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_port 和sequence::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句柄做出动态类型转换,等到正确类型之后再进行接下来的操作。