SV——连接设计和测试平台
验证一个设计需要经过几个步骤:生成输入激励,捕获输出相应,决定对错和衡量进度。要完成这个设计,首先第一步就是如何将DUT(Design Under Test)连接到测试平台。
1.将测试平台和设计分开
测试平台的代码独立于设计的代码,设计者需要编写满足规范的代码,而验证工程师需要创建使得设计不满足设计规范的场景。使用模块来保存测试平台经常会引起驱动和采样时的时序问题,在SystemVerilog引入程序块(program block),从逻辑和时间上来分开测试平台。
1.1 测试平台和DUT之间的通信
随着设计复杂度的增加,模块之间的连接也变得复杂。在本章中,为了让大家更好地了解测试平台与DUT之间的连接关系,我们以搭建仲裁器的测试平台为例进行分析。
例1:使用端口的仲裁器模型(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
测试平台定义在另一个模块中,与设计所在的模块相互独立。一般来说,测试平台通过端口与设计连接。
例2:使用端口的测试平台
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) $diaplay("@%0t:a1:grant!=2'b01",$time); ...... $finish; end endmodule
顶层网单连接了测试平台和DUT,并且包含有一个简单地时钟发生器(clock generator)。
例3:没有接口的顶层网单
module top; logic[1:0] grant, request; bit clk,rst; always #5 clk=~clk; //时钟发生器 arb_port a1 (grant,request,rst,clk); //例1 使用端口的仲裁器模型 test t1 (grant,request,rst,clk); //例2 使用端口的测试平台 endmodule
虽然通过端口连接,上面的例子看上去并不是太复杂,但是在实际的真实设计中往往含有数百个端口信号,需要数页代码来声明信号和端口。所有的这些连接都是极易出错的。如果你想添加一个新的信号,它必须在多个文件中定义和连接。针对以上问题,SystemVerilog都有相应的解决方案。
2.接口 interface
SystemVerilog使用接口为块之间的通信建模,接口看成一捆智能的连线。接口包含了连接、同步、甚至两个或者更多块之间的通信,它们连接了设计块和测试平台。
2.1 使用接口来简化连接
我们将端口捆绑成一个接口。接口扩展到测试平台和DUT的驱动和接收功能模块。时钟可以是接口的一部分或者是一个独立的端口。
最简单的接口仅仅是一组双向信号的组合。这些信号使用logic数据类型,可以使用过程语句驱动。
例4:仲裁器的简单接口
interface arb_if (input bit clk); logic[1:0] grant,request; logic rst; endinterface
例5 使用了简单接口的仲裁器模型
module arb (arb_if arbif); //DUT使用接口,接口实例名arbif ...... always@(posedge arbif.clk or posedge arbif.rst) begin if(arbif.rst) arbif.grant<=2'b00; else ...... end endmodule
可以通过使用实例名arbif.request来引用接口的信号。接口信号必须使用非阻塞赋值来驱动。
例6 使用简单仲裁器接口的测试平台
module test (arb_if arbif); ...... initial begin //此处省略了复位代码 @(posedge arbif.clk); arbif.request<=2'b01; $display("@%0t:Drove req=01",$time); repeat(2) @(posedge arbif.clk); if(arbif.grant!=2'b01) $diaplay("@%0t:a1:grant!=2'b01",$time); ...... $finish; end endmodule
所有的这些块都在top模块中例化和连接。
例7 使用简单仲裁器接口的top模块
module top; bit clk; always #5 clk=~clk; arb_if arbif(clk); //例4 arb a1(arbif); //例5 test t1 (arbif); //例6 endmodule:top
顶层模块将模块里描述的设计DUT、程序块中的测试平台TEST、接口interface连接起来;快捷符号 .**(隐式端口连接),能自动在当前级别自动连接模块实例的端口到具体信号,只要端口和信号的名字和数据类型相同。需要注意以下几点:
- 在搭建测试平台时,接口信号必须使用非阻塞赋值来驱动。
- 使用接口时需要确保在你的模块和程序块之外声明接口变量。
2.2 连接接口和端口
可以直接将接口的信号连接到端口上。
module top; bit clk; always #5 clk=~clk; arb_if arbif(clk); arb a1(.grant(arbif.grant), //.port(ifc.signal) .request(arbif.request), .rst(arbif.rst), .clk(arbif.clk)); test t1 (arbif); endmodule:top
3.使用modport将接口中的信号分组
前面的接口定义中,并没有提及信号的方向,而是使用了点对点的无信号方向的连接方式。在接口中使用modport结构能够将信号分组并指定方向。
下例代码中的monitor modport语句使测试平台能够连接到一个新增加的monitor模块。
例10 带有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。应当指出的是你需要将modport名即DUT或TEST放在接口名即arb_if的后面。
例11 接口中使用modport的仲裁器模型
module arb (arb_if.DUT arbif); ...... endmodule
例12 接口中使用modport 的测试平台
module test (arb_if.TEST arbif); ...... endmodule
顶层模块与例7一致,modport只在模块首部指明,而在模块例化时不需指明。
注意:在上面定义接口时,我们使用了modport MONITOR(例10),它能够将测试平台连接到一个新增加的monitor模块。
例13 接口使用modport的仲裁器模型
module monitor (arb_if.MONITOR arbif); always@(posedge arbif.request[0]) ...... endmodule
3.激励时序
在时钟周期级的测试平台,你需要在相对于时钟信号的合适的时间点驱动和接收同步信号。驱动得太晚或者采样得太早,测试平台的动作就会错过一个时钟周期。SystemVerilog有几种结构可以帮助你控制通信中的时序问题。
3.1 使用时钟块cb控制同步信号的时序
- 接口块可以使用时钟块来指定同步信号相对于时钟的时序。时钟块大都在测试平台中使用。
- 时钟块中的任何信号都将同步地驱动或采样,这就保证了测试平台在正确的时间点与信号交互。
- 一个接口可以包含多个时钟块,因为每个块中都只有一个时钟表达式,所以对应一个时钟域。
例14 带时钟块的接口
interface arb_if (input bit clk); logic[1:0] grant,request; logic rst; clocking cb @(posedge clk); //声明cb output request; input grant; endclocking modport TEST(clocking cb, //使用cb output rst); modport DUT (input request, rst, output grant); endinterface
从上面的例子可以看出,TEST modport将request和grant视为同步信号。
当在时钟块中使用modport时,任何同步接口信号的前面都必须加上接口名和时钟块名。arbif.cb.request是合法的,而arbif.request是不合法的。
//简单的测试平台 module test (arb_if.TEST arbif); initial begin arbif.cb.request<=0; @arbif.cb; $diaplay("@%0t:grant!=%0b",$time,arbif.cb.grant); end endmodule
arbif.cb表示时钟的有效沿,而不需要描述确切的时钟信号和边沿。arb.cb.request<=0,表示在时钟的有效沿将0的值赋给request。接口信号采用的是非阻塞赋值。
3.2 接口中的logic和wire对比
我们建议在接口中将信号定义为logic类型。logic类型的语句可以在过程语句中进行赋值也可以直接驱动,wire类型只能采用连续赋值assign。
3.3 程序块(Program Block)和时序区域(Timing Region)
测试平台应该不仅在逻辑上而且在时序方面独立于设计。通常测试平台和设计之间会存在竞争状态。会出现这种问题的根源在于设计和测试平台的事件混在同一个事件片内。如果存在一种可以在时间轴上分开这些事件的方法,确保能够在所有事件执行完毕后,测试平台开始下一个动作。那么将会很好地解决时序上的问题。
SystemVerilog是如何将测试平台的事件与设计的事件分开调度呢?
在SystemVerilog中,测试平台的代码在一个程序块中,这个与模块非常类似,但是,程序块不能有任何的层次级别,例如模块的实例、接口或者其他程序。
SystemVerilog引入一种新的时间片的划分方式。
- Active:仿真模块中设计代码
- Observed:执行SystemVerilog断言
- Reactive:执行程序中测试平台部分
- Postponed:为测试平台的输入采样,所有设计活动都结束的只读时间段采样信号。
例17 使用带有时钟块接口的测试平台
program automatic test(arbif.TEST arbif); ... initial begin arbif.cb.request<=2'b01; repeat(2) @arbif.cb; //@arbif.cb语句将等待时钟块给出有效沿@(posedge clk) if(arbif.cb.grant!=2'b01); $display ("@%0t:a1:grant!=2'b01",$time); end endprogram:test
测试代码应当包含在一个单个的程序块中。应当使用OOP通过对象而非模块来创建一个动态、分层的测试平台。
3.4 仿真的结束
第一种:遇到$finish结束
第二种:如果仅有一个程序块,那么当完成所有initial块中的最后一个语句时,仿真就结束了。如果存在多个程序块,仿真在最后一个程序块结束时结束。
第三种:执行 $exit可以提前终断任何一个程序块。
4 接口的驱动和采样
测试平台需要驱动和采样设计的信号,这主要是通过带有时钟块的接口做到的。
4.2 接口信号采样
接口要采样的信息对设计的输出信号进行采样。当你从时钟块中读取一个信号的时候,你是在时钟之前得到采样值。
例19 模块中同步接口的采样和驱动
'timescale 1ns/1ns; program test (arcif.TEST arbif); initial begin $ monitor("@%0t:grant=%h",$time,arbif.cb.grant); #50 $display("End of test"); end endprogram module arb(arb_if.DUT arbif); initial begin #7 arbif.grant=1; #10 arbif.grant=2; #18 arbif.grant=3; end endmodule
arbif.grant由一个模块驱动,可以使用阻塞赋值。
在测试平台中的program块中,当在时钟块中使用modport的时候,任何同步的接口信号都必须是arbif.cb.grant的形式。
4.3 接口信号驱动
时钟块的默认时序是在#1step延时之后采样输入信号,在#0延时之后驱动输出信号
#1step延时规定了信号在前一个时间片的Postponed区域,在设计有任何新的动作之前被采样。
#0,因为时钟模块的原因,测试平台的输出信号是同步的,所以他们直接送入设计中。
例4.22 驱动一个同步接口
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 #15 finish; end endprogram module arb(arb_if.DUT arbif); initial $monitor("@%0t:req=%h", $time,arbif.request); endmodule
从上面的例子我们可以看出,如果测试平台在时钟的有效沿驱动同步接口信号,那么其值将会立即传递到设计中。
例23 接口信号驱动
##2 arbif.cb,request<=0; //等待两个时钟周期然后赋值 ##3; //非法——必须跟赋值语句同时使用
表示等待两个时钟周期以后再驱动信号,后面必须跟着赋值语句否则是非法的。
4.6 为什么在程序(program)中不允许使用always块?
在SystemVerilog中,你可以在program中使用initial块,但是不能使用always块。这是因为在一个设计中,always块可能从仿真开始就在每一个时钟的上升沿进行触发执行。但是一个测试平台的执行过程是经过初始化、驱动和相应设计行为等步骤后结束仿真的。在这里,一个连续执行的always块不能正常工作。
如果在program块中加入always块,它将永远不会结束,这样必须调用$exit来发出程序块结束的信号。
如果你确实需要一个always块,你可以使用"initial forever"来完成相同的事情。
4.7 时钟发生器
时钟与其说跟测试平台结合得比较紧密,倒不如说它跟设计结合的更加紧密,所以时钟发生器应当定义成一个模块,而不是一个程序块,如果把时钟发生器放在program中,在Reactive中开始传递,到达Active中信号的先后顺序有可能引起0时刻的竞争。所以时钟边沿使用阻塞赋值生成,它们将在Active区域触发事件的生成。
例25 位于程序块中错误的时钟发生器
program bad_generator (output bit clk, out_sig); initial forever # 5clk=~clk; initial forever@(posedge clk) out_sig<=~out_sig; endprogram
clk和out_sig信号会引起竞争状态。
例26 模块中正确的时钟发生器
module clock_generator(output bit clk); initial always #5 clk=~clk; //在时间0之后生成时钟沿 endmodule
6.顶层作用域
在Verilog中,只有宏定义可以跨越模块的边界,而且经常被用来创建全局变量。SystemVerilog引入了编译单元(compilation unit),它是一起编译的源文件的一个组合。任何module,interface,program,package等边界之外的作用域被称为编译单元作用域,也成为$unit。
这个作用域的任何成员,比如parameter,都类似于全局成员,因为他们可以被所有低一级的块访问,但是又不同于真正的全局成员,比如parameter在编译时其他源文件不可见。下面的例子就显示了编译单元parameter、const的用法。
例31 仲裁器设计的顶层作用域
//root.sv `timescale 1ns/1ns parameter int TIMEOUT=1_000_000; const string time_out_msg="ERROR"; module top; test t1(); endmodule program automatic test; ...... initial begin #TIMEOUT; $display("%s",time_out_msg); $finish; end endprogram
实例名 $root允许你从顶层作用域开始明确地引用系统中的成员名。在这一点上它类似于Unix文件系统中的“/”。你可以通过$root指定绝对路径明确地指引跨模块的变量。下面给出几种跨模块引用的用法。在program块中,引用module top中的clk信号。
绝对引用:$root.top.clk
利用宏定义:'define TOP $root.top
'top.clk
相对引用:top.clk
例32 使用 $root的跨模块引用
`timescale 1ns/1ns parameter int TIMEOUT=1_000_000; top t1(); //顶层模块的显示例化 module top; bit clk; test t1(.*); endmodule 'define TOP $root.top program automatic test; ...... initial begin //绝对引用 $display("clk=%b", $root.top.clk); $display("clk=%b", 'top.clk); //使用宏 //相对引用 $display("clk=%b", top.clk); endprogram
7 程序——模块交互
程序块可以读写模块中的所有信号,可以调用模块中所有历程,这些历程可以改变内部信号的值,但是模块却看不到程序块。这是因为测试平台需要访问和控制设计,但是设计却又独立于测试平台的任何东西。在测试平台中使用DUT中的函数获取信息,模块中的函数可以封装两者之间的通信,并使得测试平台更便捷地跟设计保持同步。
8 System Verilog 断言Assertion SVA
使用SystemVerilog断言(SVA)在你的设计中创建时序断言。仿真器会跟踪哪些断言被激活,这样就可以在此基础之上收集功能覆盖率的数据。
8.1 立即断言:Immediate Assertion
bus.cb.request<=1; repeat(2) @bus.cb; a1:assert (bus.cb.grant==2'b01);
我们期望assert里面的内容是真的,否则输出错误信息。类似于if语句,但是等价的过程代码可能远比这些断言要复杂和冗长。
8.2 定制断言行为
a1:assert (bus.cb.grant== 2’b01) else $error("Grant not asserted");
assert断言可选then和else分句来改变默认信息,可以添加你自己想要的输出信息。
SystemVerilog有四个输出消息的函数:$info $warning $error $fatal。这些函数仅限于在断言内部使用。
8.3 并发断言
interface arb_if(input bit clk); logic[1:0] grant,request; logic rst; property request_2state; @(posedge clk) disable iff (rst); $isunknown(request)==0;//确保没有Z/X值存在 endproperty assert_request_2state:assert property(request_2state); endinterface
request信号除了在复位期间,其他任何时候都不是X或Z。
- 它是一个连续运行的模块,它为整个仿真过程检查信号的值。
- 在断言内指定了一个采样时钟。
8. ref端口
SystemVerilog引入了一种新的端口方向:ref。在Verilog中,我们熟悉的端口方向有:input、output、inout。
如果使用多个inout端口驱动一个信号,SystemVerilog将会根据所有驱动器的值、驱动强度来计算最终的信号值。
如果是多个ref端口驱动一个变量,就有可能产生竞争,因为多个模块的端口都可能更新同一变量。ref端口其实是对变量(不能是net)的引用,它的值是该变量最后一次赋的值。