UVM实战 卷I学习笔记12——UVM中代码的可重用性(1)


callback机制

在UVM验证平台中,callback机制的最大用处就是提高验证平台的可重用性。很多情况下验证人员期望在一个项目中开发的验证平台能够用于另外一个项目。但通常来说完全的重用是比较难实现的,两个项目之间或多或少会有一些差异。如果把两个项目不同的地方使用callback函数来做,而把相同的地方写成一个完整的env,这样重用时只要改变相关callback函数env可完全的重用。

除了提高可重用性外,callback机制还用于构建异常的测试用例。只是在UVM中构建异常的测试用例有很多种方式,如factory机制的重载,callback机制只是其中的一种。

广义的callback函数

前文介绍my_transaction时,曾经在其post_randomize中调用calc_crc函数:

function void post_randomize();
	crc = calc_crc;
endfunction

my_transaction最后一个字段是CRC校验信息。假如没有post_randomize(),那么CRC必须在整个transaction的数据都固定之后才能计算出来。

my_transaction tr;
assert(tr.randomize());
tr.crc = tr.calc_crc();

执行前两句之后,tr中的crc字段的值是一个随机的值,要将其设置成真正反映这个transaction数据的CRC信息,需要在randoimize()之后调用一个calc_crc,calc_crc是一个自定义的函数。

调用calc_crc的过程有点繁琐,因为每次执行randomize函数之后都要调用一次,如果忘记调用,将很可能成为验证平台的一个隐患,非常隐蔽且不易发现。期望有一种方法能够在执行randomize函数之后自动调用calc_crc函数。randomize是SV提供的函数,同时还提供一个post_randomize()函数,randomize()之后系统会自动调用post_randomize函数,如上三行代码在执行时实际如下:

my_transaction tr;
assert(tr.randomize());
tr.post_randomize();
tr.crc=tr.calc_crc();

其中tr.post_randomize是自动调用的,所以如果能够重载post_randomize函数,在其中执行calc_crc函数,那么就可以达到目的了:

function void my_transaction::post_randomize();
	super.post_randomize();
	crc=this.calc_crc();
endfunction

post_randomize就是SV提供的广义的callback函数,也是最简单的callback函数。

post_randomize的例子似乎与前面提到callback机制不同,前面强调两个项目之间。不过如果将SV语言的开发过程作为一个项目A,验证人员使用SV开发的是项目B。A的开发者预料到B可能会在randomize函数完成后做一些事情,于是A添加了post_randomize函数。B如A所料,使用了这个callback函数。

UVM为用户提供了广义的callback函数/任务:pre_body和post_body,除此之外还有pre_do、mid_do和post_do

callback机制的必要性

程序是固定的,其设计者有时不是使用者,所以作为使用者来说总希望程序的设计者能够提供一些接口满足自己的应用需求。作为这两者之间的协调,callback机制出现了。如上面例子,如果SV的设计者一意孤行只提供randomize函数,此函数执行完成之后就完成任务不做任何事情。幸运的是他听取意见加入了一个post_randomize的callback函数,这样可以使用户实现各自的想法。

由上例可以看出:

  1. 程序的开发者其实是不需要callback机制的,它完全是由程序的使用者要求的。
  2. 程序的开发者必须能够准确地获取使用者的需求,知道使用者希望在程序的什么地方提供callback函数接口,如果无法获取使用者的需求,那么程序的开发者只能尽可能地预测使用者的需求。

对于VIP(Verification Intellectual Property)来说,一个很容易预测到的需求是在driver中,在发送transaction之前,用户可能会针对transaction做某些动作,因此应该提供一个pre_tran的接口,如用户A可能在pre_tran中将要发送内容的最后4个字节设置为发送的包的序号,这样在包出现比对错误时可以快速地定位,B用户可能在整个包发送之前先在线路上发送几个特殊的字节,C用户可能将整个包的长度截去一部分……总之不同的用户会有不同的需求。正是callback机制的存在满足了这种需求,扩大了VIP的应用范围。

