UVM实战 卷I学习笔记13——UVM高级应用(2)


layer sequence

*复杂sequence的简单化

在网络传输中,以太网包是最底层的包,其上还有IP包、UDP包、TCP包等。现在只考虑IP包与mac包。my_transaction(mac包)在前文中已经定义过了,下面给出IP包的定义:

class ip_transaction extends uvm_sequence_item;
	//ip header
	rand bit [3:0] version;//protocol version
	rand bit [3:0] ihl;// ip header length
	rand bit [7:0] diff_service; // service type, tos(type of service)
	rand bit [15:0] total_len;// ip telecom length, include payload, byte
	rand bit [15:0] iden;//identification
	rand bit [2:0] flags;//flags
	rand bit [12:0] frag_offset;//fragment offset
	rand bit [7:0] ttl;// time to live
	rand bit [7:0] protocol;//protocol of data in payload
	rand bit [15:0] header_cks;//header checksum
	rand bit [31:0] src_ip; //source ip address
	rand bit [31:0] dest_ip;//destination ip address
	rand bit [31:0] other_opt[];//other options and padding
	rand bit [7:0] payload[];//data
	…
endclass

在以太网的发送中,IP包整体被作为mac包的负荷。要求在发送的mac包中指定IP地址等数据,这需要约束mac包pload的值

virtual task body();
	my_transaction m_tr;
	repeat (10) begin
		ip_tr.dest_ip == 'h10000;})
		m_tr = new("m_tr");
		assert(m_tr.randomize());
		{m_tr.pload[15], m_tr.pload[14], m_tr.pload[13], m_tr.pload[12]} == 32'h9999;
		{m_tr.pload[19], m_tr.pload[18], m_tr.pload[17], m_tr.pload[16]} == 32'h10000;
		`uvm_send(m_tr)
	end
	#100;
endtask

在ip_transaction如此多的域中,如果要对其中某一项进行约束,那么需要仔细计算每一项在my_transaction的pload中的位置,稍微不小心就很容易搞错。如果需要约束多项,那更加麻烦。既然定义了ip_transaction,这个过程完全可以简化:

virtual task body();
	my_transaction m_tr;
	ip_transaction ip_tr;
	byte unsigned data_q[];
	int data_size;
	repeat (10) begin
		ip_tr = new("ip_tr");
		assert(ip_tr.randomize() with {ip_tr.src_ip=='h9999; ip_tr.dest_ip=='h10000;})
		ip_tr.print();
		data_size = ip_tr.pack_bytes(data_q) / 8;
		m_tr = new("m_tr");
		assert(m_tr.randomize with{m_tr.pload.size() == data_size;});
		for(int i = 0; i < data_size; i++) begin
			m_tr.pload[i] = data_q[i];
		end
		`uvm_send(m_tr)
	end
	#100;
endtask

先将ip_tr实例化并调用randomize令其随机化,随机化时施加约束。随机完成后使用pack_bytes函数将所有数据打包成一个动态数组作为my_transaction的pload。这个过程比前面的简单多了,但这样的代码可重用性不高。假如现在要测一种CRC错误的情况,那么需要将上述代码改写为:

virtual task body();
	my_transaction m_tr;
	ip_transaction ip_tr;
	byte unsigned data_q[];
	int data_size;
	repeat (10) begin
		ip_tr = new("ip_tr");
		assert(ip_tr.randomize() with {ip_tr.src_ip == 'h9999; ip_tr.dest_ip = = 'h10000;})
		ip_tr.print();
		data_size = ip_tr.pack_bytes(data_q) / 8;
		m_tr = new("m_tr");
30		assert(m_tr.randomize with{m_tr.pload.size() == data_size; m_tr.crc_er r == 1});
		for(int i = 0; i < data_size; i++) begin
			m_tr.pload[i] = data_q[i];
		end
		`uvm_send(m_tr)
	end
	#100;
endtask

上述代码只改变代码的第30行,与ip_transaction相关部分没有变,变的只是my_transaction部分。同样的,如果要施加给DUT IP checksum错误的包:

virtual task body();
	my_transaction m_tr;
	ip_transaction ip_tr;
	byte unsigned data_q[];
	int data_size;
	repeat (10) begin
		ip_tr = new("ip_tr");
		assert(ip_tr.randomize() with {ip_tr.src_ip == 'h9999; ip_tr.dest_ip = = 'h10000;})
		ip_tr.header_cks = $urandom_range(10000, 0);
		ip_tr.print();
		data_size = ip_tr.pack_bytes(data_q) / 8;
		m_tr = new("m_tr");
		assert(m_tr.randomize with{m_tr.pload.size() == data_size;});
		for(int i = 0; i < data_size; i++) begin
			m_tr.pload[i] = data_q[i];
		end
		`uvm_send(m_tr)
	end
	#100;
