我们先来看源码
1,Driver 和 Sequencer的源码
uvm_driver源码
class uvm_driver #(type REQ=uvm_sequence_item,
type RSP=REQ) extends uvm_component;
// Port: seq_item_port
//
// Derived driver classes should use this port to request items from the
// sequencer. They may also use it to send responses back.
uvm_seq_item_pull_port #(REQ, RSP) seq_item_port;
uvm_seq_item_pull_port #(REQ, RSP) seq_item_prod_if; // alias
// Port: rsp_port
//
// This port provides an alternate way of sending responses back to the
// originating sequencer. Which port to use depends on which export the
// sequencer provides for connection.
uvm_analysis_port #(RSP) rsp_port;
REQ req;
RSP rsp;
.......
endclass
uvm_sequencer源码
class uvm_sequencer #(type REQ=uvm_sequence_item, RSP=REQ)
extends uvm_sequencer_param_base #(REQ, RSP);
typedef uvm_sequencer #( REQ , RSP) this_type;
bit sequence_item_requested;
bit get_next_item_called;
`uvm_component_param_utils(this_type)
// Variable: seq_item_export
//
// This export provides access to this sequencer's implementation of the
// sequencer interface, <uvm_sqr_if_base #(REQ,RSP)>, which defines the following
// methods:
//
//| Requests:
//| virtual task get_next_item (output REQ request);
//| virtual task try_next_item (output REQ request);
//| virtual task get (output REQ request);
//| virtual task peek (output REQ request);
//| Responses:
//| virtual function void item_done (input RSP response=null);
//| virtual task put (input RSP response);
//| Sync Control:
//| virtual task wait_for_sequences ();
//| virtual function bit has_do_available ();
//
// See <uvm_sqr_if_base #(REQ,RSP)> for information about this interface.
uvm_seq_item_pull_imp #(REQ, RSP, this_type) seq_item_export;
......
endclass
2,Driver 和 Sequencer 之间的握手
在uvm_driver的源码中有一个 uvm_seq_item_pull_port #(REQ,RSP) 类型的 seq_item_port,从源码上的注释看,这个tml_port是用来向sequencer请求数据 的;
在uvm_sequencer的源码中,uvm_seq_item_pull_imp #(REQ, RSP, this_type) 类型的seq_item_export,同样也是用来与 driver进行通信的;
说白了这块的逻辑也非常简单,就是driver不断的通过 seq_item_pull_port 向 sequencer请求数据,由于是pull的动作,driver是发起人,也是消费者,而sequencer是接收人,只要有seq_item,就把seq_item丢给driver,driver再通过虚接口,将seq_item发送给dut,完成数据的传输,最后在调用item_done通知sequence数据发送完了,可以发送下一笔数据了。既然是port 和imp之间的链接,那么必然在验证环境中要实现port和imp之间的连接,由于driver和sequencer都是“属于”agent的,所以我们可以在agent的connect_phase将两者连接起来:
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver #(my_sequence_item) m_drv;
my_sequencer #(my_sequence_item) m_seqr;
virtual function void connect_phase(uvm_phase phase);
// other connection ...
m_drv.seq_item_port.connect(m_seqr.seq_item_export);
endfunction
endclass
有一个很别扭的点是,明明在uvm_sequencer中定义的是imp,为啥却要起个 seq_item_export的名字,为啥不叫 seq_item_imp,这种做法应该是想要用户注意,sequencer只是一个中转站,是将sequence的 seq_item 通过 sequencer的调度将seq_item送给driver,不然起个_export的名字实在是歧义满满。
3,Sequencer和sequence之间的通信
sequencer和sequence之间的通信在源码上看到的结果不是很清晰,一共分了6个步骤,靠如下六个函数(task)完成,代码如下:
// The following methods are called, in order
//
//|
//| sequencer.wait_for_grant(prior) (task) \ start_item \
//| parent_seq.pre_do(1) (task) / \
//| `uvm_do* macros
//| parent_seq.mid_do(item) (func) \ /
//| sequencer.send_request(item) (func) \finish_item /
//| sequencer.wait_for_item_done() (task) /
//| parent_seq.post_do(item) (func) /
从上到下依次是:
1,wait_for_grant请求sequencer,是否可以获取seq_item,
2,在seq_item随机化之前的callback函数,可以对item进行修改,或者做一些打印
3,在seq_item随机化之后,送给driver之前的callback函数,可以对item进行修改(这很重要)
4,将seq_item送给sequencer,
5,等待接收driver调用item_done,
6,seq_item发送之后的callback函数(用户无法直接调用)。
4,sequence->sequencer->driver 的数据传递以及 driver -> sequence 的反馈、
这块我简单的说明一下整个数据流的过程,大前提是:driver的pull_port一直处于“饥渴”状态,等待sequencer把seq_item送过来。
1,sequence发起 wait_for_grant ,请求sequencer是否能够接收seq_item;
2,sequencer内部等待sequence aribtration调度完成,可以接收新数据;
3,sequence 通过函数 send_request(item)将seq_item发送给sequencer;
4,driver调用 get_next_item获取seq_item;
5,driver将 seq_item通过virtual interface将item发送给dut;
6,driver调用 item_done 通知sequence当前的seq_item已经发送完成了
7,sequence的函数:wait_item_done() 结束条件满足(实际上在seq_item传输过程中,会产生seq_id,item_done之后,将id通过sequencer发送给sequence,sequence通过比对seq_id来判断当前的item是否发送完,尽管id=-1也有握手的过程)。
8,进行下一个seq_item;
借用下面这个图,来说明整个过程:
当然,在整个过程中,我们没必要将整个过程的代码都完整的实现一边,大部分的代码uvm的源码已经帮我们完成了,我们只需要完成很小的一部分如下:
1,driver中需要实现的代码:
class my_driver extends uvm_driver#(my_data);
`uvm_component_utils(my_driver)
virtual task run_phase(uvm_phase phase);
super.run_phase(phase);
// 调用get_next_item 获取新的seq_item
seq_item_port.get_next_item(req);
// 发送seq_item,我们这里用#20ns 来代替
#20ns;
// 发送完seq_item之后,通知sequence,数据已经发送完
seq_item_port.item_done();
endtask
......
......
......
endclass
2,sequence中需要实现的代码:
class my_sequence extends uvm_sequence;
`uvm_object_utils(my_sequence)
virtual task body();
//1 ,create item
my_data_tx = my_data::type_id::create("tx");
// 我们这里用start_item代替上面uvm源码里面的那几个task/fun
start_item(tx);
// 随机化
tx.randomize(); // 这里也可以用randomize.with({constraint})
// 发送seq_item
finish_item(tx);
endtask
endclass
上面我们讲的这部分数据流中都隐式的包含了driver的put_reponse 和 sequence的get_response ,下面说明如何显示的实现这种driver和sequence的应答。
5,sequence->sequencer->driver 的数据传递以及 driver -> sequence 的反馈
数据流与上面的大部分类似,这里只把response的把部分加上。
这块我简单的说明一下整个数据流的过程,大前提是:driver的pull_port一直处于“饥渴”状态,等待sequencer把seq_item送过来。
1,sequence发起 wait_for_grant ,请求sequencer是否能够接收seq_item;
2,sequencer内部等待sequence aribtration调度完成,可以接收新数据;
3,sequence 通过函数 send_request(req)将seq_item发送给sequencer;
4,driver调用 seq_item_port.get(req)获取seq_item;
5,driver将 seq_item通过virtual interface将item发送给dut;
6,driver调用 seq_item_port.put(rsp) 通知sequence当前的seq_item已经发送完成了,并且反馈给sequence rsp,
7,sequence的函数:wait_item_done() 等待driver反馈 item_done();
8,get_response(),获取这个rsp
9,进行下一个seq_item;
整个过程借用下面这个图说明:
上面我们所说的response机制是:driver将rsp推送给sequencer,sequencer内部有一个rsp队列,当有新的response进入时,就push_back进入队列,sequence中的get_response()就是从这个队列中pop_front一个数据,尽管这个队列的深度只有8,你可以可以通过函数set_response_queue_depth(-1)将队列的深度设置为无限大(设为-1之后,由于是int型,所以最大深度应该是32767),但是如果不设置深度的话,超过8这个深度,就是报溢出的错。
这个深度设置其实也非常有意义,比如说,我们在连续进行寄存器的读写测试时,我们可以连续的对一片寄存器进行读操作,将所有rsp即dut返回的读数据全部存储在这个rsp的队列中,当所有的读操作都进行完成之后,我们在把这个rsp的数据读空,并根据id信息将读出来的数据一一还原,毕竟UVM自带的ral方案实在是太难弄了。。。
关于sequence arbitration,uvm源码里面有很多函数参与sequence的仲裁,关于这部分可以参考张强的《UVM实战》这本书,这里面不展开了,关于如何启动sequence会重新开一题。
个人感觉,就整个UVM的设计来看,不管他采用了什么高大上的设计模式,sequencer,sequence 和 driver 这三部分之间的通信和数据传递可以说设计的非常巧妙和复杂,满足对各种场景的验证需求,可以说是非常完善了。