除上述情形外,还存在构建异常测试用例的需求。前面已经展示过多种构建异常测试用例的方式。如果在driver中实现测试用例,那么需要使用多个分支处理这些异常情况。在有callback机制的情况下,把异常测试用例的代码使用callback函数实现,而正常测试用例则正常处理。使用这种方式可以让driver的代码非常简洁。在没有factory机制的重载功能之前,使用callback函数构建异常测试用例是最好的实现方式

UVM中callback机制的原理

前面讲述了广义上的callback函数。但callback对UVM来说有其特定的含义。考虑如下的callback函数/任务:

task my_driver::main_phase();while(1) begin
		seq_item_port.get_next_item(req);
		pre_tran(req);
		…
	end
endtask

假设这是一个成熟VIP中的driver,那么考虑如何实现pre_tran的callback函数/任务呢?它应该是my_driver的一个函数/任务。如果按照上面post_randomize的经验,那么应该从my_driver派生一个类new_driver,然后重写pre_tran这个函数/任务。

但这种想法是行不通的,因为这是一个完整的VIP,虽然从my_driver派生了new_driver,但这个VIP中正常运行时使用的依然是my_driver,而不是new_driver。new_driver这个派生类根本就没有实例化过,所以pre_tran从来不会运行。当然,可以使用factory机制的重载功能,但那样是factory机制的功能,而不是callback机制的功能,所以暂不考虑factory机制的重载功能

为解决这个问题,尝试新引入一个类:

task my_driver::main_phase();while(1) begin
		seq_item_port.get_next_item(req);
		A.pre_tran(req);
		…
	end
endtask

这样可以避免重新定义一次my_driver,只需要重新定义A的pre_tran即可。重新派生A的代价是要远小于my_driver的。

使用时只要从A派生一个类并将其实例化,然后重新定义其pre_tran函数,此时callback机制的目的就达到了。虽然看起来似乎一切顺利,但实际却忽略了一点。因为从A派生了一个类并实例化,但作为my_driver来说,怎么知道A派生了一个类呢?又怎么知道A实例化了呢?为应付这个问题,UVM中又引入了一个类,假设这个类称为A_pool,意思就是专门存放A或者A的派生类的一个池子UVM约定会执行这个池子中所有实例的pre_tran函数/任务,即:

task my_driver::main_phase();while(1) begin
		seq_item_port.get_next_item(req);
		foreach(A_pool[i]) begin
			A_pool[i].pre_tran(req);
		end
		…
	end
endtask

这样在使用时,只要从A派生一个类并将其实例化,然后加入到A_pool中,那么系统运行到上面的foreach(A_pool[i])语句时,将会知道加入了一个实例,于是就会调用其pre_tran函数/任务

有了A和A_pool,真正的callback机制就可以实现了。UVM中的callback机制与此类似,不过其代码实现非常复杂。

*callback机制的使用

要实现真正的pre_tran,需要首先定义上节所说的类A:

class A extends uvm_callback;
	virtual task pre_tran(my_driver drv, ref my_transaction tr);
	endtask
endclass

A类一定要从uvm_callback派生,另外还需要定义一个pre_tran的任务,此任务的类型一定要是virtual的,因为从A派生的类需要重载这个任务。

接下来声明一个A_pool类:

typedef uvm_callbacks#(my_driver, A) A_pool;

A_pool的声明相当简单,只需要一个typedef语句即可。另外在这个声明中除了要指明这是一个A类型的池子外,还要指明这个池子将会被哪个类使用。本例中my_driver将会使用这个池子,所以要将此池子声明为my_driver专用的。之后,在my_driver中要做如下声明

typedef class A;
class my_driver extends uvm_driver#(my_transaction);
	…
	`uvm_component_utils(my_driver)
	`uvm_register_cb(my_driver, A)
	…
endclass