endtask

这里只是对代码插入一句对header_cks赋随机值的语句。与mac相关的代码不需要做任何变更。

上面三段代码几乎完全相同,但却是不同的测试用例。同样代码在不同地方出现,这非常不合理。要提高代码的可重用性,一种办法是将与ip相关的代码写成一个函数,而与mac相关的代码写成另一个函数,将这些基本函数放在base_sequence中,新建测试用例时从base_sequence派生新的sequence,并调用之前写好的函数

另一种办法是使用layer sequence。前面代码中同一个sequence中产生两种不同的transaction,虽然这两种transaction之间有必然的联系(ip_transaction作为my_transaction的pload),但将它们放在一起并不合适。最好将它们分离,一个sequence负责产生ip_transaction,另一个负责产生my_transaction,前者将产生的ip_transaction交给后者。这就是layer sequence。

layer sequence的示例

产生ip_transaction的sequence如下:

class ip_sequence extends uvm_sequence #(ip_transaction);
	…
	virtual task body();
		ip_transaction ip_tr;
		repeat (10) begin
			`uvm_do_with(ip_tr, {ip_tr.src_ip == 'h9999; ip_tr.dest_ip == 'h10000;})
		end
		#100;
	endtask
	`uvm_object_utils(ip_sequence)
endclass

其相应的sequencer如下:

class ip_sequencer extends uvm_sequencer #(ip_transaction);
	function new(string name, uvm_component parent);
		super.new(name, parent);
	endfunction
	`uvm_component_utils(ip_sequencer)
endclass

这个sequencer需要在my_agent中实例化,在这种情况下my_agent中有两个sequencer:

function void my_agent::build_phase(uvm_phase phase);
	super.build_phase(phase);
	if (is_active == UVM_ACTIVE) begin
		ip_sqr = ip_sequencer::type_id::create("ip_sqr", this);
		sqr = my_sequencer::type_id::create("sqr", this);
		drv = my_driver::type_id::create("drv", this);
	end
	mon = my_monitor::type_id::create("mon", this);
endfunction

要使用layer sequence,最关键的问题是如何将ip_transaction能够交给产生my_transaction的sequence。由于ip_transaction是由一个sequence产生的,模仿driver从sequencer获取transaction的方式,在my_sequencer中加入一个端口,并将其实例化:

class my_sequencer extends uvm_sequencer #(my_transaction);
	uvm_seq_item_pull_port #(ip_transaction) ip_tr_port;
	…
	function void build_phase(uvm_phase phase);
		super.build_phase(phase);
		ip_tr_port = new("ip_tr_port", this);
	endfunction
	…
endclass

在my_agent中将这个端口和ip_sqr的相关端口连接在一起:

function void my_agent::connect_phase(uvm_phase phase);
	super.connect_phase(phase);
	if (is_active == UVM_ACTIVE) begin
		drv.seq_item_port.connect(sqr.seq_item_export);
		sqr.ip_tr_port.connect(ip_sqr.seq_item_export);
	end
	ap = mon.ap;
endfunction

之后在产生my_transaction的sequence中:

class my_sequence extends uvm_sequence #(my_transaction);
	…
	virtual task body();
		my_transaction m_tr;
		ip_transaction ip_tr;
		byte unsigned data_q[];
		int data_size;
		while(1) begin
			p_sequencer.ip_tr_port.get_next_item(ip_tr);
			data_size = ip_tr.pack_bytes(data_q) / 8;
			m_tr = new("m_tr");
			assert(m_tr.randomize with{m_tr.pload.size() == data_size;});
			for(int i = 0; i < data_size; i++) begin
				m_tr.pload[i] = data_q[i];
			end
			`uvm_send(m_tr)
			p_sequencer.ip_tr_port.item_done();
		end
	endtask
	`uvm_object_utils(my_sequence)
	`uvm_declare_p_sequencer(my_sequencer)
endclass

