UVM实战笔记(五)

第五章. UVM验证平台的运行
5.1 phase机制

5.1.1 task phase和function phase

UVM中的phase按是否消耗仿真时间($time打印出的时间),分为两类:

  • function phase:不消耗仿真时间,通过函数来实现
  • task phase:消耗仿真时间,通过任务来实现

下图中灰色背景的是task phase,其他的为function phase。所有phase都会按照图中的顺序自上而下自动执行(从整个验证平台来看)。
在这里插入图片描述

对于function phase来说,同一时间只有一个phase执行;对于task phase来说,run_phase和pre_reset_phase等12个小的phase并行运行。这12个小的phase称为动态运行(run-time)的phase

UVM提供了这么多的phase,目的是方便验证人员将不同的代码写在不同的phase内,有利于其他验证方法学向UVM迁移。在一般的应用中,不会将function phase和task phase全部用上。使用频率最高的是build_phase,connect_phase和main_phase

5.1.2 动态运行phase

动态运行(run-time)phase是UVM1.0引入的新phase,其他phase是在UVM1.0之前就已存在的。12个run_time phase中,reset,configure,main,shutdowm是核心。这四个phase通常模拟DUT的正常工作方式

  • reset_phase对DUT进行复位,初始化等操作
  • config_phase进行DUT的配置
  • DUT的运行主要在main_phase完成。
  • shutdown_phase则是做一些DUT断电相关的操作

通过细分实现DUT更加精确的控制

5.1.3 phase的执行顺序

UVM有9个phase:

  • build_phase

  • connect_phase

  • end_of_elaboration_phase

  • start_of_simulation_phase

  • run_phase

  • extract_phase

  • check_phase

  • report_phase

  • final_phase

执行顺序可以从以下几个角度来看:

  • 首先从整个验证平台或验证平台中的每个组件来看。9个function phase按照上表中各phase的顺序自上而下(此处指的是时间上的自上而下,即验证平台/组件的前一个phase执行完毕后再执行下一个phase。)执行的。
  • 其次是从每个function phase的角度来看。
    • build_phase是自上而下执行此处指的是空间上的自上而下,即验证平台中的每个组件在UVM树中的顺序,由根节点向叶节点自上而下执行。如先执行env的 build_phase,再执行agent的build_phase,因为env在UVM树中的位置比agent更靠上。UVM树中同一层级的组件(如i_agent和o_agent)执行顺序是如何的?UVM源码表明按照组件名称的字典顺序(但UVM并未保证一直按组件实例化名称字典的顺序,如验证平台对同一层级组件执行顺序有要求,应该修改代码,消除这种对实例化名称时字典顺序的依赖))。
    • 除build_phase外,其他7个function phase执行顺序是自下而上(空间上,同build_phase)。
    • run_phase也是按照自下而上此处自下而上指的是空间上的,但是由于run_phase消耗仿真时间,所以又不同于第一条中的时间上的自上而下,function phase不消耗仿真时间,所以仿真时间不会改变。可以理解为将验证平台中各组件的run_phase按照自下而上的顺序使用fork join_none语句全部启动,所以对于run_phae的自下而上更准确的表达是,自下而上启动,并行运行)执行。

前文提到每个run_phase又具有12个run-time phase:

  • pre_reset_phase

  • reset_phase

  • post_reset_phase

  • pre_configure_phase

  • configure_phase

  • post_configure_phase

  • pre_main_phase

  • main_phase

  • post_main_phase

  • pre_shutdown_phase

  • shutdown_phase

  • post_shutdown_phase

从全局来看run phase和run-time的执行顺序按以下代码中顺序执行。

在这里插入图片描述

有几点需要说明:

  • run_phase和12个run-time phase是并行执行

  • 12个run-time phase按照上表中的顺序自上而下执行(确切的说是自上而下启动,并行运行)。

  • 对于每一个run-time phase,验证平台设置了隐式同步处理(如假设验证平台执行到main_phase阶段,那么会等到平台中的各个组件的main_phase都执行完毕后,再进入下一个run-time phase。执行完毕的组件会等待没有执行完毕的组件,A会等待Bmain_phase组件。)。
    在这里插入图片描述

  • 在fork join块结束时,run_phase和run-time phase具有隐式的同步处理

  • 验证平台的不同组件的run_phase之间同样具有隐式的同步处理