这个声明与A_pool的类似,要指明my_driver和A。在my_driver的main_phase中调用pre_tran时并不如上节所示那么简单,而是调用一个宏来实现:

task my_driver::main_phase(uvm_phase phase);while(1) begin
		seq_item_port.get_next_item(req);
		`uvm_do_callbacks(my_driver, A, pre_tran(this, req))
		drive_one_pkt(req);
		seq_item_port.item_done();
	end
endtask

uvm_do_callbacks宏的第一个参数是调用pre_tran的类的名字,这里自然是my_driver,第二个参数是哪个类具有pre_tran,这里是A,第三个参数是调用的是函数/任务,这里是pre_tran,指明是pre_tran时要顺便给出pre_tran的参数

到目前为止是VIP的开发者应该做的事情,作为使用VIP的用户来说,需要做如下事情:

首先从A派生一个类

class my_callback extends A;
	virtual task pre_tran(my_driver drv, ref my_transaction tr);
		`uvm_info("my_callback", "this is pre_tran task", UVM_MEDIUM)
	endtask
	`uvm_object_utils(my_callback)
endclass

其次,在测试用例中将my_callback实例化,并将其加入A_pool中

function void my_case0::connect_phase(uvm_phase phase);
	my_callback my_cb;
	super.connect_phase(phase);
	my_cb = my_callback::type_id::create("my_cb");
	A_pool::add(env.i_agt.drv, my_cb);
endfunction

my_callback的实例化是在connect_phase中完成的,实例化完成后需要将my_cb加入A_pool中。同时在加入时需要指定是给哪个my_driver使用的。因为很可能整个base_test中实例化了多个my_env,从而有多个my_driver的实例,所以要将my_driver的路径作为add函数的第一个参数

至此,一个简单的callback机制示例就完成了。这个示例几乎涵盖UVM中所有可能用到的callback机制的知识,大部分callback机制的使用都与这个例子相似。

总结:对于VIP的开发者来说,预留一个callback函数/任务接口时需要做以下几步:

  • 定义一个A类
  • 声明一个A_pool类
  • 在要预留callback函数/任务接口的类中调用uvm_register_cb宏
  • 在要调用callback函数/任务接口的函数/任务中,使用uvm_do_callbacks宏

对于VIP的使用者来说,需要做如下几步:

  • 从A派生一个类,在这个类中定义好pre_tran
  • 在测试用例的connect_phase(或其他phase,但一定要在使用此callback函数/任务的phase之前)中将从A派生的类实例化,并将其加入A_pool中

本节的my_driver是自己写的,my_case0也是自己写的。完全不存在VIP与VIP使用者的情况。不过换个角度来说,可能有两个验证人员共同开发一个项目,一个负责搭建测试平台(testbench)及my_driver等的代码,另一位负责创建测试用例。负责搭建测试平台的人员为搭建测试用例的人员留下了callback函数/任务接口。即使my_driver与测试用例都由同一个人来写,也完全可以接受。

因为不同测试用例肯定会引起不同driver的行为。这些不同的行为差异可以在sequence中实现,也可在driver中实现。在driver中实现时既可以用driver的factory机制重载,也可使用callback机制。

*子类继承父类的callback机制

考虑如下情况:某公司有前后两代产品。第一代产品已经成熟,有一个已经搭建好的验证平台,要在此基础上开发第二代产品,需搭建一个新验证平台。这个新验证平台大部分与旧验证平台一致,只是需要扩展my_driver的功能,即需要从原来的driver中派生一个新的类new_driver。

另外需保证第一代产品的所有测试用例在尽量不改动的前提下能在新验证平台上通过。第一代产品的测试用例中大量使用了callback机制。由于callback池(即A_pool)在声明时指明了这个池子只能装载用于my_driver的callback。那怎样才能使原来的callback函数/任务能用于new_driver中呢?

这就牵扯到子类继承父类的callback函数/任务问题。my_driver使用上节中的定义,在此基础上派生新的类new_driver:

class new_driver extends my_driver;
	`uvm_component_utils(new_driver)
	`uvm_set_super_type(new_driver, my_driver)
	…
endclass
task new_driver::main_phase(uvm_phase phase);while(1) begin
		seq_item_port.get_next_item(req);
		`uvm_info("new_driver", "this is new driver", UVM_MEDIUM)
		`uvm_do_callbacks(my_driver, A, pre_tran(this, req))
		drive_one_pkt(req);
		seq_item_port.item_done();
	end
endtask

这里使用uvm_set_super_type宏,把子类和父类关联在一起。其第一个参数是子类,第二个参数是父类

在main_phase中调用uvm_do_callbacks宏时,其第一个参数是my_driver而不是new_driver,即调用方式与在my_driver中一样。

在my_agent中实例化此new_driver:

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

这样,上节的my_case0不用经过任何修改就可以在新的验证平台上通过。

使用callback函数/任务来实现所有的测试用例

可以在pre_tran中做很多事情,那么是否可以将driver中的drive_one_pkt也移到pre_tran中呢?答案是可以的。更进一步,将seq_item_port.get_nex_item移到pre_tran中也是可以的。

其实完全可以不用sequence,只用callback函数/任务就可实现所有测试用例。假设A类定义如下:

class A extends uvm_callback;
	my_transaction tr;
	virtual function bit gen_tran();
	endfunction
	virtual task run(my_driver drv, uvm_phase phase);
		phase.raise_objection(drv);
		drv.vif.data <= 8'b0;
		drv.vif.valid <= 1'b0;
		while(!drv.vif.rst_n)
			@(posedge drv.vif.clk);
		while(gen_tran()) begin
			drv.drive_one_pkt(tr);
		end
		phase.drop_objection(drv);
	endtask
endclass

在my_driver的main_phase中去掉所有其他代码,只调用A的run:

task my_driver::main_phase(uvm_phase phase);
	`uvm_do_callbacks(my_driver, A, run(this, phase))
