SV 接口
- 前言
- 一、接口什么时候用?如何用?
- 二. 接口的驱动和采样
- 1. 接口同步
- 2. 接口信号 采样 和 驱动
- 3. 接口中的双向信号
- 4.时钟发生器
- 5. 为什么在program 程序中不允许使用always块
- 6. 连接模块
- 7. verilog 端口
- 8. SV 端口
- 9. SV 接口类型的端口声明
- 10. 接口的modport链接
- 11. 接口的总结:
- 12. 接口应用于模块与模块之间
- 12. 接口应用于接口与端口之间
- 13. 类和模块之间的连接
- 14. 端口模式和时钟控制块
- 15. 接口和模块的对比
- 16. 接口中的logic 和 wire 对比
- 17. 时序问题
- 18. 程序块和时序区域(难点)
- 19. 仿真的结束
- 20. 指定设计和测试平台之间的时延
- 21. 顶层作用域
- 22. 程序-模块交互
前言
SV 第四章 SV 接口,读书笔记
一、接口什么时候用?如何用?
作用:1. 便于设计重用;减少了连接错误的可能性;
2. 接口包含了连接,同步,通信的功能。
3. 接口连接可以看出来能使代码变得简洁不易出错,如果想拓展接口中的信号,只需在接口中定义和使用这个信号的模块做修改,不需4. 要其他操作,这种特性极大降低了连接出错的几率。
什么时候用?:当两个块之间由两个以上的信号连接,并且使用特定的协议通信的时候,应当考虑使用接口;信号组一次次的出现,例如 网络交换机
如何用?: 使用接口时需要确保在你的模块和程序块(program block,软件建模)之外声明接口变量(就是说不能将接口定义在module和program内)。有些编译器不支持在模块中定义接口,及时某些编译器支持,接口也只是所在模块的局部变量,对设计的其他部分来说是不可见的。
1. 接口中的logic和wire对比
- logic:在测试平台的接口中使用过程赋值语句驱动一个异步信号,logic信号可以直接被驱动,但是logic不能用于一个信号有多个元件的驱动,必须用wire类型;
- wire:变量只能被连续赋值语句驱动;否则需要使用额外的代码;
2. verilog的时序问题
从上一个存储单元的输入到下一个存储单元的输入的延时必须小于一个时钟周期。所以测试平台 需要在时钟沿之后驱动芯片的输入,然后再下一个时钟沿之前读取输出。
如果时钟到达一些DUT的时间快于测试平台的激励,但是到达另一些DUT的时钟晚于这个激励。解决办法是给系统添加一点小的延迟#0,或者较大的延时#1,解决时序问题。
但是如果DUT是一个含有无延时信息的RTL代码和有延时信息的门级代码混合。
3. 设计—测试平台 间的 竞争状态
如:内存被start信号唤醒,write addr data 信号仍然保留着原来的值。
4. 仿真的结束
如果有一个程序块,完成initial块 或者 $finish
后仿真就结束了;
如果存在多个程序块,可以用$exit
提前中断任何一个程序块,也可以使用$finish
结束仿真;
二. 接口的驱动和采样
1. 接口同步
测试平台需要驱动和采用设计的信号,主要时通过带有时钟块的接口做到的。
异步信号通过接口时没有任何延时,如 rst,而时钟块的信号将得到同步。
@ bus.cb //在时钟块的有效时钟沿继续
repeat(3) @ bus.cb // 等待3个有效时钟沿
@ bus.cb.grant // 在任何边沿继续
@(posedge bus.cb.grant) // 上升沿继续
wait (bus.cb.grant == 1) //等待表达式被执行,如果已经是真,不做任何延时
2. 接口信号 采样 和 驱动
采样:arbif.cb.grant 在时钟沿到来之前采样,但是可能相对DUT,test信号采样晚两个时钟周期;
驱动:arbif(接口名).cb(时钟块名).grant (信号),一定这样写,不能跳跃层次
在时钟块中 应该使用同步驱动,即“<=”来驱动信号,因为 信号在赋值后并不会立即改变,测试平台在Reactive区域执行,设计代码在Active区域执行。
如果仅由一个模块驱动信号,可以使用阻塞赋值;非阻塞赋值 可减少竞争状态;
测试平台 在时钟有效沿 驱动同步接口信号,其值会立即传递到设计中;
测试平台 在时钟有效沿之后 驱动同步接口信号,其值将会在下一个有效沿传递到设计中;
为避免上述可能会丢失 测试的data或者赋值 而不能驱动到进入设计中;
最好用如下方式:
##2 arbif.cb.request <= 0; // 等待两个时钟周期后赋值
或者可以写成repeat(2) @bus.cb
3. 接口中的双向信号
Verilog中:如果想驱动一个双向信号,如 过程中的双向端口,需要用一个连续赋值语句来将reg连接到wire;
SV中:如果是同步信号,用连续赋值; 异步信号:虚接口或者跨模块引用和连续赋值语句
program中不允许使用always块
4.时钟发生器
时钟发生器 应该放在一个模块中,这样其与设计结合的更加紧密;创建一个时钟树,随着时钟信号进入系统并在块之间传递的时候,必须仔细地控制时钟地抖动。
1.1 在时钟0之后(#5)产生时钟沿,一般所有的 时钟边沿 使用阻塞赋值生成
module clk_generater(output bit clk)
initial
always #5 clk = ~clk;// 在时钟0之后(#5)产生时钟沿
endmodule
1.2 如果你确实需要在时钟0时刻产生一个时钟边沿,那么可以使用 非阻塞赋值语句 设置初始
值, 这就保证了时序逻辑电路比如always块都会在 时钟变化之前 执行
2.0 在program块中,使用forever生成时钟:这是错误的
initial
forever # 5 clk <= ~clk;
initial
forever (@posedge clk)
out_sig <= ~ out_sig;
不应该把时钟发生器放在程序块里,放在program里会引起信号之间的竞争,out_sig 和 clk
都从reactive区域进入设计。
功能验证 只关心 在正确的时钟周期内提供正确的值,而不是纳秒级的延时和时钟的相对偏移。
不要使用功能验证 来验证 底层时序,应该在静态时序分析工具中完成
5. 为什么在program 程序中不允许使用always块
SV中,program中可以使用initial,因为最后一个initial结束的时候,仿真实际上也默认结束了,就像执行了$finish
一样; 但是加入了 always块 , 它将会永远不结束,必须调用$exit
来发出结束program快的信号
6. 连接模块
module top;
bit clk;
always #4 clk = ~clk;
arb_if arbif(.*); // 连接测试和设计地接口
arb a1(.*); //设计模块
test t1(.*);//测试模块
endmodule: top
使用 (.*)隐式端口连接,自动连接 模块实例的端口到具体信号, 只要端口 和 信号地名字
和数据类型相同。
7. verilog 端口
- verilog 端口声明:
module count ( inout wire[31:0] data,
output reg[31:0] result,
output reg co,
input [31:0] a,b
iput tril ci);
...
endmodule
//verilog里端口的名称,方向,位数和类型都可以在端口列表声明
//也可以在端口列表只声明端口名称,在module内声明方向,位宽和类型
//若没有显式声明端口类型,则默认wire类型;默认位宽为类型的默认位宽
//上例中,a,b默认类型为wire,co的位宽为1
//为避免诸多不必要的麻烦,声明端口时,尽量名称+方向+位宽+类型一并声明
一些限制:所有端口必须显式声明方向;必须重新指定端口方向,才能改变后面端口类型;必须重新指定端口方向和类型,才能改变后面的端口位宽。
- verilog端口的连接:
//位置对应方式连接
module and_1( a,b,c); // 端口声明,模块定义
input a,b;
output c;
assign c=a&b;
endmodule
module logic(in1,in2,q); // 端口声明,模块定义
inpu in1,in2;
output q;
and_1 U1 (in1,in2,q); //模块例化连接, 按顺序,a连in1,b连in2,c连q
endmodule
//端口对应方式连接
module and_1( ..) // 端口声明,模块定义
..
endmodule
module logic(..); // 端口声明,模块定义
..
and_1 U1(.a(in1),.b(in2),.c(q)); //模块例化连接 //按位置,a连in1,b连in2,c连q
//.选择底层模块的端口,后面()里是连接的顶层模块端口
如果and_1模块连接很多个模块,则需要其他每个模块都要在and_1模块要声明一次,连接时又要声明一次。
而sv接口中,将所有模块端口信号包裹在interface和endinterface中,集中声明然后重复使用。
endmodule //用这种方法可以不按顺序写
8. SV 端口
- sv端口:
sv的端口在verilog端口基础上做了一些延伸扩展:
指定了缺省方向为inout;端口列表中下一个端口定义了类型但没有方向,则方向参照前一个端口。
需要注意:如果第一个端口既没有方向也没有类型,那么后面的所有端口都不能有方向和类型。
可以理解为此时端口列表只声明端口名称,方向和类型都在模块内声明。
9. SV 接口类型的端口声明
//显式声明一个接口类型的端口
interface chip_bus;
...
endinterface
module ram (chip_bus pins, input clock);
//在verilog学习后,刚接触sv接口总是想写成“方向+类型+名称”,比如 input logic[3:0] abc;
//这里,chip_bus已经是一个interface类型,
//且interface类型本身就可以具有input,output或者inout方向
//可以理解成chip_bus本身已经包括方向+类型,而pins就是变量名称
..
endmodule
//隐式声明一个接口类型端口
interface chip_bus;
...
endinterface
module arm (interface pins, input clock);
//参照上例,“方向+类型+名称”,明显没有声明“方向+类型”,而是用interface代替,名字为pins;
//这里只是举例隐式声明的方法,但个人来看,在实际应用中,interface的定义不止一个,
//如果都用隐式声明,个人理解会有不妥,所以实际编写代码,应该显式声明最稳妥
1.显式声明的 接口类型端口,必须连接到同一类型接口实例
2.隐式声明的 接口类型端口,可以连接到任何类型接口实例
10. 接口的modport链接
编辑器会依次来检查连线方向是否发生错误。在接口中使用modport结构能够将信号分组并指定方向。
在设计中可以通过两种方法使用modport名:
-
一种是在接口信号的程序和模块中使用modport名;
-
另外也可以在顶层模块中使用modport名,然后把接口放到程序和模块的端口表中(module,program中使用interface声明,top层使用interface.modport声明)。
正常情况下,使用第一种连接方式,因为modport是接口实现的细节,不应该出现在顶层模块中。特殊情况,如果一个模块需要多次例化,每次例化需要连接到不同的modport,那么此时应该使用第二种连接方式。
//在例化时选择modport
interface chip_bus(input logic clock,rest_n);
logic interrupt_request, grant, ready;
logic [31:0] address;
wire [63:0] data;
modport master ( input interrupt_request,
input address;
output grant, ready,
inout data,
input clock, reset_n); //接口中相对于master的信号及方向
modport slave ( output interrupt_request,
output address;
input grant, ready,
inout data,
input clock, reset_n); //接口中相对于slave的信号及方向
endinterface //modport中只定义信号对于不同模块的方向,而类型和位宽在接口中声明即可。
定义端口时就选择连接的modport
module primary (chip_bus pins);
..
endmodule
module secondary (chip_bus pins);
..
endmodule
module chip (input logic clock,restn);
chip_bus bus (clock,resetn); //连接接口前一定要例化接口
primary i1 (bus.master); //例化primary时选择连接bus中的modport master
secondary i2 (bus.slave); //例化secondary时选择连接bus中的modport slave
endmodule
11. 接口的总结:
interface my_intf (..);
..
endinterface
module abc (my_intf intf);//接口类型端口,名称为intf
..
endmodule
module top (..);
my_intf intf (..);//例化名称为int
f
abc U1 (.*)
..
endmodule
//此时 **接口例化名称 和 模块内端口名称** 相同,连接时可用.*自动连接
//如果不同,则用.索引端口再连接
注意:
1.如果一个模块端口列表中有接口类型的端口,那么这个端口必须被连接到 接口实例或者其它接口类型端口
12. 接口应用于模块与模块之间
interface arb_if(input bit clk);
logic [1:0] grant,request;
logic rst;
endinterface
module arb(arb_if arbif);
//...
always@(posedge arbif.clk or negedge arbif.rst)
begin
if(arbif.rst)
arbif.grant <= 0;
else
arb_if.grant = this.grant;
end
endmodule
module test(arb_if arbif);
//...
initial begin
//...
end
endmodule
module top;
bit clk;
always #5 clk=!clk;
arb_if arbif(clk);
arb a1(arbif);
test t1(arbif);
endmodule
12. 接口应用于接口与端口之间
module arb_port(input logic [1:0] request,output logic [1:0]grant,input logic clk,input logic rstn);
//...
endmodule
module top;
bit clk;
always #5 clk = !clk;
arb_if arbif(clk); arb_port(.grant(arb_if.grant),.request(arb_if.request),.clk(arb_if.clk),.rstn(arb_if.rstn));
endmodule
13. 类和模块之间的连接
interface SBus;
endinterface
class a;
virtual SBus bus;
function new(virtual SBus s)
bus = s;//传入一个interface给到这里的上面定义的bus中
endfunction
//...
endclass
对比:
- 对于第一点,在没有接口的情况下,模块与模块之间的连接实际上通过模块间独立的信号连接;
- 当存在接口时,是通过一个公共接口分别不同模块间的端口连接,在外部环境中没有各个独立的信号,而是只存在一个接口;
- 第三点实际上是实现类和模块之间的连接,在顶层模块中同样是实现模块和接口的连接,但是在该接口需要在类中初始化,最终实现模块和类的连接
总结
14. 端口模式和时钟控制块
modport:modport只能做方向的声明,而不能声明变量,也就是modport中所有的变量必须在interface中先声明,才能在modport中规定方向
clocking:不仅可以定义在interface,也可以定义在module,program中;另外clocking列举的的信号不是自己定义的,而是由interface定义的或者其他声明clocking的模块定义的。
(1)利用clocking采样/利用clocking做数据驱动/接口的同步:repeat (3) @bus.cb表示等待三个有效时钟沿(2)时钟块中使用modport时,同步信号需要加上接口名和时钟块名前缀。(可以采用clocking块去解决采样过程中的竞争问题)
竞争问题:比如clk1驱动clk2(信号是clk1同步信号)和d1(信号是d+1的信号),那么在clk1上升沿采集d1为1,那么等到clk2上升沿再次采集d1时才会变成2.这是由于实际上clk2和d1信号均是由clk1驱动的,因此均和clk1存在一个延迟,也就是在clk1上升沿时采样,clk2和d1信号均没有发送驱动,而当在clk2上升沿采样时,clk2和d1相较于clk1的延迟已过去,所以此时采样实际时d1+1,也就是为2.
接口块可以使用时钟块来指定同步信号相对于时钟的时序。时钟块中的任何信号都将同步地驱动或采样,这就保证了测试平台在正确的时间点与信号交互。 一个接口可以包含多个时钟块,因为每个块中都只有一个时钟表达式,所以每一个对应一个时钟域。可以在时钟块中使用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
15. 接口和模块的对比
模块的端口如果声明为input,output或者inout时,那么列化时可以不连接;模块端口如果声明为interface,那么例化时必须连接到一个接口实例或者另外一个接口端口
接口端口指的是接口中声明端口,比如外部接入的时钟信号或者复位信号(接口端口中一般只定义一些时钟信号或复位信号等公共信号,interface bus(input logic clock);
接口无法例化模块,但是接口可以例化接口;接口内部可以声明所以变量或者线网(注意是内部);接口和模块的连接(将接口与端口逐一连接,在顶层top中,采用模块名 实例名(接口名)的方式)。
软件中不能例化接口, 但是可以利用接口的指针找到接口的实例,进而找到实例中的信号。
16. 接口中的logic 和 wire 对比
logic: 用于接口中的信号,可以直接被驱动;
wire : 只能被连续赋值语句驱动,是时钟块中的信号始终是同步的;接口中的wire类型不能直接驱动,需在module中定义logic类型驱动信号,使用assign赋值语句将信号线链接,module中通过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
17. 时序问题
在实际的硬件设计中,DUT中的存储单元在时钟的有效沿锁存输入信号,这些数值由存储单元输出,然后通过逻辑块到达下一个存储单元。从上一个存储单元的输入到下一个存储单元的输入延时必须小于一个时钟周期。所以测试仪需要在时钟沿之后驱动芯片的输入,然后在下一个时钟之前读取输出。测试平台应该模拟测试仪这种行为,应该在有效时钟边沿或边沿之后驱动待测设计,然后在有效时钟沿到达之前,在满足协议时序的前提下,尽可能晚地采样。
如果DUT和测试程序仅由Verilog模块构成,这几乎是不可能实现的。如果测试平台在时钟边沿驱动DUT,就会存在竞争状态。 如果时钟到达一个DUT的时间快于测试平台的激励,但是到达另一个DUT的时钟又晚于这个激励,这种情况会导致DUT的外部时钟沿都是在相同的仿真时间达到,DUT内部有一些输入在上个时钟周期采样,但是其他的输入却在当前时钟周期采样。
解决此问题的一种方法是给系统添加延时。比如#0,0时延会强迫V代码的线程停止并在所有其他代码完成之后被重新调度执行。但在一个大型的设计中,往往不可避免地存在多个线程都想最后执行,#0带来的结果是每次运行结果不确定,所以要避免使用#0以免代码不稳定并且不可移植。
另外一个解决方法是使用一个较大的延时,#1。 RTL代码除了时钟沿之外没有其他时序信息,所以逻辑电路在时钟沿之后一个时间单位就会稳定。 但是如果一个模块使用1ns时间精度,而其他仅使用10ps的时间精度呢,那么#1意味着1ns, 10ps 还是其他的时间长度呢。**你需要在时钟的有效沿之后,并且是在任何事件发生之前,而非在一段时间之内,尽快地驱动设计。**所以应当避免使用#1延时解决时序问题。
对DUT输出信号的采样存在着相同的问题,希望在时钟有效沿到来之前的最后时刻捕获信号的值, 你可能直到下个时钟沿会出现在100ns,但是不能在100ns出现时钟边沿的时钟采样,因为设计的输出值可能已经改变了,应当在时钟沿到达之前的 Tsetup 时间上采样。
18. 程序块和时序区域(难点)
竞争问题的根源在于设计和测试平台的事件混合在同一个时间片(time slot)内, 即使在纯RTL程序中也会发生同样的问题。如果存在一种可以将时间轴上分开这些时间的方法,如在100ns时刻,测试平台可以在时钟信号变化或者设计产生任何活动之前采样设计的输出信号。在所有的事件执行完毕后,测试平台开始下一个动作。在SV中,测试平台的代码在一个程序块中,但是程序块中不能有任何的层次级别,例如模块的实例,接口或者其他程序。
SV引入了一种新的时间片的划分方式,在V中,大多数的时间在有效区域执行。在一个时间片首先执行Active区域,在这个区域中运行设计事件,包括RTL,门级代码和时钟发生器。第二个区域是Oberved区域,执行断言。接下来就是执行测试平台的Reactive区域。注意时间并不是单向向前流动,Observed和Reactive区域的事件可以触发本时钟周期内Active区域中进一步的设计事件。最后是Postponed区域,他将在时间片的最后,所有设计活动都结束后的只读时间段采样信号。
19. 仿真的结束
在V中,仿真在调度事件存在的时候会继续执行,直到遇到$finish
。
SV中增加了一种结束仿真的方法。SV把任何一个程序块都视为含有一个测试,如果仅有一个程序块,那么当完成所有的initial
块中的最后一个语句时,仿真就结束了,因为编译器认为这就是测试的结尾。即使还有模块或者程序块的线程在运行,仿真也会结束。所以,当测试结束时无需关闭所有的monitor和driver。如果存在多个程序块,仿真在最后一个程序块结束时结束,这样最后一个测试完成时仿真就会结束。可以执行$exit
提前中断任何一个程序块,也可以使用$finish
来结束仿真。
仿真并没有完全结束。模块或者程序块,可以定义一个或者多个final块执行仿真器退出前的代码。
program
int errors,warnings;
initial begin
...
end
final
$display("Test done with %0d errors and %0d warnings", errors, warnings);
endprogram
20. 指定设计和测试平台之间的时延
时钟块的默认时序是在#1step延时之后采样输入信号,在#0延时之后驱动输出信号。
#1step延时规定了信号在前一个时间片的Postponed区域,在设计有任何新的动作之前被采样,这样就可以在时钟改变之前捕获输出值。
因为时钟模块的原因,测试平台的输出信号是同步的,所以他们直接被送入设计中,在Reactive区域运行的程序块在同一个时间片再一次触发Actie区域。可以想象时钟块在设计和测试平台中插入了一个同步器来理解。(不理解)
21. 顶层作用域
有时需要在仿真过程中创建程序或者模块之外的对象,以便参与仿真的所有对象都可以访问他们。在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
指定绝对路径明确地引用跨模块的变量。
22. 程序-模块交互
程序块可以读写模块中的所有信号,可以你调用模块中的所有例程(这个例程可以改变内部信号的值,称为后门,SV需要在设计中写一个任务改变信号的值,在程序中调用这个任务),但模块看不到程序块。这是因为测试平台需要访问和控制设计,但是设计却独立于测试平台中的任何东西。
测试平台可以用函数从DUT获取信息是一个好办法。