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


sequence的其他问题

*心跳功能的实现

某些协议需要driver每隔一段时间向DUT发送一些类似心跳的信号。这些心跳信号的包与其他普通的包并没有本质区别,其使用的transaction也都是普通的transaction。

发送这种心跳包有两种选择,一种是在driver中实现,driver负责包的产生、发送

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

另一种是在sequence中实现,这个sequence被做成无限循环的sequence,它精确地计时,需要发送心跳包时,生成一个心跳包并发送出去

class heartbeat_sequence extends uvm_sequence #(my_transaction);
	virtual task body();
		while(1) begin
			#delay;
			`uvm_do(heartbeat_tr)
		end
	endtask
endclass

使用sequence的实现方式需要在sequence中引入时序,这可能会让只在sequence中写uvm_do宏的用户感觉相当不习惯。虽然代码使用了绝对延时,但一般最好不要使用绝对延时,而使用virtual sequence。一般在sequencer中通过config_db::get得到virtual sequence,在sequence中使用p_sequencer.vif的形式引用

virtual task body();
	my_transaction heartbeat_tr;
	while(1) begin
		repeat(10000) @(posedge p_sequencer.vif.clk);
		grab();
		`uvm_do(heartbeat_tr)
		ungrab();
	end
endtask

一个driver除发送心跳包之外还会发送其他包。这意味着要在这个driver相应sequener上启动多个sequence。在这些sequence产生的transaction中,心跳包优先级较高,当前正在发送的包在发送完成后应立即发送心跳包,所以在上述sequence中应使用grab功能

如果使用virtual sequence启动此sequence,需要使用fork join_none的方式:

class case0_vseq extends uvm_sequence #(my_transaction);
	…
	virtual task body();
		case0_sequence normal_seq;
		heartbeat_sequence heartbeat_seq;
		heartbeat_seq = new("heartbeat_seq");
		heartbeat_seq.starting_phase = this.starting_phase;
		fork
			heartbeat_seq.start(p_sequencer.p_sqr);
		join_none
		`uvm_do_on(normal_seq, p_sequencer.p_sqr)
	endtask
	…
endclass

normal_seq为另外一个启动的sequence,不能使用如下方式启动:

virtual task body();
	case0_sequence normal_seq;
	heartbeat_sequence heartbeat_seq;
	fork
		`uvm_do_on(heartbeat_seq, p_sequencer.p_sqr)
		`uvm_do_on(normal_seq, p_sequencer.p_sqr)
	join
endtask

因为心跳sequence是无限循环的。上述的启动方式会导致整个body无法停止

使用fork join_none形式启动心跳sequence的一个问题是driver可能正在发送一个心跳包,但此时virtual_sequence的objection被撤销了,main_phase停止,退出仿真。在某些DUT的实现中这种只发送一半的包是不允许的,可能导致最终检查结果异常。为避免这种情况出现,在心跳sequence中要发送transaction前raise objection,在发送完后drop objection

class heartbeat_sequence extends uvm_sequence #(my_transaction);
	…
	virtual task body();
		my_transaction heartbeat_tr;
		while(1) begin
			repeat(100) @(posedge p_sequencer.vif.clk);
			grab();
			starting_phase.raise_objection(this);
			`uvm_do_with(heartbeat_tr, {heartbeat_tr.pload.size == 50;})
			`uvm_info("hb_seq", "this is a heartbeat transaction", UVM_MEDIUM)
			starting_phase.drop_objection(this);
			ungrab();
		end
	endtask
	…
endclass

这种方式在启动时要谨记给此心跳sequence的starting_phase赋值

如果不使用手工方式启动此sequence,也可以使用default_sequence的方式启动。此时相当于my_sequencer上以两种不同方式启动了两个sequence:一是以default_sequence的形式启动,二是在virtual_sequence中启动。无论在sequence或在driver中实现心跳包的功能,都完全可以。由于心跳包要和其他包竞争driver,所以如果使用driver实现心跳包,则需手工实现仲裁功能。而如果在sequence中实现,由于UVM的sequence机制天生具有仲裁功能,可省略仲裁的代码。

在sequence中实现的另一好处是可以更容易控制心跳频率的改变。如测试一个心跳包异常的测试用例,使其每隔5个心跳包少发一个心跳包,只需要重写一个sequence即可。这个新sequence对老的心跳sequence没有任何影响,同时也不需要对driver进行任何变更。

sequence与driver共同组合用于控制激励源的发送。要控制某种特定激励源的发送时,这种控制功能既可由driver实现也可由sequence实现。某些情况可将driver的一些行为移到sequence实现,这会使验证平台的编写更简单。UVM提供强大的灵活性,同样一件事情可使用多种方式实现。

只将virtual_sequence设置为default_sequence

介绍cofig_db机制时曾介绍config_db机制的最大问题在于其set函数的第二个参数是一个字符串,而UVM本身不对这个字符串所代表的路径是否有效做任何检查。这会导致一些莫名其妙的问题。在引入了bus_agt后,假如在env中使用如下代码将其实例化:

virtual function void build_phase(uvm_phase phase);
	super.build_phase(phase);
	bus_agt = bus_agent::type_id::create("bus_agt", this);
	i_agt = my_agent::type_id::create ("i_agt ", this);
	… 
endfunction

这个实例化不存在任何问题并充分考虑到了代码的美观性,对双引号进行了对齐。在某个测试用例中可以使用如下方式分别为他们设置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.sqr.main_phase",
											"default_sequence", 
											case0_seq::type_id::get());
	uvm_config_db#(uvm_object_wrapper)::set(this, 
											"env.bus_agt.sqr.main_phase",
											"default_sequence", 	
											case0_bus_seq::type_id::get());
endfunction

运行上述测试用例发现bus_agt的sequencer上设置的default_sequence启动了,但i_agt的sequencer上设置的default_sequence没有启动。这是为什么?

这个bug非常隐蔽。当将bus_agt和i_agt实例化的时候,为了美观,在i_agt的名字后加了空格,而UVM将双引号之间的字符串都当做i_agt的名字

假如在i_agt.sqr中调用get_full_name函数,那么得到的结果如下:

`uvm_test_top.env.i_agt .sqr