5.1.4 UVM树的遍历

UVM树的节点中有以下几种关系:

  • 父子(agent与driver)
  • 兄弟(driver和monitor)
  • 叔侄(driver和scb)

就build_phase而言,对于父子节点,先执行父节点,后执行子节点;对于兄弟节点,按照实例化名称的字典顺序执行;对于叔侄节点,执行顺序按照不同的遍历方式会有不同的执行顺序。

遍历有两种方式:

  • 广度优先:简单来说是按照UVM树中的层级结构执行,先执行第一层中的所有节点,再进入下一层执行第二层中的所有节点,一直到最后一层。

  • 深度优先:在UVM树中,从根节点出发沿着一条路径执行到叶节点,然后返回根节点向另外一条路径执行到根节点,直到所有的节点执行完毕,最后返回根节点。UVM中使用的深度优先遍历方式

注:遍历方式可参考树的遍历方式。
树的遍历 https://blog.csdn.net/weixin_44590534/article/details/106179644?spm=1001.2014.3001.5501

由于UVM使用的是深度优先遍历,那么叔侄节点的执行顺序存在不确定性(如果scb实例化名称比agent实例化名称字典顺序靠前,那么先执行scb再执行driver。反之则先执行driver再执行scb。如代码中要求执行顺序,需要修改代码,消除这种不确定性)。

5.1.5 super.phase的内容

对于bulid_phase来说,uvm_component对其做的最重要的事情是自动获取通过config_db::set设置的参数。如果要关掉这个功能,可以在自己的build_phase中不调用super.build_phase。除了在build_phase外,UVM在其他phase中几乎没有做任何相关的事情。除build_phase外,在写其他phase时,完全可以不必加上super.xxx_phase语句。当然,这个结论只适用于直接扩展自uvm_component的类,如果是扩展自用户自定义的类,那么根据具体的子类情况考虑是否调用super.xxx_phase。

5.1.6 build阶段出现UVM_ERROR停止仿真

当出现uvm_fatal时,仿真器会认为出现了重大的错误。在大型设计中,真正的仿真前的编译,优化可能会花费一个多小时的时间。完成编译,优化后开始仿真,几秒后,出现一个uvm_fatal就停止仿真。当修复了这个问题,再次重新运行,发现又有一个uvm_fatal出现。如此反复,可能会消耗大量时间。但是如果将这些uvm_fatal替换为uvm_error,将所有类似的问题一次暴露出来,一次修复,这会极大缩减时间,提高效率

5.1.7 phase的跳转

在之前的例子中,各phase都是顺序执行的,前一个phase执行完后才执行后一个。使用phase的跳转可以实现phase之间的跳来跳去。phase的跳转是比较高级的功能,本节只举一个简单的例子。

