在第一节中我们提过,为什么不把 transaction 实例化、随机和驱动全部放入 driver 中完成,我们验证的主要工作量,除了搭建验证环境之外,还有一大工作-拼凑场景case,其中不同场景中 transaction 的发送给数量和组织形式各有差异,我们如果把 transaction 放入driver 中,就需要经常改动 driver ,如果将驱动协议部分放入 driver 中,而在 sequnce 中完成 transaction 的实例化和随机化任务,我们创建新 case 时仅需要新建一个 sequence 即可。
上述过程如下图所示:
采用 sequence + sequencer + driver 实现驱动有以下优势:
a.sequence 、sequencer 和driver 三者之间分工合作,任务明确
b.driver 仅实现依据验证 Spec 实现驱动协议部分,可以重用
c.sequence 可以在上层对 transaction 进行管理组织
d.具有更好的重用性-不同的 sequence 实现不同的场景
e.具有更好的管控性如控制 sequence 的执行顺序及其不同 sequnece 之间的同步
1.什么是 sequence
上文提到,子弹,弹夹和枪的比喻,想必大家都有映像,sequence 就像一个弹夹,里面装了很多"子弹",而这里的"子弹"就是 transaction, 弹夹可长可短,只要子弹型号一致(同一种 transaction),就能通过 sequencer 和 driver 把数据驱动给 DUT。在下图中,有三个 sequence 挂载于同一个 sequncer 上(当然也可不同时间挂在其中一个 sequence),通过 driver 进行数据驱动,其中 sequencer 和 driver 是共同使用的。
1.1.sequence 实例
class my_transaction extends uvm_sequence_item;
`uvm_object_utils(my_transaction )
rand bit[47:0] dmac;
rand bit[47:0] smac;
constrain dma_cons{
dmac inside {[0:100]};
smac inside {[8:90]};
}
function void pre_randomize();
endfunction
function void post_randomize();
endfunction
...
endclass
class my_sequence extends uvm_sequence(# my_transaction )
`uvm_object_utils(my_sequence)
task body();
repeat(item_cnt) begin
`uvm_do(req);
get_response(req);
end
endtask
endclass
首先,我们要根据验证 Spec 实现 my_transaction(sequence_item 类),主要是完成随机变量的定义和约束(constrain),其中 pre_randomize() 函数会在 sequence_item 在调用 randomize() 函数前自动调用,同理 post_randomize() 为其后自动调用,这两个函数为空函数,需要用户根据自己的需要实现;
第二,实现 my_sequence 类,它必须继承于 uvm_sequence,(# my_transaction )为 uvm_sequence 参数,如果不指定则默认为 uvm_sequence_item类型,同时必须实现 body() 函数,它会在调用 sequence 启动是自动被调用。最后需要说明一下 'uvm_do(req), 这是一个 uvm library 定义的宏,它会完成 my_transaction 的实例化、随机化和驱动任务,在收到 driver 的反馈信息后,才结束此次发送,否则会一直等待。其中的 req 也为 uvm library 在 uvm_sequence 中定义成员变量,只要继承于它,即可直接用,其类型为 my_transaction(默认为 uvm_sequence) ;
第三,需要将 my_sequence 挂载于能发送 (# my_transaction ) 的 sequencer 上;
第四,启动 sequence。后续两项在本节后续会详细讲述。
2.2.sequence 管控性实例
class my_sequence extends uvm_sequence(# my_transaction )
`uvm_object_utils(my_sequence)
clk_rst_seq seq_clk;
reg_cfg_seq seq_ral;
data_trans_seq seq_data;
task body();
`uvm_do(seq_ral);
repeat(item_cnt) begin
`uvm_do(seq_a);
`uvm_do(seq_b);
end
endtask
endclass
实例中,my_sequence包含 3 个 sequence 成员变量,其中 seq_clk 用于产生复位信号,在发送结束后,连续发送 item_cnt 组,seq_ral 和 seq_data 数据。
2.什么是 sequencer
sequencer 为 uvm_component,是验证环境的不动产;而 sequence 是 uvm_object 类型,是环境中的动态产物,随时可以创建和析构。它在验证环境作为 sequnce 于 driver 之间的连接器,我们创建出来的 sequence 实例的 sequence_item 需要通过 sequencer 才能进入验证环境中,继而才能传输给 driver。sequencer 的应用实例代码如下(直接继承于 uvm_sequencer,不需要添加任何代码):
class my_sequencer extends uvm_sequencer #(my_transaction);
`uvm_component_utils(my_sequencer);
function new(string name, uvm_component phase);
super.new(name,parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
endfunction
endclass
3.什么是 driver
driver 也为 uvm_component,是验证环境的不动产,常用套路如下,继承于 uvm_driver ,重新实现 run_phase 函数, 因为一般不在 driver 中结束验证仿真,所以 driver 的 run_phase() 中采用了一个永动机,while(1)操作,它只要 seq_item_port 有 transaction 就会通过 get_next_item(req)获取,并通过 send(req) 函数按照 验证 Spec 时序驱动至 DUT,最后在发送结束后,通过 seq_item_port.item_done() 通知 sequence 当前 transaction已经发送完毕,可以发送下一个;也可以通过专用回复通道 seq_item_port.put_response(rsp) 通知 sequence 当前 transaction已经发送完毕,其中的 rsp 为返回的 transaction ,sequence 中的 get_response(req)和 driver 中的 put_response(rsp),需要配合一起使用或者都不用,否则会产生挂死。
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver );
...
virtual task run_phase(uvm_phase phase);
...
while(1)begin
seq_item_port.get_next_item(req);
send(req);
rsp = new("rsp");
rsp.set_id_info(req);
seq_item_port.item_done();
seq_item_port.put_response(rsp);
end
endtask
endclass
4.sequence、sequencer 和 driver 连接关系
sequence 为 uvm_obejct 类型,它与组件类 sequencer 的关系并非连接,而是挂载。在 uvm 中组件之间的连接一般在 connect_phase 完成,是一种固定的连接关系,在验证环境仿真期间不会发送变换,sequence 与 sequencer 具体的挂载过程将在 sequence 启动 一节详细讲述。
sequencer 和 driver 之间为组件之间的连接-TLM通信,uvm library 为它们之间内嵌了两套 TLM 通信端口,一组为 seq_item_port 和 seq_item_export,这组 TML 端口用于 sequence 与 driver 之间双向通信(seq_item_port .item_done(rsp) 也可以用于返回 transaction);另一组为 rsp_export 和 rsp_port,这组 TML 端口仅用于 driver 向 sequencer 传递反馈信息(传递 rsp),为单向通信方式。
先解释一下 ·uvm_do(req),这个宏具体做了什么,其主要动作如下代码所示,
virtual task body();
…
tr = new("tr");
start_item(tr);
assert(tr.randomize() with {tr.pload.size() == 200;});
finish_item(tr);
…
endtask
稍微解释一下上述代码:
a. tr = new("tr"); : 首先对待发送的 transaction 实例化
b. start_item(tr); : 通过 start_item 获取 sequencer 的权限;
c. assert(tr.randomize() with {tr.pload.size() == 200;}); : transaction 随机化;
d. finish_item(tr); : 将 transaction 发送至 driver ,在获取到 driver 反馈信息后结束。
start_item 和 finish_item细节如下:
为了理清上述代码的所有过程,我们首先得清楚 sequence 和 sequencer 与 driver 之间的信息传递的过程,一个 sequence 在向 sequencer 发送 transaction 前,要先向 sequencer 发送一个请求,sequencer 把这个请求放在一个仲裁队列中。作为 sequencer,它需做两件事情:**第一,检测仲裁队列里是否有某个 sequence 发送 transaction 的请求;第二,检测 driver 是否申请 transaction **。
a. 如果仲裁队列里有发送请求,但是 driver 没有申请 transaction,那么 sequencer 将会一直处于等待 driver 的状态,直到 driver 申请新的 transaction。此时,sequencer 同意 sequence 的发送请求,sequence 在得到 sequencer 的批准后,产生出一个 transaction 并交给 sequencer,后者把这个 transaction 交给driver。
b. 如果仲裁队列中没有发送请求,但是 driver 向 sequencer 申请新的 transaction,那么 sequencer 将会处于等待 sequence 的状态,一直到有 sequence 递交发送请求,sequencer 马上同意这个请求,sequence 产生 transaction 并交给 sequencer,最终 driver 获得这个transaction。
c. 如果仲裁队列中有发送请求,同时 driver 也在向 sequencer 申请新的 transaction,那么将会同意发送请求,sequence 产生 transaction 并交给 sequencer,最终 driver 获得这个 transaction。
现在上述代码就容易理解了,pre_do,mid_do和 post_do 为空函数,需要用户根据需求实现;wait_for_grant() 用于向 sequencer 请求发送transaction;send_request() 用于等待 sequencer 批准,在获得权限后,将 transaction 发送给 sequencer;wait_for_item_done()用于等待 sequencer 传递的结束信号(sequencer 在 driver 执行 item_done 后,才认为当前 transaction 已经发送结束)。
除了 'uvm_do 宏外,常用宏的还有 'uvm_create(创建 transaction)、 'uvm_send(发送 transction) 和 'uvm_do_with(比 'uvm_do 多了随机化) 等,这些宏的使用方式大同小异,在使用时再去看书吧。
5.sequence 启动方式
a. my_sequence.start(my_sequencer) :start 函数有两个用途,其一,将 my_sequence 挂在于 my_sequencer 上;其二,启动 my_sequence(启动后,自动执行 body() 函数)
b. uvm_config_db#(uvm_object_wrapper)::set(this, “sqr.main_phase”,”default_sequence”,my_sequence::type_id::get()) :工程应用中最常用的方式,将 my_sequencer 挂载于 sqr(env 中 sequencer 实例名) 上,该 sequence 会在 main_phase 运行阶段启动。也可以将 default_sequence 设置在其他 phase 运行时启动,需要注意的是这种方式,设置的带待启动的 sequence 是采用的已经被实例化的实例名,而不是 sequence 的类名;
c.uvm_config_db#(uvm_object_wrapper)::set(this, “sqr.main_phase”,”default_sequence”,my_sequence::get_type) ,这种启动方式和第二种类似,只是采用的是 sequence 类名。
task my_sequencer::main_phase(uvm_phase phase);
...
seq.starting_phase = phase;
seq.start(this);
endtask
c. uvm_x_on(my_sequence, my_sequencer) :此处的 uvm_x_on() 主要有两种 uvm_do_on()和 uvm_creat_on(),它们相比 uvm_do()和 uvm_creat() 原有功能,额外增加的是,将 my_sequence 挂载于 my_sequencer 上。
6.什么是 virutal sequence/sequencer
6.1.为什么要引入 virutal sequence/sequencer
为了帮助理解,我们先来说说为什么要引入 virtual sequence/sequencer 在前面的示例中,我们均对 sequence 和 sequencer 通过 #(my_transaction),设置了当前 sequence 和 sequencer 的参数类型,但这同时也限定了当前 sequencer 仅能传递参数类型是 #(my_transaction) 的 sequence 类型。其实下面代码所示的这种用法是有前提的,因为 'uvm_do(sequence) 这个宏默认采用的 sequencer 是 my_sequence 类挂载的 sequncer,所以 clk_rst_seq、reg_cfg_seq 和 data_trans_seq 3 个 sequnce类的参数类型必须同为 #(my_transaction) 参数。
class my_sequence extends uvm_sequence(# my_transaction )
`uvm_object_utils(my_sequence)
clk_rst_seq seq_rst;
reg_cfg_seq seq_ral;
data_trans_seq seq_data;
task body();
`uvm_do(seq_rst);
repeat(item_cnt) begin
`uvm_do(seq_ral);
`uvm_do(seq_data);
end
endtask
endclass
那么有没有办法将两个截然不同的 transaction 交给同一个 sequencer 呢?可以,只是需要将 sequencer 和 driver 能够接受的数据类型设置为 uvm_sequence_item (默认类型):
class my_sequencer extends uvm_sequencer #(uvm_sequence_item);
class my_driver extends uvm_driver#(uvm_sequence_item);
这样带来的问题是,由于 driver 中接收的数据类型是 uvm_sequence_item,如果它要使用 clk_rst_seq、reg_cfg_seq 和 data_trans_seq 中的成员变量,必须使用 cast 转换:
task my_driver::main_phase(uvm_phase phase);
clk_rst_seq seq_rst;
reg_cfg_seq seq_ral;
data_trans_seq seq_data;
…
while(1) begin
seq_item_port.get_next_item(req);
if($cast(seq_rst, req)) begin
drive_my_transaction(req);
`uvm_info("driver", "receive clk_rst_seq type ", UVM_MEDIUM)
end
else if($cast(seq_ral, req)) begin
drive_your_transaction(req);
`uvm_info("driver", "receive reg_cfg_seq type ", UVM_MEDIUM)
end if($cast(seq_data, req)) begin
drive_your_transaction(req);
`uvm_info("driver", "receive data_trans_seq type ", UVM_MEDIUM)
end
else begin
`uvm_error("driver", "receive type is unknown")
end
seq_item_port.item_done();
end
endtask
通过 cast() 来区分是不是很麻烦,如果你不想这种方式,virutal sequence/sequencer 就是你的最佳选择:
virutal sequence : 可以用于承载不同目标的 sequence (参数不同),组织协调不同的 sequence 按照一定的协议顺序,异步或者同步启动; 它与普通的 sequence 的区别是,它可以包含不同类型的 sequence,其他没有任何区别。
virutal sequencer : virutal sequencer 和普通 sequencer 不同,它用于桥接其 virtual sequencer 内包含的 sequencer,即 virutal sequencer 用于链接所有底层 sequencer 的句柄;virtual sequencer 本身并不传递 transaction,因此 virutal sequencer 不需要和任何 driver 进行 TML 连接;但是用户需要在顶层的 connect_phase 中做好 virtual sequencer 中各 sequencer 句柄与底层 sequencer 实体的一一对接,避免句柄悬空。
如上图所示,包含了 virutal sequence / sequencer 以及 3 个 agent,其中 virutal sequence 中包含三个句柄/实例,并在其 body() 函数中对 3 个 sequence 通过 'uvm_do 宏发送。virutal sequencer 中仅包含 3 个句柄,其并不对这 3 个句柄进行实例化,而上通过 connect_phase() 指向 3 个 agent 中的对应 sequencer 实例,在此 virutal sequencer 相当于 sequence 和 sequencer 之间的一个中转路由器的作用,上图中红线部分用于完成 virtual sequencer 中各 sequencer 句柄与底层 sequencer 实体的一一对接。
6.2.virutal sequence/sequencer 实例
//step1: virtual sequence 实现
class vseq extends uvm_sequence;
`uvm_declare_p_sequencer(vseq);
clk_rst_seq seq_rst;
reg_cfg_seq seq_ral;
data_trans_seq seq_data;
...
`uvm_object_utils(vseq)
task body();
`uvm_do_on(seq_rst,p_sequencer.seqr_rst);
repeat(item_cnt) begin
`uvm_do_on(seq_rst,p_sequencer.seqr_ral);
`uvm_do_on(seq_rst,p_sequencer.seqr_data);
end
endtask
endclass
//step2: virtual sequencer 实现
class vseqr extends uvm_sequencer;
clk_rst_seqr seqr_rst;
reg_cfg_seqr seqr_ral;
data_trans_seqr seqr_data;
`uvm_component_utils(vseqr)
...
endclass
//step3: sequencer 连接
class env extends uvm_env;
vseqr my_vseqr;
`uvm_component_utils(env)
virtual function void build_phase(uvm_phase phase);
my_vseqr = vseqr::type_id::create("vseqr",this);
endfunction
virtual function void connect_phase(uvm_phase phase);
seqr_rst = clk_rst_agt.seqr_rst;
seqr_ral = reg_cfg_agt.seqr_ral;
seqr_data = data_trans_agt.seqr_data;
endfunction
endclass
//step4 vseq 启动
class my_test extends uvm_test;
`uvm_component_utils(my_test)
virtual function void build_phase(uvm_phase phase);
`uvm_config_db#(uvm_object_wrapper)::set(this,"env.vseqr.main_phase","default_sequence",vseq::get_type())
endfunction
endclass
上述代码中,步骤一,实现了 virtual sequence ,因为 3 个 sequence 的 sequencer 不同,需要采用 'uvm_do_on 宏 单独将对应的 sequence 挂载到对应的 sequencer 上;步骤三,实现了 vseq 中句柄与对应 agent 的 sequencer 实例连接;最后提一点 'uvm_declare_p_sequencer(vseq) 宏用于定义一个 p_sequencer 句柄,其类型为,当前 vseq 挂载的 vseqr 类型,'uvm_declare_p_sequencer(vseq)的代码原型为:
`define uvm_declare_p_sequencer(SEQUENCER)\
SEQUENCER p_sequencer;\
virtual function void m_set_p_sequencer();\
super.m_set_p_sequencer();
if(!cast(p_sequencer,m_sequencer))\
...
endfunction
6.3.m_sequencer Vs p_sequencer
a. sequencer 中自带的 default sequencer,指向当前 sequence 挂载的 sequencer,类型为 uvm_sequencer(m_sequencer 不能对指向的 vseqr 中的 seqr 句柄进行点操作)
b. p_sequencer 为 m_sequencer 经过 cast 后的指针,类型与当前 sequence 挂载的 sequencer一致。