可以看到空格是名字的一部分。这种bug让人防不胜防,运气好可能马上会发现这个bug,但如果不好可能要一两个小时才能发现。或许会说这一切都是由代码美观引起的。代码美观本身没有错,并不能因为这一处小小的bug而放弃对代码美观的追求。

真正的问题还在于config_db机制不对set函数的第二个参数提供检查。当config_db::set用的越多,这种bug出现的机率也就越大。因此应尽量避免config_db::set的使用。在本节中尽量少设置default_sequence,只将virtual sequence设置为default_sequence。

如果只将virtual sequence设置为default_sequence,那么所有的其他sequence都在其中启动。带来一个好处是向sequence传递参数更加方便。上面已经见识过config_db机制可能带来的隐患。

如果使用virtual sequence启动一个sequence,那么可使用如下方式为其赋值:

class case_vseq extends uvm_sequence;
	virtual task body();
		normal_seq nseq;
		nseq = new();
		nseq.xxx = yyy;
		nseq.start(p_sequencer.p_sqr)
	endtask
endclass

这很大程度避免了config_db的字符串引出的问题

disable fork语句对原子操作的影响

网络通信系统有各种计数器。通常其类型是W1C,即写1清零。那么对该计数器就存在如下情况:总线正在对其进行写清操作,同时DUT内部正在累加此计数器。这种极端情况可能导致计数器计数错误或直接挂起,后续完全无法再正常计数。因此需要对这种情况做测试。

在virtual sequence中开启如下两个进程:

class caw_vseq extends uvm_sequence;
	caw_seq demo_s;
	logic[31:0] rdata;
	virtual task body();
		uvm_status_e status;
		if(starting_phase != null)
			starting_phase.raise_objection(this);
		demo_s = caw_seq::type_id::create("demo_s");
		fork
			begin
				demo_s.start(p_sequencer.p_cp_sqr);
			end
			while(1) begin
				p_sequencer.p_rm.counter.write(status, 1, UVM_FRONTDOOR);
			end
		join_any
		disable fork;
		p_sequencer.p_rm.counter.read(status, rdata, UVM_FRONTDOOR);
		demo_s.start(p_sequencer.p_cp_sqr);
		p_sequencer.p_rm.counter.read(status, rdata, UVM_FRONTDOOR);
		if(starting_phase != null)
			starting_phase.drop_objection(this);
	endtask
endclass

上述代码看似解决了问题,但出现新情况是程序无法终止。此测试用例在运行disable fork语句之后读取计数器时会发现此寄存器正在写,于是一直等待。究其原因在于UVM的寄存器模型的write操作是原子操作,如果只使用disable fork语句野蛮终止,那么此原子操作尚未完成,于是虽然进程终止了,但其中一些原子操作标志位并没有清除,从而出现错误

正确的解决方法是:

class caw_vseq extends uvm_sequence;
	caw_seq demo_s;
	semaphore m_atomic = new(1); //notice
	logic[31:0] rdata;
	virtual task body();
		uvm_status_e status;
		if(starting_phase != null)
			starting_phase.raise_objection(this);
		demo_s = caw_seq::type_id::create("demo_s");
		fork
			begin
				demo_s.start(p_sequencer.p_cp_sqr);
				m_atomic.get(1);
			end
			while(1) begin
				if(m_atomic.try_get(1)) begin
					p_sequencer.p_rm.counter.write(status, 1, UVM_FRONTDOOR);
					m_atomic.put(1);
				end
				else begin
					break;
				end
			end
		join
		p_sequencer.p_rm.counter.read(status, rdata, UVM_FRONTDOOR);
		demo_s.start(p_sequencer.p_cp_sqr);
		p_sequencer.p_rm.counter.read(status, rdata, UVM_FRONTDOOR);
		if(starting_phase != null)
			starting_phase.drop_objection(this);
	endtask
