作为一个验证工程师,每一个顶层tb里一般都少不了下面这段代码(前后可能还会有些timeformatf或者uvm_config_db的设置),并且书上都告诉你这是uvm环境启动的入口。
initial begin
run_test();
end
那么问题来了,
1、这个run_test()是一个函数还是个任务?
2、它定义在哪里?
3、为什么module里能够直接调用?
4、有时候可以带参数有时候又可以不带,区别在哪里?
5、作为uvm环境的入口,这个run_test()是怎么做到启动uvm环境的?
我们在uvm源代码里搜索run_test()的定义,找到如下定义:
这个run_test()根源调用的还是uvm_root里定义的run_test()我们稍后再去分析,等于说uvm_globals将uvm_root里定义的run_test()通过单例模式获取uvm_root的唯一实例并将其封装成了一个全局可见的任务run_test(),那我们不禁要问:这个uvm_globals文件是如何实现里面的内容全局可见的呢?
我们在搜下uvm_globals关键字发现它在uvm_base.svh里被include了
而uvm_base.svh这个文件又是在uvm_pkg.svh里被include的
这样一来,只要我们在任何需要调用run_test()的作用域内执行import uvm_pkg:😗,我们就可以调用uvm_globals里所调用的全局变量/方法了。
通过上面的分析我们就已经回答了前3个疑问了,即
1、这个run_test()是一个函数还是个任务?task
2、它定义在哪里?uvm_root中,并在uvm_globals.svh中被封装成了一个全局可见任务
3、为什么module里能够直接调用?只要import uvm_pkg::*就可以调用uvm中的全局任务
这里再给大家补充一点systemverilog中关于作用域的知识点:
以上两种import uvm_pkg::的方法,对应实际uvm内的可见范围是不同的。
方式1是一种全局import,编译后会放在$unit编译单元中,这样后面所有编译的文件都能看到uvm import的相关内容;
方式2这样import只会在module tb内可见,跳出这个module后面编译的文件是看不到uvm import的相关内容的,所以只要不是全局import,在每个独立的作用域内都会去import uvm_pkg::;
所以大家以后import package的时候一定要明白你import的内容的可见域是什么范围,以免出现class not defined这种情况。
下面我们接着一点点分析看看uvm_root里的run_test都做了些什么。首先是变量的声明
关于m_init_objection的相关定义如下
超纲了,不解释。
接着我们来回答run_test()里添加参数与否的问题。对于UVM初学者来说,我们应具备如下常识:
1、仿真运行时如果有参数+UVM_TESTNAME=casename,仿真跑的就是casename这么个testcase,run_test()里面带不带参数都不影响
2、如果运行时没有加参数+UVM_TESTNAME,那么我们需要指定参数run_test(“casename”)来跑casename这么个testcase,这时候run_test()里必须指定参数
并且我们还应该知道+UVM_TESTNAME也是factory机制的一种应用,通过+UVM_TESTNAME指定的参数在运行时对run_test()指定的参数做override。
我们通过分析源代码,看看uvm内部是怎么实现如上机制的。
首先我们要想理解这段文字,还应具备如下基本知识(我们要知道的实在太多了,能不能好好单纯的分析run_test()?)
而在uvm中我们这里通过是否define宏UVM_NO_DPI分成了两种实现,一个是通过DPI-C通过C语言获取参数,另一个通过
v
a
l
u
e
s
values
valuesplusargs()获取参数。代码如下
对于DPI-C的实现,通过的是一个叫做uvm_cmdline_process类层层调用实现的,相关代码(层层递归寻找)如下(自行理解,这里不做解读)
综上所述,我们获取到了命令行+UVM_TESTNAME传递的参数赋值给test_name(覆盖run_test()原本传进的参数),并且将testname_plusarg赋值为1。
如果没有命令行参数+UVM_TESTNAME,则使用run_test()原本传递来的参数,也就是说执行到384行后test_name中存入的就是我们将要运行的testcase的名字。
我们继续分析后续代码。
386行代码就是要保证一定要传入testcase名字,不管是通过run_test()参数还是通过命令行override的参数,否则将不执行如下创建uvm_test_top的工作。
387-391就是在判断run_test()是否被调用过,调用过就会将uvm_test_top作为uvm_root的child存入到m_chilldren中。
392行就是创建testcase的实例,并且实例名为uvm_test_top(声明在335行的uvm_component类型)。我们这里看下uvm_factory里create_component_by_name的定义
返回的是一个uvm_component类型,最终再
c
a
s
t
给
u
v
m
t
e
s
t
t
o
p
。通过以上代码就实现了将我们传入的
t
e
s
t
c
a
s
e
创建并
cast给uvm_test_top。 通过以上代码就实现了将我们传入的testcase创建并
cast给uvmtesttop。通过以上代码就实现了将我们传入的testcase创建并cast给uvm_test_top,这样uvm_test_top就作为所有testcase唯一的实例名,uvm树形结构打印full_name的最顶层(虽然他还有一个parent是uvm_root(inst为uvm_top,name为"top"))。
403-411行是确保创建成功,否则报错,即如果我们要运行的testcase没有被编译,或者没有传入任何参数给test_name。
413-420一般会执行417行的打印内容,413和417的场景使用非常少,不做解读。
这段是核心代码,先看看process类的定义(systemverilog的内建类,可以通过process类访问进程和控制进程。)
关于process的使用实例可以参考我之前写的systemverilog语法研究的文章:关于process类的使用
425可以认为就是获取当前进程,没有则创建,然后启动m_run_phases()
上面这部分内容我们留到后面将phase机制的运行时再讲,可以认为就是phase机制启动的入口,phase按顺序执行。
终于看见曙光了!最后一段代码如下
等待所有phase执行完,kill掉开启的进程,report_summarize()源代码如下:
默认打印到屏幕,指定file后会打印到文件。
finish_on_comletion默认值为1,结束仿真。
于是我们回答了最后两个问题:
4、有时候可以带参数有时候又可以不带,区别在哪里?带参数就是默认运行的testcase的名字,不带参数必须用命令行传入+UVM_TESTNAME参数来运行对应的testcase,并且命令行参数会重写run_test()的默认参数。
5、作为uvm环境的入口,这个run_test()是怎么做到启动uvm环境的?如上对代码的一步步分析。
总结:
整个uvm验证平台的执行过程可以用下面这张图来简单的表示
以上分析过程中还存在一些没有解释清楚或者超出认知范围的复杂概念,后面会慢慢不上,敬请期待!