转载请标明出处:
原文发布于:[浅尝辄止,未尝不可的博客](https://blog.csdn.net/qq_31019565)
Verilog时序问题和SystemVerilog TestBench激励时序
最近我温习《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中使用。
接口中的 logic 和 wire 对比
这本书出于易用性角度,建议将 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
- 在接口中如果使用过程赋值语句驱动一个异步信号,那么该信号必须是 logic 类型。
- wire类型变量只能被连续赋值语句驱动。
- 时钟块儿中的信号时钟是同步的,可以定义为logic或者wire。
- 如上代码所示,logic 信号可以直接驱动,而 wire 需要额外的代码。
- 接口中的信号使用logic的另一个原因是,如果使用了多个元件的驱动源,编译器会自动报错。
- 如果当前项目代码重用于将来,变化的是一个logic信号被多个元件驱动,而该信号没有在任何一个时钟块,那么将不得不修改logic类型。
Verilog的时序问题
DUT和TestBench 无论在逻辑上还是时序上都应该是相互独立的。在实际的硬件设计中,DUT中的存储单元在时钟的有效沿锁存输入信号。这些数值由存储单元输出,然后通过逻辑块到达下一个存储单元。从上一个存储单元的输入到下一个存储单元的输入的延时必须小于一个时钟周期。所以测试仪需要在时钟沿之后驱动芯片的输入,然后再下一个时钟沿之前读取输出。DUT应该在时钟有效沿或者有效沿之后被及驱动。然后在下一个有效沿到达之前,TestBench尽可能晚的进行采样。
问题被提出
如果DUT和测试程序只有Verilog构成,几乎是不可能实现的:
- 如果TestBench在时钟边沿驱动DUT,会造成竞争现象。
- 如果时钟到达一些DUT的时间快于激励,但是到达另一些DUT的时间又晚于激励,这样会导致:在DUT外部,时钟在相同的时间到达;在DUT内部,有些输入在上一个时钟周期采样,但是其他输入在当前的时钟周期采样。
解决方案
方案一,给系统加小小的延迟,#0。
方案二,给系统加较大的延迟,#1。
由于弊端的存在,会避免#0,#1解决时序问题。
DUT和TestBench间的竞争状态
//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的输出,需要在时钟沿到达之前进行采样。
程序块(Program Block)和时序区域(Timing Region)
SystemVerilog的合理运用可以把TestBench和DUT的事件分开调度。SV中,TestBench中的代码在一个程序块儿中,跟模块非常类似。但区别是程序块不能够有任何的层次级别。
SystemVerilog引入了一种新的时间片的划分方式。