个人认为自学最大的门槛在于:用怎样的形式来输出所学知识,以保证对所学知识的理解是深刻的。我选择写博客来记录自学过程中的疑惑和答案。
SystemVerilog Testbench Lab系列博客将作为我这段时间学习sv的输出,希望能够学会sv在验证中的使用。Synopsys的sv_lab是学习sv的入门资料(EETOP上可以搜到),这一系列的博客都将基于此展开。
了解DUT
创建测试平台之前,阅读rtl代码了解DUT。我觉得验证人员需要关注的是DUT实现了什么功能,不必过分关注实现的细节。
在Synopsys的lab中,DUT是一个路由器,简单而言其功能是:router共有16个输入端口,每个端口可以并行工作,按照destination address、paddings、data
的顺序并行地发送数据;router根据路由逻辑,将输入端口的数据送到对应的输出端口。其发送数据与接收数据的时序图分别如下面两张图所示。
构建SV测试平台
interface
接口模块将test program和dut连接起来,需要定义信号的类型、信号的位宽、信号的方向等。
// interface
interface router_io(input bit clk);
// signals type
logic reset_n;
logic [15:0] frame_n;
logic [15:0] valid_n;
……
logic [15:0] busy_n;
// signals direction - synchronous to 'clk'
clocking cb @(posedge clk);
output reset_n;
output frame_n;
output valid_n;
……
input busy_n;
endclocking:cb
// signal direction - asynchronous to others
modport TB(clocking cb, output reset_n);
endinterface:router_io
logic
为四态逻辑,默认值为X,在sv中比reg和wire更加常用。- reg只能在always、initial等过程块中赋值;wire只能用assign连续赋值
- logic既能在过程块中赋值,也能连续赋值。
bit
为两态逻辑,默认值为0。当sv中不需要变量拥有四个状态时,则定义为bit。- 同步信号在clocking中定义,信号的方向在modport中定义。
- 此处的信号方向是针对test program而言的,与dut方向相反。
test program
测试程序给定了dut需要完成的动作,在端口定义时将上述interface模块中的modport包含进来。可以理解为,test program的入口参数是一个router_io类型的interface。
// test program
program automatic test(router_io.TB rtr_io);
initial begin
$vcdplus;
reset();
end
// concrete task
task reset();
rtr_io.reset_n = 1'b0; // '=' and '<=' lead to the same results
rtr_io.cb.frame_n <= '1;
rtr_io.cb.valid_n <= '1;
##2 // delay two clock cycles ???
rtr_io.cb.reset_n <= 1'b1;
repeat(15) @(rtr_io.cb);// repeat 15 clock cycles
endtask:reset
endprogram:test
automatic
关键字表明数据的生命周期是动态的。对于automatic
而言,将test定义为automatic
意味着其内部的所有变量默认也是automatic
的(即所有变量随着test program的建立/销毁而建立/销毁)。- 定义module、interface、package或者program时,限定
automatic
或static
关键字的目的是使得在过程块中声明的变量有统一默认的生命周期。 - 对于
static
类型的变量,用户在声明变量时应同时对其初始化,而且初始化只会伴随它的生命周期发生一次,并不会随着方法调用被多次初始化。
- 定义module、interface、package或者program时,限定
@(rtr_io.cb)
是更新仿真时间的方式,括号里是在TB参数列表中例化的interface名和clocking名。- 上述语句表示更新单次时钟周期;若更新多个周期则在前面加
repeat(n)
,n
表示时钟周期数。
- 上述语句表示更新单次时钟周期;若更新多个周期则在前面加
harness file
实际上就是顶层文件,例化上述几个模块,构成一个完整的测试平台。test program端口的输入信号来自interface,dut的端口信号定义成interface相关的hierarchy形式。
// harness file
`timescale 1ns/1ps
module router_test_top;
parameter simulation_cycle=100; // clock cycle
bit SystemClock;
router_io top_io(SystemClock); // intansitate interface
test t(top_io); // add program
router dut( // intansitate dut
.clock(top_io.clk),
.reset_n(top_io.reset_n),
……
.busy_n(top_io.busy_n)
);
initial begin
$timeformat(-9, 1, "ns", 10);
SystemClock = 0;
……
end
endmodule
- 底层中分别例化interface、test program、dut,并且通过interface连接起来。
$timeformat
是sv的内部函数,定义仿真时间。其中的4个参数分别是单位、精度、显示后缀、显示位宽,详见link。
一些疑惑
-
##2
是sv中的延时语句,##
运算符在testbench中用于延迟特定时钟沿或时钟周期单位。通过仿真验证此处的作用是等第2个时钟上升沿到来。 - 图1是时钟SystemClock初始值为0的仿真波形,reset_n等待了1.5个clock cycle;图2是时钟SystemClock初始值为1的仿真波形,reset_n等待了2个clock cycle。
- 注意:未点亮的信号为interface中时钟块内的信号(图上半部分),点亮的信号为interface的端口信号(图下半部分)。
clocking cb @(posedge clk); // above
output reset_n;
output frame_n;
output valid_n;
……
endclocking:cb
…… // below
logic reset_n;
logic [15:0] frame_n; // unknown at initial state in Fig1
logic [15:0] valid_n;
……
- 对比两张图中的frame_n和valid_n信号,发现interface中时钟块内的信号立即有效,因为task在仿真0时刻时直接给时钟块内的信号赋值;而interface端口的信号有效时刻与时钟上升沿同步,行为类似于在时钟上升沿传递信号值。
-
reset_n
与cb.reset_n
的关系是怎样的? - 显而易见的是,reset_n定义在时钟块之外,与clk是异步的;而cb.reset_n定义在时钟块之内,与clk是同步的。在原task的基础上,尝试将task中的reset_n改成cb.reset_n,得到以下编译报错:时钟块中的信号使用非法的阻塞赋值。
……
rtr_io.reset_n = 1'b0; -> rtr_io.cb.reset_n = 1'b0;
……
Error: Clocking block output rtr_io.cb.reset_n is illeagally driven by a blocking assignment.
- 根据提示修改阻塞赋值为非阻塞赋值,得到的波形如下:可以看出若时钟块外的reset_n无赋值,则cb.reset_n在时钟上升沿将信号值传递给reset_n。frame_n和valid_n信号的赋值情况与之类似。
……
rtr_io.reset_n = 1'b0; -> rtr_io.cb.reset_n <= 1'b0;
……
- 在原task的基础上,将
##2
延时语句注释,仿真波形如下:时钟上升沿到来之前reset_n为0,时钟上升沿到来之后reset_n为1,即cb.reset_n在时钟上升沿将信号值传递给reset_n。因而可以认为:若cb.reset_n为未知值,则reset_n保持异步值;否则,reset_n在时钟有效沿到来时寄存cd.reset_n的值(此时是同步的)。
……
##2 -> // ##2
……
- 在原task的基础上,将
##2
延时语句、cb.reset_n信号拉高语句注释,得到如下的仿真波形:cb.reset_n一直是未知值,reset_n也一直保持异步值0,印证了上述观点。
……
##2 -> // ##2
rtr_io.cb.reset_n <= 1'b1; -> // rtr_io.cb.reset_n <= 1'b1;
……