task my_driver::main_phase(uvm_phase phase);
	`uvm_info("driver", "main phase", UVM_LOW)
	fork
		while(1)
		begin
			seq_item_port.get_next_item(req);
			drive_one_pkt(req);
			seq_item_port.item_done();
		end
	begin
		@(negedge vif.rst_n);
		phase.jump(uvm_reset_phase::get());
	end
	join
endtask

如上代码,在监测到vif.rst_n下降沿信号时,在main_phase中使用phase.jump(uvm_reset_phase::get());完成从main_phase到reset_phase的跳转。phase执行的过程如下:

在这里插入图片描述

由于在main_phase中使用了jump跳转到reset_phase,所以reset_phase~main_phase这些phase执行了两遍

phase跳转的难点在于跳转前后的清理和准备工作。特别是对scb,跳转前后内部缓存队列except_queue数据应该清空,同时要容忍跳转后DUT可能输出一些异常数据。

jump函数的原型是:

function void uvm_phase::jump(uvm_phase phase);

jump函数的参数必须是一个uvm_phase类型的变量。在UVM中,这样的变量共有如下几个:

//function phase
uvm_build_phase::get();
uvm_connect_phase::get();
uvm_end_of_elaboration_phase::get();
uvm_start_of_simulation_phase::get();

//task phase and run-time phase
uvm_run_phase::get();
uvm_pre_reset_phase::get();
uvm_reset_phase::get();
uvm_post_reset_phase::get();
uvm_pre_configure_phase::get();
uvm_configure_phase::get();
uvm_post_configure_phase::get();
uvm_pre_main_phase::get();
uvm_main_phase::get();
uvm_post_main_phase::get();
uvm_pre_shutdown_phase::get();
uvm_shutdown_phase::get();
uvm_post_shutdown_phase::get();

//function phase
uvm_extract_phase::get();
uvm_check_phase::get();
uvm_report_phase::get();
uvm_final_phase::get();

并不是所有的phase都可以作为jump的参数。从main_phase跳转到build_phase~start_of_simulation的function phase是不可行的。从main_phase跳转到run_phase也是不可行的,因为run_phase不是main_phase的前驱或者后继phase。如上的phase清单中,从main_phase跳转到pre_reset_phase之后的所有phase都是可以的。从main_phase跳转到reset_phase是一种向前跳转,这种向前跳转,只能是main_phase前的run-time phase的一种。除了向前跳转还可以向后跳转,如从main_phase跳转到shutdown_phase。在向后跳转中,除了run-time phase外,还可以是function phase,如从main_phase跳转到final phase。

5.1.8 phase机制的必要性

V中有非阻塞和阻塞赋值,对应的,在仿真器中要实现分为NBA区域和Active区域,这样在不同的区域做不同的事情,可以避免竞争关系导致的变量值不确定情况。

UVM中phase的设计哲学是在不同的时间做不同的事情引入phase机制,很大程度上解决了因代码顺序杂乱可能会引发的问题

5.1.9 phase的调试

UVM提供命令行参数UVM_PHASE_TRACE来对phase机制进行调试。

<sim command> +UVM_PHASE_TRACE

5.1.10 超时退出

在验证平台运行时,有时测试用例会出现挂起(hang up)的情况。在这种情况下,仿真时间一直向前走,driver或者monitor并没有发出或者收到transactin,也没有UVM_ERROR出现。一个测试用例的运行时间是可以预计的,如果超过这个时间,那么通常就是出错了。UVM中通过uvm_root的set_timeout函数可以设置超时时间。

//第一个参数表示设置的时间,第二个参数表示此设置是否可以被其后的其他set_timeout语句覆盖
uvm_top.set_timeout(500ns, 0);

默认的超时退出时间是9200s,通过宏UVM_DEFAULT_TIMEOUT来指定。

还可以在命令行中设置超时退出时间

<sim command> +UVM_TIMEOUT=<timeout>, <overridable>
5.2 objection机制

5.2.1 objection与task phase

在进入某一个phase时,UVM会收集此phase提出的所有objection,并且实时检测所有的objection是否被撤销了,当发现所有都已经撤销后,那么就会关闭此phase,开始进入下一个phase。当所有的phase都执行完毕后,就会调用$finish来将整个的验证平台关闭。

task main_phase(uvm_phase phase);
	phase.raise_objection(this);
	...
	phase.drop_objection(this);
endtask

如果UVM发现此phase没有提起任何objcetion,那么将会直接跳转到下一个phase中。UVM用户一定要注意:如果想执行一些耗费时间的代码,那么要在此phase下任意一个component中至少提起一次objection。这只对12个run-time的phase,对run_phase则不适用。

由于run_phase与动态运行的phase是并行运行的,如果12个动态运行的phase有objection被提起,那么run_phase根本不需要raise_objection就可以自动执行。对于run_phase来说,有两个选择可以使其中的代码运行:第一是其他动态运行的phase中有objection被提起。这种情况下,运行时间受其他动态运行phase中objection控制,run_phase只能被动接受。第二是在run_phase中raise_objection,这种情况下运行时间完全受run_phase控制。

在这里插入图片描述

举一个例子来形象化objection机制的执行。上图是由env,model,scb组成的大楼,每一层是一个phase。这个建筑物每一层都有三个房间,其中最外层最大的是env,其中又包含model和scb两个房间。在env,model,scb三个房间中,分别有一个电梯run_phase可以直通楼顶(并行运行)。

  • 在每层的每个房间及各个房间的井中,都有可能存在着僵尸(objection)及需要通电才能运转的机器(需执行的代码),整个大楼处于断电状态。
  • 有一个叫做UVM的植物,在经历start_of_simulation_phase之后,于0时刻进入到最顶层(共12层),pre_reset_phase。在进入后,它首先为本层的所有房间及所有电梯(run_phase)通电,如果房间中及井中有机器,那么这些机器就会运转起来。这颗植物在通电完毕后开始检测各个房间有没有僵尸(raise_objection),如果任意一个房间中有僵尸,那么就开始消灭这些僵尸,一直到所有僵尸都消失(drop_objection)。当所有的僵尸被消灭后,就断掉这一层各个房间中的电,所有正在运行的机器将会停止运转,然后UVM植物进入下一层。需要注意的是,它只断掉了所有房间的电,而并没有断掉所有的电梯(run_phase)中的电,所有如果各个电梯中如果有机器,那么他们依然正常运转
  • 如果所有的房间中都没有僵尸,那么它直接断电并进入下一层。在这种情况下,所有的机器只发出一声轰鸣声,便被紧急终止了。
  • 这颗UVM植物逐层消灭僵尸,一直到消灭底层post_shutdown_phase中的僵尸。此时,12个动态运行的phase全部结束,他们中的僵尸全部被消灭。这颗UVM植物并不是立刻进入extrac_phase,而是开始查看所有的电梯(run_phase)中是否有僵尸,如果有就开始消灭他们,一直到所有的僵尸消失,否则直接断掉电梯中的电,所有电梯中正在运转的机器停止运转。当run_phase中的僵尸也被消灭完毕后,开始进入extrac_phase。

5.2.2 参数phase的必要性

phase的引入是为了便于在任何component的main_phase中都能raise_objection。因为要raise_objection都必须使用phase.raise_objection来完成,所以必须将phase作为参数传递到main_phase等任务重。

task main_phase(uvm_phase phase);

上述讨论的都是在task_phase中使用objection机制。那么类似build_phase等function phase是否也可以使用objection机制呢?在function phase中使用objection机制不会报错,但是objection机制更多的是面向main_phase等task phase,而不是面向function phase。

5.2.3 控制objection的最佳选择

  • 在scb中进行控制。(如果要在scb中控制objection,需要去除无限循环,设置成有条件跳出的循环,然后执行drop_objection)
  • 在sequence中进行控制(当sequence完成后,再撤销此objection)

在实际的验证平台中两种方式都有应用,用的最多的是第二种,这也是UVM所提倡的

5.2.4 set_drain_time的使用

在这里插入图片描述

无论任何功能的模块,都有其处理延时。如上图,0时刻DUT开始接收输入,直到p时刻才有数据输出。在seq中,n时刻发送完毕最后一个transaction,如果此时刻立即drop_objection,那么最后的n+p时刻DUT输出的包将无法接收到。因此在seq中,最后一个包发送完毕后,要延时p时间才能drop_objection。

要延时的时间与激励有很大关系。如上图,seq发送长包的时间大于短包的。在随机发送激励时,延时的大小也是随机的,所以无法精准的控制延时,只能根据激励选择一个最大的延时。因此UVM为objection机制引入了drain_time这一属性。

所谓drain_time,指当所有的僵尸都被消灭后,UVM植物并不马上进入下一层,而是等待一段时间,在这段时间内,那些正在运行的机器依然正常地运转,时间到才会进入下一层。

task base_test::main_phase(uvm_phase phase);
	phase.phase_done.set_drain_time(this, 200)
endtask

phase_done是uvm_phase内定义的一个成员变量。当调用phase.raise_objection或者phase.drop_objection时,其实质是调用phase_done的raise_objection和drop_objection。

drain_time属于uvm_objection的一个特性。一个phase对应一个drain_time,并不是所有的phase共享一个drain_time。在没有设置的情况下,drain_time默认值为0。

5.2.5 objection的调试

UVM提供了命令行参数来进行objection的调试:

<sim command> +UVM_OBJECTION_TRACE
5.3 domain的应用

5.3.1 domain简介

上述讨论的内容DUT是一个独立的部分,假设现在DUT要分成两个独立的部分,两个独立的部分可以分别复位,配置,启动,那么就需要用到damain。domain是UVM中一个用于组织不同组件的概念。默认情况下,验证平台的所有component都位于一个名字为common_domain的domain中,若要体现出独立性,那么两个部分的reset_phase,config_phase,main_phase等就不该同步。此时就应该让其中的一部分从common_domain中独立起来,使其位于不同的domain中。

在这里插入图片描述

在这里插入图片描述

domain把两块时钟域隔开,之后两个时钟域的各个动态运行(run_time)的phase就可以不必同步。这里domain只能隔离run-time的phase,对于其他phase,其实还是同步的,即两个domain的run_phase依然是同步的,其他function phase也是同步的

5.3.2 多domain的例子

class B extends uvm_component;
	uvm_domain new_domain;
	`uvm_component_utils(B)
	
	function new(string name, uvm_component parent);
		super.new(name, parent);
		new_domain = new("new_domain");
	endfunction
	
	virtual function void connect_phase(uvm_phase phase);
		set_domain(new_domain);
	endfunction
