本期将讲解 UVM 环境构成和启动方式。主要参考资料为
[白皮书]: http://bbs.eetop.cn/thread-320165-1-1.html
[红宝书]: http://rockeric.com/
环境构成
进行仿真验证的基本流程是
1. 例化 DUT
2. 产生并发送激励
3. 检测响应
4. 检查响应是否正确
在验证环境中,产生并发送激励将会交给两个不同的类完成,即 uvm_driver 和 uvm_sequence,检测响应通过 uvm_monitor 完成,而检查响应是否正确通过 uvm_scoreboard。除了保证某项功能正确,我们还需要能够确保 spec 中的每一项功能都通过测试,而衡量验证完备性的指标之一就是功能覆盖率,在我们的验证环境中收集功能覆盖率的任务则交给了 conv_coverage 实现。
接下来将以数据从 uvm_driver 驱动到 DUT,再从 DUT 到 uvm_monitor,再到 uvm_scoreboard 的顺序讲解验证环境的构成。
接口定义
当我们需要进行仿真验证时,与 DUT 的交互是一个必要的内容,所我们首先分析 DUT 的接口,较为简单,一共有四组接口,一组寄存器配置接口,三组数据接口用于输入特征图、权重和偏置数据的读取,一组数据接口用于输出特征图的存储接口。
interface 的定义在顶层的 tb.sv 中,三组输入数据接口可以使用同一类型的接口实现,下列代码中的具体内容省略了,详情请自行查看。最后一组接口用于检测寄存器的内容,当前版本没有使用寄存器模型,所以这个接口是必要的。
interface cfg_intf (input clk , input rst_n);
clocking drv_ck @(posedge clk);
default input #1ns output #1ns;
endclocking
clocking mon_ck @(posedge clk);
default input #1ns output #1ns;
endclocking
endinterface
interface mem_in_intf (input clk , input rst_n);
clocking drv_ck @(posedge clk);
default input #1ns output #1ns;
endclocking
clocking mon_ck @(posedge clk);
default input #1ns output #1ns;
endclocking
endinterface
interface mem_out_intf (input clk , input rst_n);
clocking mon_ck @(posedge clk);
default input #1ns output #1ns;
endclocking
endinterface
interface conv_intf (input clk , input rst_n);
clocking mon_ck @(posedge clk);
default input #1ns output #1ns;
endclocking
endinterface
注意在 interface 定义中,分别定义了两个时钟块,一组驱动用的时钟块,一组检测用的时钟块,目的就是为了模拟真实的建立保持时间,时钟块的具体用法可以参考绿皮书的第四章内容。
环境组件
有了接口定义以后,通过接口定义,我们便能够与 DUT 交互,那么进行交互我们需要做什么呢?
首先看整体结构,如果看不清,后台回复 UVM 结构图获取 VISIO 文件。按照接口进行分类,可以分成两大类,一类通过接口与 DUT 实现交互,另外一类构成了其他的组件,例如 checker,通过其他组件收集到的数据进行数据比对,保证 DUT 的功能正确性。
验证环境一共由四个 PKG 组成,通过在顶层 import 导入:
import cfg_pkg::*;
import mem_in_pkg::*;
import mem_out_pkg::*;
import conv_pkg::*;
与 DUT 直接联系的组件
我们从 DUT 与验证环境的接口处开始说起,cfg_pkg 包含了对于寄存器进行验证的组件。构建 UVM 环境基本的几个组件包括 uvm_driver,uvm_sequencer,uvm_monitor,uvm_agent。而 uvm_sequence_item 和 uvm_sequence 则不属于环境的组件,他们是环境组件之间传递信息的载体。
package cfg_pkg;
import uvm_pkg::*;
`include "uvm_macros.svh"
typedef enum {WR,RD,IDLE}cmd_t;
class cfg_item extends uvm_sequence_item;
endclass : cfg_item
class cfg_driver extends uvm_driver #(cfg_item);
endclass :cfg_driver
class cfg_sequencer extends uvm_sequencer #(cfg_item);
endclass: cfg_sequencer
class cfg_base_sequence extends uvm_sequence #(cfg_item);
endclass: cfg_conv_sequence
class cfg_monitor extends uvm_monitor;
endclass: cfg_monitor
class cfg_agent extends uvm_agent;
cfg_driver driver;
cfg_monitor monitor;
cfg_sequencer sequencer;
local virtual cfg_intf vif;
endclass:cfg_agent
endpackage
UVM 的思想之一就是要降低组件之间的耦合度,让组件的功能更加单纯。
uvm_driver 和 uvm_monitor 是距离 DUT 最近的两个组件,可以直接与 DUT 的接口进行互动。
uvm_monitor 通过检测接口上的信号,转化为数据包,如实地发送给 checker,只实现这一单纯的功能,而对于驱动 DUT 这一功能则交给 uvm_driver。
uvm_driver 通过从 uvm_sequencer 获取到的 uvm_sequence_item 解析出驱动数据,如实的将 uvm_sequence_item 的内容驱动到 DUT 的接口上,也只实现这一单纯的功能,至于具体的激励内容,则通过 uvm_sequencer 暴露接口给顶层环境,让验证人员通过 uvm_sequencer 发送激励。
而 uvm_sequencer 的功能就更加简单了,只需要实现传递 uvm_sequence_item 即可,不需要关注其他的工作。
而 uvm_agent 中则通常会例化四个组件,uvm_driver,uvm_sequencer,uvm_monitor 和对应的 interface。uvm_agent 的功能也非常单一,仅仅只是将对于一组接口的相关组件进行一个打包,把他们整合起来,这样在顶层就只需要例化这一个组件即可。
mem_in_pkg 和 mem_out_pkg 的内容整体上和 cfg_pkg 基本一致,不再赘述。
与 DUT 没有直接联系的组件
通过 cfg_pkg 的内容,我们实现了对 DUT 的驱动与检测,那么驱动的内容从何而来,而检测的数据包又要发送到哪里去呢?从前面图中我们可以看到,除了五个 agent 以外,我们还有其他的组件,包括 conv_checker,conv_coverage 和 conv_virtual_sequencer。
class conv_checker extends uvm_scoreboard;
endclass:conv_checker
class conv_coverage extends uvm_component;
endclass:conv_coverage
class conv_virtual_sequencer extends uvm_sequencer;
cfg_sequencer cfg_sqr;
mem_in_sequencer fmi_sqr;
mem_in_sequencer wt_sqr;
mem_in_sequencer bias_sqr;
endclass:conv_virtual_sequencer
conv_checker 继承自 uvm_scoreboard,他通过前面所述的五个 agent 中的 monitor,获取 DUT 的信息,进行数据对比检查。在现在的 DUT 中,他所实现的功能是,在一次卷积运算结束后,使用软件算法直接进行卷积运算,然后与 DUT 的计算结果进行对比,确保 DUT 功能正确。
UVM 并没有预置的类用于覆盖率收集,所以 conv_coverage 继承自 uvm_component,成为最简单的 UVM 组件。由于除了寄存器的接口以外,其他都是简单的 sram 接口,所以只对寄存器进行覆盖率收集。覆盖率组件通过覆盖率的收集,量化功能验证的完备性,根据对应的功能点,设定对应的覆盖率,而功能验证的目的就是为了达到 100%的功能覆盖率。
conv_virtual_sequencer 继承自 uvm_sequencer,其本身并没有什么功能,所以他的名字中带有 virtual,他只是一个虚拟的 sequencer。其内部包含了 cfg_sqr,fmi_sqr,wt_sqr 和 bias_sqr,作用就是将他们整合在一起,sequencer 就是一根数据线,uvm_sequence_item 就是传输的数据,而 virtual_sequencer 就一个集线器或者说一个拓展坞,把很多条数据线绑在一起。
conv_env
数据的驱动由 uvm_driver 实现,检测由 uvm_monitor 实现,激励由 uvm_sequencer 传递,数据对比由 conv_checker 实现,覆盖率收集由 conv_coverage 实现。那么接下来就需要把这些组件全部整合在一起,成为一个验证环境,这便是 conv_env。在这里,我们只需要完成各个组件的例化和他们之间的连接,不要关心其他工作。
class conv_env extends uvm_env;
cfg_agent cfg_agt;
mem_in_agent fmi_agt;
mem_in_agent wt_agt;
mem_in_agent bias_agt;
mem_out_agent fmo_agt;
conv_checker chker;
conv_coverage cvrg;
conv_virtual_sequencer virt_sqr;
endclass: conv_env
uvm_test
现在我们已经获得了一个针对卷积模块的验证环境,那么如何开始仿真测试?回忆前面所提到的 virtual_sequencer,我们只需要通过 virtual_sequencer 对每个 DUT 的接口进行驱动,就能让 DUT 运转起来。针对每一个测试,我们需要创建对应的 uvm_test 类,然后再 uvm_test 内通过 virtual_sequencer 进行激励发送即可。
class conv_base_test extends uvm_test;
conv_env env;
task run_phase(uvm_phase phase);
phase.raise_objection(this);
this.run_top_virtual_sequence();
phase.drop_objection(this);
endtask
virtual task run_top_virtual_sequence();
endtask
endclass: conv_base_test
上述代码中的 run_phase 的内容就是在构建环境后,整个仿真真正需要进行的测试内容。可以看到我们定义了一个 run_top_virtual_sequence 方法,用于运行 virtual_sequence。
与 virtual_sequencer 对应的,virtual_sequence 就是 virtual_sequencer 所需要传输的内容,它的内部会包括各式各样的 sequence,针对每一个 agent 发送不同的激励。通过修改 virtual_sequence 的内容,我们就能够完成不同的测试用例。
启动方式
这里先不讨论整个环境的树状结构、连接方式和运行机制,这些将在后续的推送中讲解。
在构建完整个环境和测试用例以后,我们就需要在顶层启动测试。
module tb ();
logic clk;
logic rst_n;
conv i_conv (); // 这里省略了端口连接,具体请参考实验代码
// clock generation
initial begin
clk <= 0;
forever begin
#5 clk <= !clk;
end
end
// reset trigger
initial begin
#10 rst_n <= 0;
repeat(10) @(posedge clk);
rst_n <= 1;
end
import uvm_pkg::*;
`include "uvm_macros.svh"
import cfg_pkg::*;
import mem_in_pkg::*;
import mem_out_pkg::*;
import conv_pkg::*;
cfg_intf cfg_if(.*);
mem_in_intf fmi_if(.*);
mem_in_intf wt_if(.*);
mem_in_intf bias_if(.*);
mem_out_intf fmo_if(.*);
conv_intf conv_if(.*);
assign conv_if.start =i_conv.i_regfile.start ;
assign conv_if.done =i_conv.i_regfile.done ;
assign conv_if.fmap_in_w =i_conv.i_regfile.fmap_in_w ;
assign conv_if.fmap_in_h =i_conv.i_regfile.fmap_in_h ;
assign conv_if.fmap_in_ch_div_32 =i_conv.i_regfile.fmap_in_ch_div_32 ;
assign conv_if.k_w =i_conv.i_regfile.k_w ;
assign conv_if.k_h =i_conv.i_regfile.k_h ;
assign conv_if.fmap_out_w =i_conv.i_regfile.fmap_out_w ;
assign conv_if.fmap_out_h =i_conv.i_regfile.fmap_out_h ;
assign conv_if.fmap_out_w_div_32 =i_conv.i_regfile.fmap_out_w_div_32 ;
assign conv_if.fmap_out_ch_div_32 =i_conv.i_regfile.fmap_out_ch_div_32 ;
assign conv_if.pooling_bypass =i_conv.i_regfile.pooling_bypass ;
assign conv_if.act_bypass =i_conv.i_regfile.act_bypass ;
assign conv_if.padding_cnt =i_conv.i_regfile.padding_cnt ;
assign conv_if.stripe =i_conv.i_regfile.stripe ;
assign conv_if.last_pixel =i_conv.i_regfile.last_pixel ;
assign conv_if.last_pixel_div_32 =i_conv.i_regfile.last_pixel_div_32 ;
assign conv_if.fmap_out_ch =i_conv.i_regfile.fmap_out_ch ;
assign bias_if.addr[15:8]='0;
initial begin
uvm_config_db#(virtual mem_in_intf)::set(uvm_root::get(), "uvm_test_top", "fmi_in_vif", fmi_if);
uvm_config_db#(virtual mem_in_intf)::set(uvm_root::get(), "uvm_test_top", "wt_vif", wt_if);
uvm_config_db#(virtual mem_in_intf)::set(uvm_root::get(), "uvm_test_top", "bias_vif", bias_if);
uvm_config_db#(virtual cfg_intf)::set(uvm_root::get(), "uvm_test_top", "cfg_vif", cfg_if);
uvm_config_db#(virtual mem_out_intf)::set(uvm_root::get(), "uvm_test_top", "fmo_vif", fmo_if);
uvm_config_db#(virtual conv_intf)::set(uvm_root::get(), "uvm_test_top", "conv_vif", conv_if);
// If no external configured via +UVM_TESTNAME=my_test, the default test is
// std_test
run_test("std_test");
end
endmodule : tb
在 tb 的顶层模块中,我们要做 5 件事:
1. 定义时钟与复位 2. 例化 dut3. 例化与连接各个 interface4. 将每个 interface 句柄通过 uvm_config_db 传递到环境中去 5. 通过 run_test()方法启动测试
uvm_config_db 是 UVM 所提供用于传递数据的静态方法,在后续的推送中将会展开讲解。这里值得注意的是,一定要在 run_test()之前实现 uvm_config_db 传递,否则在 run_test()开始后,环境内部将无法获取句柄,导致报错。
run_test()是 UVM 提供的测试启动方法,传递参数是一个字符串变量,该字符串将用于指定默认的 testcase。如果在命令选项中,没有通过+UVM_TESTNAME 指定具体的 TESTNAME,将会运行默认的 testcase。
总结
本次讲解了验证环境的基本组件和构成,以及在顶层启动的注意事项。
1. 与 DUT 直接交互的组件为 uvm_driver 和 uvm_monitor,传递激励信息的组件为 uvm_sequencer,uvm_agent 将三者组合起来。
2.uvm_scoreboard 获取各个 uvm_monitor 传递过来的数据,进行比对,保证 DUT 功能的正确性。
3.conv_coverage 用于收集覆盖率
4.conv_virtual_sequencer 将每个 uvm_agent 中的 uvm_sequencer 集中起来进行管理,起到集线器或者说路由器的效果
5.conv_env 将上述所有组件容纳起来,并且进行连接
6.uvm_test 通过编写 conv_virtual_sequence,经由 conv_virtual_sequencer 发送激励实现不同的 testcase
7. 在顶层实现各项例化,并且在 run_test()之前传递接口句柄
8. 通过 run_test()启动测试,并且指定默认 testcase