endtask

在建立新的测试用例时,只需要从A派生一个类,并重载其gen_tran函数:

class my_callback extends A;
	int pkt_num = 0;
	virtual function bit gen_tran();
		`uvm_info("my_callback", "gen_tran", UVM_MEDIUM)
		if(pkt_num < 10) begin
			tr = new("tr");
			assert(tr.randomize());
			pkt_num++;
			return 1;
		end
		else
			return 0;
	endfunction
	`uvm_object_utils(my_callback)
endclass

这种情况下新建测试用例相当于重载gen_tran。如果不满足要求,还可以将A类的run任务重载。

在这个示例中完全丢弃了sequence机制,在A类的run任务中进行控制objection,激励产生在gen_tran中。

callback机制、 sequence机制和factory机制

上一节使用callback函数/任务实现所有测试用例,几乎完全颠覆从头到尾一直在强调的sequence机制。前面见识到了使用factory机制重载driver来实现所有测试用例的情况。

callback机制、sequence机制和factory机制在某种程度上来说很像: 它们都能实现搭建测试用例的目的。只是sequence机制是UVM一直提倡的生成激励的方式,UVM为此做了大量的工作,如构建了许多宏、嵌套的sequence、virtual sequence、可重用性等。

factory机制部分列出的那四条理由,依然适用于callback机制。虽然callback机制能够实现所有的测试用例,但某些测试用例用sequence实现更加方便。virtual sequence的协调功能在callback机制中就很难实现

callback机制、sequence机制和factory机制并不是互斥的,三者都能分别实现同一目的。当这三者互相结合时,又会产生许多新的解决问题的方式。如果在建立验证平台和测试用例时,能够择优选择其中最简单的一种实现方式,那么搭建出来的验证平台一定是足够强大、足够简练的。实现同一事情有多种方式,为用户提供了多种选择,高扩展性是UVM取得成功的一个重要原因

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值