转载请标明出处:
原文发布于:[浅尝辄止,未尝不可的博客](https://blog.csdn.net/qq_31019565)
最近我温习《SystemVerilog验证-测试平台编写指南(第二版)》这本书,看到了第4.3节,激励时序的问题,还是花了一些时间去看。SV这条路任重道远。本文中大部分是摘录,自己认为是重要的就留下了,希望可以加深理解,留下一些积累。有志同道合的朋友可以一起探讨,欢迎指教。
TestBench和DUT之间的合理配合是非常重要的。在时钟周期合适的时间点,驱动和接收同步信号显得尤为重要。驱动太晚或者采样太早,测试平台的动作就可能错过一个时钟周期。即使在同一个Time Slot中,比如同一个时刻100ns,TestBench和DUT也会引起竞争。一个信号的同时被写入和读出,读取到的值到底是旧的还是新的,这是一个很大的问题。在Verilog中,非阻塞赋值可以在TestBench驱动DUT的时候解决掉这个问题。但是TestBench不能够确保采样到的DUT的值是最新值。SystemVerilog中的几种结构或许可以对控制时序有所帮助。
时钟块主要是为了用来指定同步信号相对于时钟的时序。在同一个时钟块中,所有的信号都会被同步的驱动和采样。这样就保证了测试平台在正确的时间点与信号进行交互。时钟块主要是在TestBench中使用。
//example_1
//interface with clocking block
interface example_1_interface(input bit clk);
logic enable;
logic [1:0] request,grant;
//clocking cb_1
clocking cb_1 @(posedge clk);
default input #1step output #0;
input enable;
input grant;
output request;
endclocking:cb_1
//clocking cb_2
clocking cb_2 @(posedge clk);
default input #1step output #0;
input enable;
input grant;
input request;
endclocking:cb_2
//modport
modport mp_1(clocking cb_1);
modport mp_2(clocking cb_2);
endinterface:example_1_interface
如上代码所示,在一个interface中,可以包含多个clocking block。并且每一个块中都有一个时钟表达式,对应着一个时钟域。典型的时钟表达式 @(posedge clk),它定义了单时钟沿。示例代码中定义 default 语句,指定一个时钟偏移。但是在默认的情况下输入信号仅在设计执行前被采样,并且设计的输出信号在当前Time Slot又被驱动回当前设计,后文详述。
//example_2
//simple testbench : module test
module test(example_1_interface.mp_1 intf);
initial begin
intf.cb_1.enable <= 1'b1;
@intf.cb_1;
$display("%0t:Grant = %b",$time,intf.cb_1.grant);
end
endmodule
如上代码所示,一旦定义了clocking block,TestBench就可以使用类似于 @intf.cb_1 表达式进行时钟等待,而不需要确切的描述时钟的边沿。example_1中定义了modport。每个modport中的信号的方向都是相对于modport而言的,这些信号也只在对应的modport中使用。
这本书出于易用性角度,建议将 interface 中的信号定义为 logic 类型。这里只说比较结果,对于需要采用哪种不做争论。
//example_3
//introducing how to drive logic type signals and wire type signals
//interface example_3_interface
interface example_3_interface();
wire wire_type;
logic logic_type;
endinterface:example_3_interface
//simple testbench : module test
module test (example_3_interface intf);
logic local_wire;
assign = intf.wire_type = local_wire;
initial begin
intf.logic_type <= 0;//直接驱动异步logic信号
local_wire <= 1;//只能通过assign驱动wire信号
...
end
endmodule
DUT和TestBench 无论在逻辑上还是时序上都应该是相互独立的。在实际的硬件设计中,DUT中的存储单元在时钟的有效沿锁存输入信号。这些数值由存储单元输出,然后通过逻辑块到达下一个存储单元。从上一个存储单元的输入到下一个存储单元的输入的延时必须小于一个时钟周期。所以测试仪需要在时钟沿之后驱动芯片的输入,然后再下一个时钟沿之前读取输出。DUT应该在时钟有效沿或者有效沿之后被及驱动。然后在下一个有效沿到达之前,TestBench尽可能晚的进行采样。
问题被提出
如果DUT和测试程序只有Verilog构成,几乎是不可能实现的:
解决方案
方案一,给系统加小小的延迟,#0。
方案二,给系统加较大的延迟,#1。
由于弊端的存在,会避免#0,#1解决时序问题。
//example_4
//DUT:module memory
module memory(input wire start,write,
input wire [7:0] addr,
inout wire [7:0] data);
logic [7:0] mem[256];
always @(posedge start)begin
if (write)
mem[addr] <= data;
...
end
endmodule
//testbench : module test
module test(output logic start,write,
output logic [7:0] addr,data);
initial begin
start = 0;//信号初始化
write = 0;
#10;//短暂的延时
addr = 8'h42; //发起第一个命令
data = 8'h5a;
start = 1;
write = 1;
...
end
endmodule
如上代码所示,是一个DUT和TestBench可能存在竞争状态的实例。竞争状态出现在TestBench先产生Start信号的时候,当memory被start信号唤醒之后,write、addr、data,还保留着原来的值。TestBench和DUT都在用这些值,可能会造成竞争。同样对于DUT的输出,需要在时钟沿到达之前进行采样。
SystemVerilog的合理运用可以把TestBench和DUT的事件分开调度。SV中,TestBench中的代码在一个程序块儿中,跟模块非常类似。但区别是程序块不能够有任何的层次级别。
SystemVerilog引入了一种新的时间片的划分方式。
将Time Slot划分为有序的区域是为了实现DUT和TestBench之间互相作用的可预测性。
SystemVerilog主要调度区域:
区域名 | 行为 |
---|---|
Active | 仿真模块中的设计代码(RTL、时钟发生器、门级代码) |
Observed | 执行SystemVerilog 断言 |
Reactive | 执行程序中的测试平台部分 |
Postponed | 为测试平台的输入采样信号 |
时钟块的默认时序是在#1step延时之后采样输入信号,在#0延时之后驱动输出信号,1step延时规定了信号在前一个时间片的postpone区域,在设计有任何新的动作之前被采样,这样就可以在时钟改变之前获得输出值,因为时钟模块的原因,测试平台的输出信号是同步的,所以它们直接送入设计中。在Reactive区域运行的程序块在同一个时刻再一次触发Active区域。
在Verilog中,仿真在调度事件存在的时候回继续执行,直到遇到$finish。SystemVerilog增加了一种结束仿真的方法。SV中任何一个程序块都被视为含有一个测试,如果仅仅有一个程序块,那么当完成所有initial块中的最后一个语句的时候,仿真就结束了。因为编译器认为这就是测试的结尾,即使还有模块或者程序块的线程在运行仿真也会结束。所以当测试结束时,无需关闭所有的monitor和driver。如果存在多个程序块,那么在最后一个程序块结束时,仿真结束。也可以用$exit提前中断一个程序块,或者用$finish结束仿真。
参考文献:
1、《SystemVerilog验证–测试平台编写指南(原书第二版)》
2、IEEE Standard for SystemVerilog—Unified Hardware Design,Specification, and Verification Language(version 2017)