由于要用到sequencer中的ip_tr_port,所以要使用declare_p_sequencer宏声明sequencer。该sequence被做成一个无限循环的sequence,因为它需要时刻从ip_tr_port得到新的ip_transaction,这类似于driver中的无限循环。由于设置了无限循环,所以不能在其中提起或者撤销objection。objection要在ip_sequence中控制。

之后需要启动这两个sequence。可以使用default_sequence的形式

function void my_case0::build_phase(uvm_phase phase);
	super.build_phase(phase);
	uvm_config_db#(uvm_object_wrapper)::set(this, "env.i_agt.ip_sqr.main_phase",
										"default_sequence", ip_sequence::type_id::get());
	uvm_config_db#(uvm_object_wrapper)::set(this, "env.i_agt.sqr.main_phase",
										"default_sequence", my_sequence::type_id::get());
endfunction

也可以使用default_sequence的形式,前提是vsqr中已经有成员变量指向相应的sequencer

class case0_vseq extends uvm_sequence;
	virtual task body();
		ip_sequence ip_seq;
		my_sequence my_seq;
		fork
			`uvm_do_on(my_seq, p_sequencer.p_my_sqr)
		join_none
		`uvm_do_on(ip_seq, p_sequencer.p_ip_sqr)
	endtask
endclass

当后面构建CRC错误包的激励时只需要建立crc_sequence,并在my_sequecer上动。此时ip_sequencer上依然是ip_sequence,不受影响。

需要构建checksum错误的激励时,也只需要建立cks_err_seq,并在ip_sequencer上启动,此时my_sequencer上启动的是my_sequence,不受影响。

layer sequence对于初学者来说会比较复杂。上一节中layer sequence只是解决问题的一种策略,另一种策略是在base_sequence中写函数/任务。这个例子相比base_sequence,layer sequence并没有明显优势。但当问题非常复杂时,layer sequence会逐渐体现出其优势。大型验证平台中layer sequence的应用非常多。

layer sequence与try_next_item

上节中, 最终的my_driver使用get_next_item从my_sequencer中得到数据:

task my_driver::main_phase(uvm_phase phase);while(1) begin
		seq_item_port.get_next_item(req);
		drive_one_pkt(req);
		seq_item_port.item_done();
	end
endtask

前面曾提过与get_next_item相比,try_next_item更加接近实际情况。在实际应用中try_next_item用得更多。现在将get_next_item改为try_next_item:

task my_driver::drive_idle();
	`uvm_info("my_driver", "item is null", UVM_MEDIUM)
	@(posedge vif.clk);
endtask
task my_driver::main_phase(uvm_phase phase);while(1) begin
		seq_item_port.try_next_item(req);
		if(req == null) begin
			drive_idle();
		end
		else begin
			`uvm_info("my_driver", "get one pkt", UVM_MEDIUM)
			drive_one_pkt(req);
			seq_item_port.item_done();
		end
	end
endtask

重新运行上节例子会发现在前后两个有效的req之间,my_driver总会打印一句“item is null”,说明driver没有得到transaction:

UVM_INFO my_driver.sv(39) @ 81100000: uvm_test_top.env.i_agt.drv [my_driver] get one pkt
UVM_INFO my_driver.sv(24) @ 166300000: uvm_test_top.env.i_agt.drv [my_driver] item is null
UVM_INFO my_driver.sv(39) @ 166500000: uvm_test_top.env.i_agt.drv [my_driver] get one pkt

当my_driver没有得到transaction,它只是等待一个时钟,相当于空闲一个时钟。在某些协议中除非故意出现空闲,否则这样正常的驱动数据中出现的空闲将会导致时序错误。避免这个问题的一个办法是不用try_next_item,而使用get_next_item。但正如一开始说的,try_next_item更接近真实的情况。使用get_next_item有两个问题

  1. 某些协议并不是上电复位后马上开始发送正常数据,而是开始发送一些空闲数据sequence,这些数据有特定的要求,并非一成不变。当空闲数据sequence发送完毕后经过某些交互开始发送正常数据。开始发送正常数据后就不能再在正常数据中间插入空闲数据。这种情况如果使用get_next_item,那么将难以处理在上电复位后要求发送的空闲数据sequence。
  2. 当drop_objection后drain_time的时间也要求发送空闲数据sequence。但此时sequence已不提供transaction了,所以my_driver无法按照要求驱动这些空闲数据sequence。