endclass

通过使用semaphore,每次写counter寄存器之前都会试图从semaphore中得到一个键值,如果无法得到则表示另外一个进程(demo_s进程)已执行完毕,此时while循环没有必要进行下去,直接终止。

DUT参数的随机化

验证有两大问题:一是向DUT灌输不同的激励,二是为DUT配置不同的参数。前面一直在介绍如何发送不同的激励,本节介绍如何在UVM中为DUT配置不同的参数。

使用寄存器模型随机化参数

前面曾介绍过可以使用寄存器模型的随机化及update来为DUT选择一组随机化的参数

assert(p_rm.randomize());
p_rm.updata(status, UVM_FRONTDOOR);

上述方式随机化的参数可能是任意组合。但很多情况下用户希望的是一种特定的组合。如对于一个压缩算法来说它可以有有损压缩、无损压缩及不压缩三种模式。在建立测试用例时需要为这个算法模块至少建立四个测试用例:有损压缩的、无损压缩的、不压缩的及以上三种随机组合的。在建立前三个测试用例时,需要将参数随机化的范围缩小。

如何缩小随机化的范围?这里提供三种方式:一是只将需要随机化的寄存器调用randomize函数,其他不调用。在调用时指定约束:

assert(p_rm.reg1.randomize() with { reg_data.value == 5'h3;});
assert(p_rm.reg2.randomize() with { reg_data.value >= 7'h9;});

二是在调用整体的randomize函数时为需要指定参数的寄存器指定约束

assert(p_rm.randomize() with {reg1.reg_data.value == 5'h3; reg2.reg_data.value >= 7'h9});

第三种方式是借助factory机制的重载功能,从需要随机的寄存器中派生新类,在新类中指定约束,最后再使用重载替换掉原先寄存器模型中相应的寄存器

class dreg1 extends my_reg1;
	constraint{
		reg_data.value == 5'h3;
	}
endclass
class dreg2 extends my_reg2;
	constraint{
		reg_data.value >= 7'h9;
	}
endclass

*使用单独的参数类

上节提供了使用寄存器模型来随机化DUT参数的方式。考虑需要一种跨越寄存器的约束,如需要寄存器a中的field0的值与寄存器b中field0的值的和大于100。上节介绍的三种方式中,只有第二种能实现

assert(p_rm.randomize() with {rega.field0.value + regb.field0.value >=100;});

由于这个约束对所有测试用例都适用,因此期望它能够写在寄存器模型的constraint里

class reg_model extends uvm_reg_block;
	constraint reg_ab_cons{
		rega.field0.value + regb.field0.value >=100;
	}
endclass

寄存器模型如果是自己手工创建的,那么在其中加入constraint没有任何问题。但通常寄存器模型都是由一些脚本命令自动创建。验证平台中需要用到寄存器的地方有如下三个,一是RTL代码中,二是SV中,三是C语言中必须时刻保持这三处的寄存器完全一致。当一处更新时,其他两处必须相应更新。寄存器成百上千个,如果全部手工来做将会非常耗费时间和精力。因此一般会将寄存器的描述放在一个源文件中,如word、excel、xml文档中,然后使用脚本从中提取寄存器信息并分别生成相应的RTL代码、UVM中及C语言中的寄存器模型。当寄存器更新时只更新源文件即可,其他的可以自动更新。这种方式省时省力,是主流的方式。

使用脚本创建寄存器模型的情况下,在寄存器模型中加入constraint比较困难。因为很难在源文件(如word文档等)中描述约束,尤其是存在跨寄存器的约束时。所以很多生成寄存器模型的工具并不支持约束。

为解决这个问题,可针对DUT中需随机化的参数建立一个dut_parm类,并在其中指定默认约束

class dut_parm extends uvm_object;
	reg_model p_rm;
	…
	rand bit[15:0] a_field0;
	rand bit[15:0] b_field0;
	constraint ab_field_cons{
		a_field0 + b_field0 >= 100;
	}
	task update_reg();
		p_rm.rega.write(status, a_field0, UVM_FROTDOOR);
		p_rm.regb.write(status, b_field0, UVM_FROTDOOR);
	endtask
endclass

这段代码中指定了一个update_reg任务,用于当参数随机化完成后把相关的参数更新到DUT中。

在virtual sequence中可以实例化这个新类,随机化并调用update_reg任务:

class case0_cfg_vseq extends uvm_sequence;
	…
	virtual task body();
		dut_parm pm;
		pm = new("pm");
		assert(pm.randomize());
		pm.p_rm = p_sequencer.p_rm;
		pm.update_reg();
	endtask
	…
endclass

这种专门的参数类的形式在跨寄存器的约束较多时特别有用。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值