第四章. 连接设计和测试平台
4.1 测试平台和DUT之间通信
DUT和测试平台(Test)通常是分开的模块(module,描述硬件),可以在顶层(top)中将DUT和Test例化,然后根据对应信号进行连接(注意信号的方向)。
//待测DUT
module arb_port (output logic [1:0] grant,
input logic [1:0] request,
input logic rst,
input logic clk);
...
always@(posedge clk or posedge rst)
begin
if(rst) grant <= 2'b00;
else ...
end
endmodule
//测试模块(测试平台定义在另一个模块中,与设计所在的模块相互独立)
module test(input logic[1:0] grant,
output logic[1:0] request,
output logic rst,
input logic clk);
initial
begin
@(posedge clk) request <= 2'b01;
$display("@%0t:Drove req = 01", $time);
repeat (2) @(posedge clk);
if(grant != 2'b01) $display("@%0d: a1: grant != 2'b01", $time);
$finish;
end
endmodule
//在顶层中例化DUT和Test并进行连接(不使用接口)
module top;
logic [1:0] grant, request;
bit clk, rst;
//系统时钟发生器
always #5 clk = ~clk;
//clk在DUT和Test都为输入,由顶层clk驱动
//DUT中grant为输出,Test中grant为输入,则由顶层模块连接后,驱动由DUT发送到Test
arb_port a1 (grant, request, rst, clk);
test t1(grant, request, rst, clk);
endmodule
在一个真实的设计中往往含有数百个端口(port,相对于硬件DUT来说,一个端口就是一个信号)信号,需要数页代码来声明信号和端口。所以这种连接方式是极易出错的,因为一个信号可能流经几个设计层次,它必须一次又一次的被声明和连接。如果相对现有的信号进行拓展新信号,必须在多个文件中定义和连接,所以SV引入接口(interface,相对于Test来说,抽象化概念)来简化连接。
4.2 接口
SV使用接口为程序块之间的通信建模,接口可以看作一捆智能的连线。接口包含了连接,同步,通信的功能。
4.2.1 使用接口简化连接
//定义接口
interface arb_if(input bit clk);
logic [1:0] grant, request;
logic rst;
endinterface
//使用arb_if定义DUT
module arb(arb_if arbif);
...
always @(posedge arbif.clk or posedge arb_if.rst)
begin
if(arbif.rst) arbif.grant <= 2'b00;
else arbif.grant <= next_grant;
...
end
endmodule
//使用arb_if定义Test
module test(arb_if arbif);
...
initial
begin
...
@(posedge arbif.clk);
arbif.request <= 2'b01;
$display("@%0d: Drove req = 01", $time);
repeat(2) @(posedge arbif.clk);
if(arbif.grant != 2'b01) $display("@%0d: al: grant != 2'b01", $time);
$finish;
end
endmodule
//使用接口在顶层模块中连接DUT与Test
module top;
bit clk;
always #5 clk = ~clk;
arb_if arbif(clk);
arb a1 (arbif);
test t1(arbif);
endmodule
与端口相比,使用接口连接可以看出来能使代码变得简洁不易出错,如果想拓展接口中的信号,只需在接口中定义和使用这个信号的模块做修改,不需要其他操作,这种特性极大降低了连接出错的几率。
使用接口时需要确保在你的模块和程序块(program block,软件建模)之外声明接口变量(就是说不能将接口定义在module和program内)。有些编译器不支持在模块中定义接口,及时某些编译器支持,接口也只是所在模块的局部变量,对设计的其他部分来说是不可见的。
4.2.2 连接接口和端口
如果DUT中端口使用4.1中的方式定义(不使用接口,端口声明),而Test使用4.2.1中的方式声明(使用接口),此时连接就涉及到如何连接接口与端口,接口与端口的连接是以点对点的连接方式进行的。实际开发过程中常用此方式进行连接。
//顶层模块连接接口与端口
module top;
bit clk;
always #5 clk = ~clk;
arb_if arbif(clk);
//DUT实例化过程中'.'代表一个端口,后面名字代表端口名称,括号内为与该端口连接的信号
arb_port a1(.grant(arbif.grant),
.request(arbif.request),
.rst(arbif.rst)
.clk(arbif.clk));
test t1(arbif);
endmodule
4.2.3 使用modport将接口中的信号分组并指定方向
在4.1中端口信号的声明包含了方向,编辑器会依次来检查连线方向是否发生错误。在接口中使用modport结构能够将信号分组并指定方向。
//带有modport的接口
interface arb_if(input bit clk);
logic[1:0] grant, request;
logic rst;
modport TEST(output request, rst,
input grant, clk);
modport DUT(input request, rst, clk,
output grant);
modport MONITOR(input request, grant, rst, clk);
endinterface
//在接口中使用modport
module arb(arb_if.DUT arbif);
...
endmodule
module test(arb_if.TEST arbif);
...
endmodule
//顶层例化与4.2中相同,因modport只需要在模块定义时指明,而模块例化时不需要指明。使用modport更确切得代表了一个真实的设计,尤其是信号的方向
在设计中可以通过两种方法使用modport名:一种是在接口信号的程序和模块中使用modport名;另外也可以在顶层模块中使用modport名,然后把接口放到程序和模块的端口表中(module,program中使用interface声明,top层使用interface.modport声明)。正常情况下,使用第一种连接方式,因为modport是接口实现的细节,不应该出现在顶层模块中。特殊情况,如果一个模块需要多次例化,每次例化需要连接到不同的modport,那么此时应该使用第二种连接方式。
并非接口中的每个信号都是必须连接的,某些模块可能只关注接口的某些信号而忽略其他信号。
4.2.4 接口的优缺点
优点:便于设计重用,减少连接错误,可拓展性强,modport允许多个信号捆绑到一起,也可以通过定义方向借助编译器进行自动检查。
缺点:对于点对点连接,使用modport接口描述跟使用信号列表的端口一样冗余。必须同时使用信号名和接口,可能会使模块变得冗长。如两个连接的模块是一个不会被重用的专用协议,使用接口需要做比端口连接更多的工作。连接两个不同的接口很困难,需正确拆分独立的信号并正确的驱动。
4.3 激励时序(难点)
4.3.1 使用时钟块控制同步信号的时序
**接口块可以使用时钟块来指定同步信号相对于时钟的时序。时钟块中的任何信号都将同步地驱动或采样,这就保证了测试平台在正确的时间点与信号交互。**一个接口可以包含多个时钟块,因为每个块中都只有一个时钟表达式,所以每一个对应一个时钟域。可以在时钟块中使用default语句指定一个时钟偏移。一旦定义了时钟块,测试平台就可以用@arbif.cb表达式等待时钟,而不需要描述确切的式中信号和边沿@(posedge arbif.cb)
。这样即使改变了时钟块中的时钟或者边沿,也不需要修改测试平台的代码。
//带时钟块的接口
interface arb_if(input bit clk);
logic [1:0] grant, request;
logic rst;
clocking cb @(posedge clk);
output request;
input grant;
endclocking
modport TEST (clocking cb, output rst);
modport DUT (input request, rst, output grant);
endinterface
//测试平台
module test(arb_if.TEST arbif);
initial
begin
arbif_cb_request <= 0;
@arbif.cb;
$display("@%0t: Grant=%b", $time, arbif.cb.grant);
end
endmodule
4.3.2 接口中的logic和wire对比
cb块中声明了块中的信号在时钟的上升沿有效,**信号的方向是相对于modport(也就说方向相对于采样的模块)的,接口中的信号建议定义为logic,因logic易用。如果测试平台在接口中使用过程赋值语句驱动一个异步信号,那么该信号必须是logic类型。wire类型变量只能被连续赋值语句驱动,时钟块中的信号始终是同步的,可以定义为logic或者wire。logic信号可以直接被驱动,而wire需要使用额外的代码。**建议使用logic的另一个原因是,如果你无意中使用了多个元件的驱动源,编译器会自动报错。
//接口中驱动logic和wire信号
interface asynch_if();
logic l;
wire w;
endinterface
module test(asynch_if ifc);
logic local_wire;
//接口中的wire类型不能直接驱动,需在module中定义logic类型驱动信号,使用assign赋值语句将信号线连接,module中通过logic类型驱动
assign ifc.w = local_wire;
initial
begin
ifc.l <= 0; //直接驱动异步logic信号
local_wire <= 1; //借助logic驱动wire类型
end
endmodule
4.3.3 V的时序问题
在实际的硬件设计中,DUT中的存储单元在时钟的有效沿锁存输入信号,这些数值由存储单元输出,然后通过逻辑块到达下一个存储单元。从上一个存储单元的输入到下一个存储单元的输入延时必须小于一个时钟周期。所以测试仪需要在时钟沿之后驱动芯片的输入,然后在下一个时钟之前读取输出。测试平台应该模拟测试仪这种行为,应该在有效时钟边沿或边沿之后驱动待测设计,然后在有效时钟沿到达之前,在满足协议时序的前提下,尽可能晚地采样。
如果DUT和测试程序仅由V模块构成,这几乎是不可能实现的。如果测试平台在时钟边沿驱动DUT,就会存在竞争状态。如果时钟到达一个DUT的时间快于测试平台的激励,但是到达另一个DUT的时钟又晚于这个激励,这种情况会导致DUT的外部时钟沿都是在相同的仿真时间达到,DUT内部有一些输入在上个时钟周期采样,但是其他的输入却在当前时钟周期采样。
解决此问题的一种方法是给系统添加延时。比如#0
,0时延会强迫V代码的线程停止并在所有其他代码完成之后被重新调度执行。但在一个大型的设计中,往往不可避免地存在多个线程都想最后执行,#0带来的结果是每次运行结果不确定,所以要避免使用#0以免代码不稳定并且不可移植。
另外一个解决方法是使用一个较大的延时,#1
。 RTL代码除了时钟沿之外没有其他时序信息,所以逻辑电路在时钟沿之后一个时间单位就会稳定。但是如果一个模块使用1ns
时间精度,而其他仅使用10ps
的时间精度呢,那么#1
意味着1ns
,10ps
还是其他的时间长度呢。你需要在时钟的有效沿之后,并且是在任何事件发生之前,而非在一段时间之内,尽快地驱动设计。所以应当避免使用#1延时解决时序问题。
对DUT输出信号的采样存在着相同的问题,希望在时钟有效沿到来之前的最后时刻捕获信号的值,你可能直到下个时钟沿会出现在100ns
,但是不能在100ns
出现时钟边沿的时钟采样,因为设计的输出值可能已经改变了,应当在时钟沿到达之前的Tsetup
时间上采样。
4.3.4 程序块和时序区域(难点)
竞争问题的根源在于设计和测试平台的事件混合在同一个时间片(time slot)内,即使在纯RTL程序中也会发生同样的问题。如果存在一种可以将时间轴上分开这些时间的方法,如在100ns
时刻,测试平台可以在时钟信号变化或者设计产生任何活动之前采样设计的输出信号。在所有的事件执行完毕后,测试平台开始下一个动作。在SV中,测试平台的代码在一个程序块中,但是程序块中不能有任何的层次级别,例如模块的实例,接口或者其他程序。
SV引入了一种新的时间片的划分方式,在V中,大多数的时间在有效区域执行。在一个时间片首先执行Active
区域,在这个区域中运行设计事件,包括RTL,门级代码和时钟发生器。第二个区域是Oberved
区域,执行断言。接下来就是执行测试平台的Reactive
区域。注意时间并不是单向向前流动,Observed
和Reactive
区域的事件可以触发本时钟周期内Active
区域中进一步的设计事件。最后是Postponed
区域,他将在时间片的最后,所有设计活动都结束后的只读时间段采样信号。
4.3.5 仿真的结束
在V中,仿真在调度事件存在的时候会继续执行,直到遇到$finish。SV中增加了一种结束仿真的方法。SV把任何一个程序块都视为含有一个测试,如果仅有一个程序块,那么当完成所有的initial块中的最后一个语句时,仿真就结束了,因为编译器认为这就是测试的结尾。即使还有模块或者程序块的线程在运行,仿真也会结束。所以,当测试结束时无需关闭所有的monitor和driver。如果存在多个程序块,仿真在最后一个程序块结束时结束,这样最后一个测试完成时仿真就会结束。可以执行$exit提前中断任何一个程序块,也可以使用$finish来结束仿真。
4.3.6 指定设计和测试平台之间的时延
时钟块的默认时序是在#1step
延时之后采样输入信号,在#0
延时之后驱动输出信号。#1step
延时规定了信号在前一个时间片的Postponed
区域,在设计有任何新的动作之前被采样,这样就可以在时钟改变之前捕获输出值。因为时钟模块的原因,测试平台的输出信号是同步的,所以他们直接被送入设计中,在Reactive区域运行的程序块在同一个时间片再一次触发Actie区域。可以想象时钟块在设计和测试平台中插入了一个同步器来理解。(不理解)
4.4 接口的驱动和采样
测试平台需要驱动和采样设计的信号,这主要是通过带有时钟块的接口实现的。异步信号通过接口时没有任何时延,比如rst
,而时钟块中的信号将得到同步。
4.4.1 接口同步
可以使用V中的@
和wait
来同步测试平台中的信号。
//信号同步
program automatic test(bus_if.TB bus);
initial
begin
@bus.cb; //时钟块的有效时钟沿(posedge,negedge)继续
repeat(3) @bus.cb; //等待3个有效时钟沿
@bus.cb.grant; //在任何时钟沿继续
@(posedge bus.cb.grant) //上升沿继续
@(negedge bus.cb.grant) //下降沿继续
wait (bus.cb.grant == 1); //等待表达式被执行,如果已经是真,不做任何延时
@(posedge bus.cb.grant or negedge bus.rst); //等待几个信号
end
endprogram
4.4.2 接口信号采样
当从时钟块读取一个信号时,是在时钟沿之前得到采样值。
`timesacle 1ns/1ns
program test(arb_if.TEST arbif);
initial
begin
$monitor("@%0t: grant = %h", $time, arbif.cb.grant);
# 50ns $display("End of test");
end
endprogram
module arb(arb_if.DUT arbif);
initial
begin
# 7 arbif.grant = 1; //@7ns
# 10 arbif.grant = 2; //@17ns
# 8 arbif.grant = 2; //@25ns
end
endmodule
时序图如下,在仿真时刻25ns时刻,采样值为2而不是3,说明信号的新值不是在下一个时钟周期(25ns~35ns)传递给测试平台的。
4.4.3 接口信号驱动
当在时钟块中使用modport时,任何同步接口信号多必须加上接口名(arbif)和时钟块名(cb)的前缀。
4.4.5 通过时钟块驱动接口信号
在时钟块中应当使用同步驱动,接口信号必须使用非阻塞赋值来驱动。信号在赋值后并不会立刻改变(在时钟沿赋值除外)。例如测试平台在100ns的时候同时产生了arbif.cb.request和arbif.cb,那么request信号在100ns的时候就会改变。如果测试平台在101改变arbif.cb.request信号,那么该变化直到下一个时钟沿才会传递给设计。
busif.cb.request <= 1; //同步驱动
busif.cb.cmd <= cmd_buf; //同步驱动
如果测试平台在时钟的有效沿改变接口信号,那么其值会立刻传递到设计中,这是因为时钟块的默认输出延时是#0。如果测试平台在时钟有效沿之后驱动输出,那么该值直到时钟的下一个有效沿才会传递到设计中。
program test(arb_if.TEST arbif);
initial
begin
#7 arbif.cb.request <= 3; //@7ns
#10 arbif.cb.request <= 2; //@17ns
#8 arbif.cb.request <= 1; //@25ns
#25 finish;
end
endprogram
module arb(arb_if.DUT arbif);
initial
$monitor("@%0t: req = %h", $time, arbif.request);
endmodule
时序图如上,第二个周期产生的值2永远不会被DUT捕获,因为第三个周期结束时测试平台产生了值1。异步的驱动时钟块信号会导致数值丢失,应该使用时钟延时前缀以保证在时钟沿驱动信号。
##2 arbif.cb.request <= 0; //等待两个时钟周期后赋值
##3 //非法,必须跟赋值语句同时使用
如果想在驱动一个信号前等待两个时钟周期,可以使用repeat(2)@bus.cb;或将时钟周期延时##2.。后一种方式只能在时钟块里作为驱动信号的前缀来使用,因为它需要知道使用哪个时钟来做延时。
4.4.6 接口中的双向信号
V中,如果想要驱动一个双向信号,比如一个过程代码中的双向端口,需要用一个连续赋值语句来将reg连接到wire。
SV中,当你在程序对线网赋值时,SV实际上将值写到了一个驱动该线网的临时变量中,所有驱动器输出经过判断后程序可以直接通过连线读取该值。
interface master_if(input bit clk);
wire [7:0] data; //双向信号
clocking cb @(posedge clk);
inout data;
endclocking
modport TEST (clocking cb);
endinterface
program test (master_if mif);
initial
begin
mif.cb.data <= 'z; //三态总线
@mif.cb;
$display(mif.cb.data); //从总线读取
@mif.cb;
mif.cb.data <= 7'h5a; //驱动总线
@mif.cb;
mif.cb.data <= 'z; //释放总线
end
endprogram
SV中没有明确定义如何驱动接口中的异步双向信号,驱动双向信号有两种解决方式:使用一个跨模块引用或连续赋值语句。
4.6.7 为什么在program中不允许使用always块
在一个设计中,一个always块可能从仿真的开始就会在每一个时钟的上升沿触发执行。但是一个测试平台的执行过程是经过初始化,驱动和响应设计行为等步骤后结束仿真的。在测试平台中,一个连续执行的always模块不能正常工作。当program中最后一个initial块结束的时候,仿真实际上也默认结束了,就像执行了$finish一样,如果加入了一个always块,它将永远不会结束,这样就不得不显式调用$exit来发出程序块结束的信号。如果确实需要一个always块,可以使用initial forever来完成。
4.6.8 时钟发生器
时钟发生器跟设计结合地更加紧密,所以时钟发生器应当定义一个模块。不应该把时钟发生器放在程序块里。如下clk
和out_sig
信号都从Reactive
区域开始传递,在Active区域进入设计,根据这两个信号到达的先后不同可能会引起竞争状态。将时钟发生器放在一个模块中可以避免竞争状态。
program bad_generator(output bit clk, out_sig);
initial forever #5 clk <= ~clk;
initial
begin
forever @(posedge clk) out_sig <= ~out_sig;
end
endprogram
如下是一个正确的时钟发生器,他有意避免了0时刻的边沿以免竞争情形的发生。所有的时钟边沿使用阻塞赋值生成,他们将在Active
区域触发事件的发生。如果确实需要0时刻产生一个时钟边沿,那么可以使用非阻塞赋值语句设置初始值,这样所有的时钟敏感逻辑电路比如always块都会在时钟变换之前执行。
module clock_generator (output bit clk);
initial
always #5 clk = ~clk //在时间0之后生成时钟沿
endmodule
4.5 连接模块
module top;
bit clk;
always #4 clk = ~clk;
arb_if arbif(.*);
arb al(.*);
test t1(.*);
endmodule
快捷符号.*
(隐式端口连接),能自动在当前级别自动连接模块实例的端口到具体信号,只要端口和信号的名字和数据类型相同。端口列表中的接口必须连接,否则SV编译器不通过。
4.6 顶层作用域
有时需要在仿真过程中创建程序或者模块之外的对象,以便参与仿真的所有对象都可以访问他们。在V中,只有宏定义可以跨越模块的边界,而且经常被用来创建全局变量。
SV中引入了编译单元,它是仪器编译的源文件的一个组合。任何module,macromodule,interface,program,package或者primitive边界之外的作用域被称为编译单元作用域,也称为$unit
。这个作用域内的任何成员,比如parameter,都类似于全局变量,因为它可以被所有低一级的块访问。但是它们又不同于真正的全局成员,例如parameter在编译时其他源文件不可见。
有些工具,比如Synopsys VCS,它同时编译所有的SV代码,所以$unit
是全局的。但是Synopsys Design Compiler一次编译一个模块或者一组模块,这时$unit
可能只包含了一个或者几个文件的内容。其他供应商的EDA工具可能一次编译所有的文件或者只是一个子集,结果导致**$unit是不可移植的。**
实例名$root
允许你从顶层作用域开始明确地引用系统中的成员名。此时,$root
类似于Unix文件系统中的根目录/
。对于VCS这样一次编译所有文件的工具。$root
和$unit
是等价的。当你的代码引用另一个模块中的成员时,编译器首先在本作用域内查找,然后在上一层作用域内查找,如此往复直到到达顶层作用域。可以通过使用$root指定绝对路径明确地引用跨模块的变量。
4.7 程序-模块交互
程序块可以读写模块中的所有信号,可以调用模块中的所有例程,但是模块看不到程序块。程序可以调用模块中的例程执行不同的动作,这个例程可以改变内部信号的值。因为当前SV标准没有定义怎样在程序块内改变信号的值,所以需要在设计中写一个任务来改变信号的值,然后在程序中调用这个任务。
4.8 SVA断言
可以使用SVA在设计或者能够创建时序断言。断言的例化跟其他设计块的例化相似,而且在整个仿真过程中都是有效的。仿真器会跟踪哪些断言被激活,这样就可以在此基础上收集功能覆盖率的数据。
4.8.1 立即断言
测试平台的过程代码可以检查待测设计的信号值和测试平台的信号值,并且在存在问题时采取相应的行动。
bus.cb.request <= 1;
repeat (2) @bus.cb;
if(bus.cb.grant != 2'b01) $display("Error, grant!=1");
断言比if
语句更加紧凑,设计期望断言表达式为真,否则输出后面的错误。
bus.cb.request <= 1;
repeat (2) @bus.cb;
a1: assert(bus.cb.grant == 2'b01);
如果产生了一个grant信号,那么测试继续执行,如果信号不符合期望值,仿真将给出断言失败信息。断言是声明性代码,他的执行过程和过程性代码有很大差异。使用几行断言,可以验证复杂的时序关系,等价的过程代码可能远比这些断言要复杂和冗长。
//一个断言有可选的then和else分句。
a1:assert(bus.cb.grant == 2'b01)
else $error("Grant not asserted");
SV有四个输出消息的函数:$info
,$warning
,$error
和$fatal
。这些函数仅允许在断言内部使用,不允许在过程代码中使用。
4.8.2 并行断言
可以理解并行断言是一个连续运行的模块,它为整个仿真过程检查信号的值。需要在断言内指定一个采样时钟。
//并行断言,检查request在非复位状态下没有X/Z信号
interface arb_if(input bit clk);
logic [1:0] grant, request;
logic rst;
property request_2state;
@(posedge clk) disable iff (rst);
$isunknown(request) == 0;
endproperty
assert_request_2state:assert property (request_2state);
endinterface
断言还有许多其他的用法。例如,可以在接口中使用断言,这样接口不仅可以传送信号值也可以检查协议的正确性。
4.9 ref端口的方向
SV引入了一种新的端口方向ref,ref是对变量的引用,它的值是该变量最后一次赋的值。如果将一个变量连接到多个ref端口,就可能产生竞争,因为多个模块的端口都可能更新同一个变量。
4.10 仿真的结束
仿真在程序块中的最后一个initial块结束是结束。其实,在最后一个initial块完成时,它隐式地调用$exit
以标志程序的结束。当所有的程序块都退出了,$finish
函数的隐性调用也就结束了。也可以在需要的时候直接调用$finish
来结束仿真。
但是,程序并没有完全结束。模块或者程序块可以定义一个或者多个finial
块来执行仿真器退出前的代码。此块可以放置清理任务,在finial块中不能调度事件,或者有任何时延信息。
program test;
int errors, warnings;
initial
begin
...
end
finial $display("Test done with %0d errors and %0d warnings", errors, warnings);
endprogram
参考文献:
SystemVerilog验证 测试平台编写指南(原书第二版)张春 麦宋平 赵益新 译