所以还是应该使用try_next_item。但前面ip_sequence与my_sequence的代码没有插入任何时延,所以my_driver应一直得到有效的req,不应出现得不到transaction的情况。那问题出在什么地方?

SV是按照事件驱动进行仿真的。每时刻有很多事件,为处理这些事件,SV使用时间槽管理它们。

如下图,时间轴上方为n时刻的部分时间槽,下方为n+1时刻的部分时间槽。n时刻的时间槽p中,driver驱动数据调用item_done,以及调用try_next_item试图获取下个transaction。my_sequencer一方面使try_next_item等待一个时间槽,另一方面将item_done转发给my_sequence(事实上并非简单的转发,而是通知my_sequence当前的transaction已经被driver驱动完毕,可以产生下一个transaction,为了方便可认为转发item_done)。

这里为什么要令try_next_item等待一个时间槽呢?因为my_sequence收到item_done的信息,向其sequencer递交产生下个transaction请求,及最后生成transaction交给sequencer需要时间来完成。my_sequence收到item_done后,也向ip_sequencer发出item_done信息,并使用get_next_item获取下一个item。ip_sequencer把item_done转发给ip_sequence。ip_sequence收到item_done后,结束上一个uvm_do,开始下一个uvm_do产生新item。上述这一切都是在时间槽p中完成的。
图1
在时间槽p结束时(或是时间槽p+1开始时),ip_sequencer的item缓存中已经有数据了,此时my_sequence的get_next_item得到了数据。但此时my_sequencer的item缓存中依然为空。driver发出的try_next_item在这个时间槽发现my_sequencer的item缓存为空,于是直接返回null,driver得到null后开始drive_idle,即等待下一个时钟的上升沿。

时间槽p+1结束时(或是时间槽p+2开始时),my_sequence已将生成的数据送入my_sequencer的item缓存了。但此时driver并没有向my_sequencer索要数据,而是处于@(posedge clk)的状态。

n+1时刻下个时钟上升沿到来。n+1时刻的时间槽1,driver开始try_next_item。此时my_sequencer收到了这个请求,虽然此时它的缓存中是非空的,但依然让try_next_item等待一个时间槽。在n+1时刻的时间槽2, driver的try_next_item如愿得到了想要的transaction。

上述过程可看出主要问题是n时刻时driver的try_next_item调用过早。如果不是在时间槽p调用,而是在时间槽p+1调用,那么在时间槽p+2时,try_next_item就可以由my_sequencer的item缓存中得到transaction了。在UVM中可以通过调用任务uvm_wait_for_nba_region来实现:

task my_driver::drive_idle();
	`uvm_info("my_driver", "item is null", UVM_MEDIUM)
	@(posedge vif.clk);
endtask
task my_driver::main_phase(uvm_phase phase);while(1) begin
		uvm_wait_for_nba_region();
		seq_item_port.try_next_item(req);
		if(req == null) begin
			dirve_idle();
		end
		else begin
			drive_one_pkt(req);
			seq_item_port.item_done();
		end
	end
endtask

*错峰技术的使用

上节通过增加uvm_wait_for_nba_region的方式能够解决问题,但它并非完美的解决方案。假如上述layer sequence又多了一层,如下图所示:
在这里插入图片描述

这种情况下从udp_seq发出item到driver的try_next_item能够检测到需要2个时间槽的延时。只增加一个uvm_wait_for_nba_region是没有用处的,需要再增加一个。当layer sequence的层数再增加时,相应的也需要再增加。这种解决方案显得非常low

上述问题的关键在于item_done和try_next_item是在同一时刻被调用,这导致了时间槽的竞争。如果能够将它们错开调用,那么这个问题也将不会是问题:

task my_driver::drive_idle();
	`uvm_info("my_driver", "item is null", UVM_MEDIUM)
endtask
task my_driver::main_phase(uvm_phase phase);while(1) begin
		@(posedge vif.clk);
		seq_item_port.try_next_item(req);
		if(req == null) begin
			drive_idle();
		end
		else begin
			drive_one_pkt(req);
			seq_item_port.item_done();
		end
	end
endtask

在item_done被调用后并非立即调用try_next_item,而是等待下一时钟上升沿到来后再调用。这种情况下会变为下图所示形式:
在这里插入图片描述
在这里插入图片描述

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值