endclass

以上代码新建了一个domain,并将其实例化。在connect_phase中通过set_domain将B将入到此domain中。

set_domain的原型是:

//第二个参数表示是否递归调用
function void uvm_component::set_domain(uvm_domain domain, int hier=1);

由于B及其子节点在build_phase中完实例化,所以这里一般在connect_phase中调用set_domain。当B加入到new_domain后,他与其他component(默认位于common domain中)的run-time phase就异步了。

5.3.3 多domain中phase的跳转

phase的跳转将只局限于某一个domain中。不能由一个domain中的phase跳转到另一个domain的phase中。

5.4 验证平台执行过程梳理(自己的理解)
  • 使用脚本配置仿真参数和启动测试用例。仿真时使用脚本配置仿真参数(如随机种子,log文件,覆盖率,信息冗余度等参数),并启动测试用例。脚本启动的用例名被捕获,赋值给UVM_TESTNAME参数保存。UVM根据UVM_TEST_NAME创建uvm_test_top(名字由UVM内部指定,不可以更改)的UVM树的根节点。

  • 依次创建UVM树形结构。开始创建uvm平台,按照从上到下(UVM树中的节点位置)的顺序执行各组件的build_phase。创建结束后会调用uvm_root.svh打印UVM树的拓扑结构。组件在实例化完成后,开始从下到上依次执行connect_phase进行各种端口的连接。connect_phase是在根据验证平台的树形结构从下到上执行(仿真log不显示)。最后依次执行其他phase(其他phase不再陈述,具体可以参考run_log,其phase执行的顺序参考5.1.3中讨论的顺序执行)。

  • 激励的发送与数据流传输。在执行到run-time phase的main_phase时,如果某个组件(通常是virtual sequencer)的main_phase设置有default_sequence,则会自动执行其default_seq的内容(此步骤是将sequence关联到验证平台)。通常是在vitrual sequencer的main_phase中启动sequence启动sequence后会自动执行其body函数(body函数通常定义如何发送激励,)。此时seq就将激励发送给sequencer(此时激励就从seq加载到了验证平台中),然后sequencer就将激励转发给相应的driver。driver执行其main_phase函数,通过接口按照时序将数据加载至DUT。monitor组件的main_phase也是并行执行的,monitor组件的main_phase一般写成一个死循环,根据特定的时序进行检测,当满足时序要求,进行数据包的采样,然后把采集到的数据包数据发送给参考模型和比对板(此数据包为输入数据,参考模型模拟DUT行为)。至此,激励发送和传输的过程结束。

  • 最后执行run_phase后面的其他phase。这里说一下report_phase,当执行到此phase时,各组件按照UVM树中的顺序自下而上依次打印report信息(特别对于scb组件,打印出比对激励的比对信息)。当执行完所有phase时,仿真就停止了。

  • 在执行完仿真后,脚本会调用VCS相关的命令创建波形相关文件(FSDB格式的文件)。

注:以上步骤为个人总结,可能存在错误,这部分持续更新。

参考文献:

UVM实战(卷Ⅰ)张强 编著 机械工业出版社