(一)一个agent的情况
1:从uvm_sequence_item开始定义数据的最小单位:transaction(事务)
例如:
class apb_sequence_item extends uvm_sequence_item; //从uvm_sequence_item继承出来的类用于定义事务。
'uvm_object_utils(apb_sequence_item) //使用object注册
//在transaction中一般定义一些接口信号的随机变量,并添加一些约束,还有一些验证平台的控制信号
logic rand ['APB_ADDR_WIDTH-1 : 0] addr; //接口信号
logic rand ['APB_DATA_WIDTH-1 : 0] data; //接口信号
logic rand we; //接口信号
logic rand delay; //控制信号,控制发包间隔
logic rand err; //若随机的地址无效,则返回error
//约束
constraint addr_aligment { addr[1:0]==0;} 数据以32bit位并行传输,地址以0-4-8-c顺序变化,低两位为2‘b00
constraint delay_bonds {delay inside {[1:20]};}
extern function new(string name="apb_sequence_item");
endclass
function apb_sequence_item::new(string name="apb_sequence_item");
super.new(name);
endclass
2.编写sequence,发送req
在apb_sequence_item定义好之后,数据传输就会以apb_sequence_item为单位进行数据传输,在传输之前还要对这些数据进行一个封装,也就是生成各种各样的序列,这部分功能是由sequence完成的,这些序列都有一定的功能,比如apb_write_seq完成读的功能,apb_read_seq完成写的功能,具体哪些序列会被驱动到dut接口上是由test来决定的。比如下面这个具有写数据到dut的一个序列。
calss apb_write_seq extends uvm_sequence #(apb_sequence_item);
'uvm_object_utils(apb_write_seq)
//主要有两个方法,一个是new,另一个是body,其中body为任务,主要是为了完成发送事务的功能
function new(string name="aob_write_seq");
super.new(name);
endfunction
virtual task body();
apb_sequence_item req;
//发送sequence的方法有两种,一种是采用宏:'uvm_do(req),使用宏可以代替一下四个步骤,但是不能自己定义其他的限制。且使用'umv_do只是发送sequence,真正的启动是在test中。另一种是四步法,如下:
req=apb_sequence_item::type_id::creat("req") //1.例化
start_item(req); //2.获取sequencer的授权(test中的启动),开始生产数据
if(!req.randomize)begin //3.随机化sequence
'uvm_info("body","req randomize failed")
end
//一般随机化之后会对某些信号进行处理以获得我们想要的功能,比如使这个序列产生写的功能,那么就需要将写使能置为1
req.we=1; //使用点操作来访问序列信号,并对其赋值。
finish_item(req); //4.将sequence发送到sequencer
endtask
endclass
3.定义sequencer、连接sqr与driver
一个sequence在向sequencer发送transaction前,要先向sequencer发送一个请求,sequencer把这个请求放在一个仲裁队列中。作为sequencer,它需做两件事情:第一,检测仲裁队列里是否有某个sequence发送transaction的请求;第二,检测driver是否申请transaction。
只有一个agent时,sequencer一般在agent类之前使用typedef定义一下,然后在agent的build中例化、agt的connect phase中连接sqr与drv。
typedef uvm_sequencer #(apb_sequence_item) my_sequencer;
//在agt中声明并例化sqr
class i_agt extends uvm_agent;
my_driver drv;
my_monitor mon;
my_sequencer sqr; //声明aqr
...
function void build_phase(uvm_phase phase) //例化seqr、mon、drv
super.build_phase(phase);
if(is_active == UVM_ACTIVE) begin
drv= my_driver::type_id::create("drv",this);
sqr= my_sequencer::type_id::create("sqr",this);
end
mon = my_monitor::type_id::create("mon",this)
endfunction
function void connect_phase(uvm_phase phase)//连接
super.connect_phase(phase);
if(is_active == UVM_ACTIVE) begin
drv.seq_item_port.connect(sqr.seq_item_export);
end
endfunction
endclass
4.driver接受req
在uvm_driver中有成员变量seq_item_port,而在uvm_sequencer中有成员变量seq_item_export,这两者之间可以建立一个“通道”,通道中传递的transaction类型就是定义my_sequencer和my_driver时指定的transaction类型,这里是apb_dequence_item类型。
class driver extends uvm_driver #(apb_sequence_item);
'uvm_component_utils(driver)
extern function new(string name="driver",uvm_phase phase=null);
extern tase main_phase();
extern task drive_one_packet(apb_sequence_item req);
endclass
function driver::new(string name="driver",uvm_conponent parent=null);
super.new(name,parent);
endfunction
task driver::main_phase();
apb_sequence_item req;
seq_item_port.get_next_item(req); //uvm_driver的内部接口:seq_item_port
drive_one_packet(req);
seq_item_port.item_done(); //反馈信号,下次调用get_next_item()之前,item_done被调用才可以。
endtask
当driver使用get_next_item得到一个transaction时,sequencer自己也保留一份刚刚发送出的transaction。当出现sequencer发出了transaction,而driver并没有得到的情况时,sequencer会把保留的这份transaction再发送出去。那么sequencer如何知道driver是否已经成功得到transaction呢?如果在下次调用get_next_item前,item_done被调用,那么sequencer就认为driver已经得到了这个transaction,将会把这个transaction删除。换言之,这其实是一种为了增加可靠性而使用的握手机制。
5.启动sequence
sequence不在uvm的层次结构中,需要挂载在sequencer上,通过sequencer来启动。
sequence的启动通常在这么几个地方做:(建议统一在test的main_phase中启动)
1:在某一个component(比如env、sqr)的main_phase中。
2:测试用例类uvm_test的main_phase中。
3:还可以在已经被启动的sequence的body()方法中。
启动方式有两种:
//以在uvm_test中为例
class base_taest extends uvm_test;
'uvm_component_utils(base_teat)
extern function new(string name="base_test",uvm_conponent parent =null); //注意conponent的new函数有两个参数,分别为字符串名name称和parent
extern task main_phase (uvm_phase phase);
...
endclass
function base_test::new(string name="base_test",uvm_conponent phase= null);
super.new(name,parent)
endfunction
task base_test::main_phase(uvm_phase phase);
super.build(phase);
//方法一:通过start函数指定seqencer启动sequence,需要在启动前后提起/撤销objection
phase.raise_objection(this);
apb_write_seq m_seq; //声明要启动的sequence
m_seq=apb_write::type_id::creat("m_seq"); //例化sequence
m_seq.start(env.i_agt.sqr); //将sequence交给指定的sequencer,括号中是sequencer的路径
phase.drop_objection(this);
//方法二:通过config_db的方式, 该方式有先例化sequence和直接使用cinfig_db的方式
//如果使用config_db的方式启动sequence,那么objection的提起与撤销将在发送sequence的时候,
//也就是在sequence中发送seq前后使用starting_phase来提起/撤销objection
//方法2.1:先定义和例化
apb_write_sqe m_seq;
m_seq=apb_write_seq::type_id::creat("m_seq");
config_db #(apb_write_seq) set::(this, "env_i_agt_sqr.main_phase", "defualt_sequence", m_seq)
//方法2.2:直接使用config_db ,是比较常用的一种办法。
config_db #(uvm_object_wrapper) set::(this,"env.i_agt.aqr.main_phase" , "defualt_sequence", apb_write_seq::type_id::get());
//注意参数列表里要由uvm_sequence_base变成uvm_object_wrapper
endtask
还可以在已经启动的sequence中启动sequence
calss apb_write_seq extends uvm_sequence #(apb_sequence_item);
'uvm_object_utils(apb_write_seq)
//主要有两个方法,一个是new,另一个是body,其中body为任务,主要是为了完成发送事务的功能
extern function new(string name ="apb_write_seq");
extern task body();
endclass
function apb_write_seq::new(string name="aob_write_seq");
super.new(name);
endfunction
task apb_write_seq :: body();
apb_sequence_item req;
if(starting_phase!=null)
start_phase.raise_objection(this);
apb_read_seq seq;
seq=apb_read_seq::type_id::create("seq");
seq.start(p_sequencer.sqr);
if(strting_phase!=null)
starting_phase.drop_objection(this);
endtask
什么是正在启动的sequence?见下图在某一个test中启动了多个sequence,这些sequence的关系可以是并行、串行或者层次执行(蓝色部分),在层次执行中就有在正在启动的sequence中启动别的sequence。
(二)有多个agent,且各个agent之间需要相互协调
虚序列的由来:
假设现在在测试平台中有两个agent,分别为agent1与agent2。如果某个测试用例想发送这样一个序列:先让agent1发送写序列,再让agent2发送读序列,也就是说这两个序列之间有一个先后关系。
如何发送这样的序列呢?
第一个办法是使用全局事件触发同步:也就是第一个序列发送完后触发事件,然后第二个序列再开始发送。这种办法使用了全局事件实现了一次同步,如果有多次同步,在使用这些方法就会显得很笨拙。
在这里提出第二种方法:虚序列。定义一个virtual_sequence,在该vertual_sequence中嵌入多个sequence,以调度这些sequence。虚序列还要配合虚序列器(virtual_sequencer)使用,虚序列器中的sequencer用于指向实际的sequencer。
注意此处虚序列的嵌入和普通序列的嵌入:普通sequence中嵌套的sequence都在同一个sequencer上启动(通过sequencer仲裁决定);而在virtual sequence中嵌套的sequence可以在不同sequencer实体上同时启动。
”虚"在哪?
system verilog中有很多虚拟类,虚方法,虚接口,这些都需要virtual定义,
本次要介绍的虚序列器(virtual_sequencer)及虚序列(virtual_sequence)也带有虚字。
但是此虚非彼虚,虚序列器和虚序列不需要使用virtual声明,而是都继承自uvm_sequencer和uvm_sequence。这里所说的“虚”指的是:虚序列器本身并不传递事务,也不连接到driver,他只是用于控制其他sequencer。由于并不实际传递事务,所以定义的时候可以不指定要传递的事务类型。
实际上,代码是自由的,virtual sequencer也只是一个名字而已,谁也没有规定叫了这个名字就不能去发送sequence item,也没有谁规定一个验证环境只能有一个virtual sequencer。只不过,这种多sequence的管理方法比较好用和科学,才被单独提出来和介绍,一切要根据实际情况去做决定。
什么时候需要用到虚序列?
这里给出了三种场景:
第一种是只有一个agent的情况,这个时候多个激励驱动这一个agent是不需要virtual sequence的。
第二情况是有多个agent,但这些agent的sequence并不互相影响,也就是说agent1的sequence和agent2的sequence并没有先后之分。这种情况也没有必要设置vietual sequence
第三种情况是有多个agent,而且这些agent之间需要相互协调,比如上文描述的agent1的sequence要先发送,然后再发送agent2的sequence,这就需要virtual sequence来进行控制了。
具体使用方法:
1.定义虚序列器,并嵌入序列器句柄。
//假设现在平台中有两个agent:apb_agent与spi_agent
class v_sequencer extends uvm_sequencer;
apb_sequencer apb_vsqr; //嵌入虚序列器
spi_sequencer api_vsqr;
endclass
2.定义虚序列,并嵌入序列,然后将使用'uvm_do_with将这些序列挂载到虚序列器中的句柄上。
class v_sequence extends uvm_sequence ;
'uvm_object_utils(v_sequence)
'uvm_declare_p_sequencer(v_sequencer) //通过该宏将虚序列绑定到虚序列器
apb_sequence apb_seq;
spi_sequence spi_seq; //嵌入虚序列
virtual task body();
'uvm_do_on(apb_seq , p_sequencer.apb_sqr) //将虚序列发送到虚序列器对应的句柄上。
'uvm_do_on(api_seq , p_sequencer.spi_sqr)
endclass
3.将虚序列器中的句柄指向真实的序列器。
class env extends uvm_enivernment;
...
v_sequencer v_sqr;
apb_agent apb_agt;
spi_agent spi_agt;
function void connect_phase(uvm_phase phase);
super.connect_phase(phase)
v_sqr.apb_sqr=apb_agt.sqr;
v_sqr.spi_sqr=spi_agt.sqr;
endclass
4.启动虚序列:从下面的代码中可以看出,使用vritual sequence可以减少config_db的使用量,因为config_db第二个参数是字符串,非常容易出错,在设计时应尽量减少config_db的使用。假如没有virtual sequence,则每个面向不同sequencer的sequence都要使用config_db启动,而使用了virtual sequence的只需要一次config_db。
class test extends uvm_test;
...
task main_phase();
config_db #(uvm_object_wrapper)set::(this ,"env.v_sqr.main_phase", "default_phase", v_sequence::type_id::get())
endclass
总结:
所以使用virtual sequence相当于集结了众多面向不同sequencer的sequence群,这些sequence在virtual sequence中被控制调度,并发送到对应虚序列器中的sequencer句柄中,这些句柄都指向的真实的sequencer中。
virtual sequence:承载不同目标sequencer的sequence群落,实现sequence同步;virtual sequence一般只会挂载到virtual sequencer上,且没有自己的sequence_item,只用于控制其他的sequence执行顺序,起统一调度作用。
virtual sequencer:桥接其它sequencer,即连接所有底层sequencer的句柄(指针),是一个中心化的路由器。virtual sequencer本身并不传送item数据对象,因此不需要与driver进行TLM连接。所以用户需在顶层的connect阶段做好virtual sequencer中各个sequencer句柄与sequencer实体对象的一一连接,避免句柄悬空。
参考:UVM中sequence的启动方式_小小verifier的博客-CSDN博客
SystemVerilog|UVM|如果你要搞很多Sequence,请看过来-面包板社区
UVM仿真技术(Virtual Sequence & Virtual Sequencer) - 知乎
UVM——虚序列器与虚序列(virtual sequencer与virtual sequence)_SD.ZHAI的博客-CSDN博客