接口、程序块与断言
1 接口
1.1 使用接口简化连接
// 接口
interface arb_if(input bit clk);
logic [1:0] grant,request;
logic rst;
endinterface
// 使用了简单接口的仲裁器
module arb (arb_if arbif);
...
always@(posedge arbif.clk or posedge arbif.rst)
begin
if(arbif.rst)
arbif.grant <= 2'b00;
else
arbif.grant <= next_grant;
...
end
endmodule
// 使用简单仲裁器接口的测试平台
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)
$display("@%0t:a1:grant!=2'b01",$time);
$finish
end
endmodule
// top模块
module top;
bit clk;
always #5 clk = ~clk;
arb_if arbif(clk);
arb a1(arbif);
test t1(arbif);
endmodule
1.2 使用modport将接口中的信号分组
在接口中使用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
// 接口中使用modport的测试平台
module test (arb_if.TEST arbif);
...
endmodule
// 接口中使用modport的仲裁器监视模块
module monitor(arb_if.MONITOR arbif);
always@(posedge arbif.request[0]) begin
$display("@%0t:request[0] asserted",$time);
@(posedge arbif.grant[0]);
$display("@%0t:grant[0] asserted",$time);
end
always@(posedge arbif.request[1]) begin
$display("@%0t:request[1] asserted",$time);
@(posedge arbif.grant[1]);
$display("@%0t:grant[1] asserted",$time);
end
endmodule
1.3 使用时钟块
采样时发生竞争(delta cycle的存在),会导致采样数据错误。为了避免在RTL仿真中发生信号竞争的问题,建议通过非阻塞赋值或者特定的信号延迟来解决同步问题。这里我们介绍使用clocking时钟块来决定信号的驱动和采样的方式。
delta cycle
存在的问题:
- 在RTL仿真时,由于无法确定具体电路的延迟时间,默认情况下时钟驱动电路时会添加一个**无限最小的时间(delta cycle)**的延迟,这个延迟要比最小时间单位精度还要小(可以理解成远小于1ps)。
- 由于各种可能性,clk与被采样数据之间如果只存在若干个delta-cycle的延迟,那么采样就会出问题。
在时钟上升沿采样信号,只看vld信号(1->0)就会疑惑采样到的是1还是0,在RTL仿真时,是看不到真实的物理时序信息的,但实际采样到的是1;真实电路对应的时vld_actual信号,它的变化会较clk有一个detal-cycle的延迟,这样看在时钟上升沿采集到的是1。
还有一种方法就是,设置采样vld信号的时间,设置在时钟上升沿之前 #t 采样,这样采出来的数据也是准确的;同样也可以设置信号输出时间,设置在时钟上升沿之后 #t 输出,这样输出的数据也是准确的;
interface chnl_intf(input clk, input rstn);
logic [31:0] ch_data;
logic ch_valid;
logic ch_ready;
logic [ 5:0] ch_margin;
// 定义时钟块
clocking drv_ck @(posedge clk);
//采样时间
default input #1ns output #1ns; //在clk上升沿的前1ns对其进行输入采样,在事件的后1ns进行输出驱动
//声明变量方向
output ch_data, ch_valid;
input ch_ready, ch_margin;
endclocking
endinterface
在SystemVerilog中引入时钟块是为了解决在写testbench时对于特定时序和同步处理的要求而设计的。
时钟块是在一个特定的时钟上的一系列同步的信号,它基本上能够将testbench中与时序相关的结构、函数和过程块分开,能够帮助设计人员根据transaction 和 cycle完善testbench,时钟块只能在module、interface或program中声明。
// 可置位的二进制计数器
module COUNTER (input Clock, Reset, Enable, Load, UpDn, input [7:0] Data, output reg[7:0] Q);
always @(posedge Clock or posedge Reset)
if (Reset)
Q <= 0;
else
if (Enable)
if (Load)
Q <= Data;
else
if (UpDn)
Q <= Q + 1;
else
Q <= Q - 1;
endmodule
// 不采用时钟块的testbench
module Test_Counter;
timeunit 1ns;
reg Clock = 0, Reset, Enable, Load, UpDn;
reg [7:0] Data;
wire [7:0] Q;
reg OK;
// Clock generator
always
begin
#5 Clock = 1;
#5 Clock = 0;
end
// Test stimulus
initial
begin
Enable = 0;
Load = 0;
UpDn = 1;
Reset = 1;
#10; // Should be reset
Reset = 0;
#10; // Should do nothing - not enabled
Enable = 1;
#20; // Should count up to 2
UpDn = 0;
#40; // Should count downto 254
UpDn = 1;
// etc. ...
end
// Instance the device-under-test
COUNTER G1 (Clock, Reset, Enable, Load, UpDn, Data, Q);
// Check the results
initial
begin
OK = 1;
#9;
if (Q !== 8'b00000000)
OK = 0;
#10;
if (Q !== 8'b00000000)
OK = 0;
#20;
if (Q !== 8'b00000010)
OK = 0;
#40;
if (Q !== 8'b11111110)
OK = 0;
// etc. ...
end
endmodule
// 采用时钟块的testbench
module Test_Counter_w_clocking;
timeunit 1ns;
reg Clock = 0, Reset, Enable, Load, UpDn;
reg [7:0] Data;
wire [7:0] Q;
// Clock generator
always
begin
#5 Clock = 1;
#5 Clock = 0;
end
// Test program
// 将验证部分与设计部分进行隔离(实现方式就是将软件验证部分放置program中)
program test_counter;
// SystemVerilog "clocking block"
// Clocking outputs are DUT inputs and vice versa
default clocking cb_counter @(posedge Clock);
default input #1step output #4;
output negedge Reset;
output Enable, Load, UpDn, Data;
input Q;
endclocking
// Apply the test stimulus
initial begin
// Set all inputs at the beginning
Enable = 0;
Load = 0;
UpDn = 1;
Reset = 1;
// Will be applied on negedge of clock!
##1 cb_counter.Reset <= 0;
// Will be applied 4ns after the clock!
##1 cb_counter.Enable <= 1;
##2 cb_counter.UpDn <= 0;
##4 cb_counter.UpDn <= 1;
// etc. ...
end
// Check the results - could combine with stimulus block
initial begin
// Sampled 1ps (or whatever the precision is) before posedge clock
##1 assert (cb_counter.Q == 8'b00000000);
##1 assert (cb_counter.Q == 8'b00000000);
##2 assert (cb_counter.Q == 8'b00000010);
##4 assert (cb_counter.Q == 8'b11111110);
// etc. ...
end
// Simulation stops automatically when both initials have been completed
endprogram
// Instance the counter
COUNTER G1 (Clock, Reset, Enable, Load, UpDn, Data, Q);
// Instance the test program - not required, because program will be instanced implicitly.
endmodule
2 程序块
2.1 程序块和时序区域
SystemVerilog 如何把测试平台的事件和设计的事件分开调度?
SystemVerilog 引入一种新的时间片的划分方式,如下图所示。
- 在一个时间片内首先执行的是
Active
区域,在这个区域中运行设计事件,包括 RTL 、门级代码和时钟发生器。 - 第二个区域是
Observed
区域,执行断言。 - 接下来就是执行测试平台的
Reactive
区域。注意到时间不是单向地前向流动 ——Observed
和Reactive
区域的事件可以触发本时钟周期内Active
区域中进一步的设计事件。 - 最后就是
Postponed
区域,它将在时间片的最后,所有设计活动都结束后的只读时间段采样信号。
Active
:仿真模块中的设计代码
Observed
:执行SystemVerilog断言
Reactive
:执行程序中的测试平台部分
Postponed
:为测试平台的输入采样信号
// 使用带有时钟块接口的测试平台
program automatic test (arb_if.TEST arbif);
...
initial begin
arbif.cb.request <= 2'b01;
$display("@%0t:Drove req=01",$time);
repeat(2) @arbif.cb; // @arbif.cb 语句将等待时钟块给出的有效沿 @(posedge clk)
if(arbif.cb.grant != 2'b01)
$display("@%0t:a1:grant != 2'b01",$time);
end
endprogram : test
2.2 仿真的结束
在verilog中,仿真在调度事件存在时会继续执行,直到遇到 $finish
。
SystemVerilog新增加了一种结束仿真的方法:SystemVerilog把任何一个程序块都视为含有一个测试,如果仅有一个程序块,那么当完成所有initial块中最后一个语句时,仿真就结束了,因为编译器认为这就是测试的结尾,即使还有模块或者程序块的线程在运行,仿真也会结束。如果存在多个程序块,仿真在最后一个程序块结束时结束,可以执行 $exit
提前中断任何一个程序块。
2.3 程序块(program)中不允许使用 always
块
在SystemVerilog中,可以在 program
中使用 initial
块,但是不能使用 always
块。SystemVerilog程序比由许多并行执行的块构成的Verilog 更接近C程序,它拥有一个(或多个)程序入口。在一个设计中,一个 always
块可能从仿真的开始就会在每一个时钟的上升沿触发执行。但是一个测试平台的执行过程是经过初始化、驱动和响应设计行为等步骤后结束仿真的。在这里,一个连续执行的 always
模块不能正常工作。
当 program
中最后一个initial块结束的时候,仿真实际上也就默认结束了,就像执行了 $finish
一样。如果加入了一个always块,它将永远不会结束,这样就不得不明确地调用 $exit
来发出程序块结束的信号。如果确实需要一个always块,可以使用 initial forever
来完成相同的事情。
3 断言
可以使用SystemVerilog断言(SVA)在设计中创建时序断言。断言的例化跟其他设计块的例化相似,而且在整个仿真过程中都是有效的。仿真器会跟踪哪些断言被激活,这样就可以在此基础上收集功能覆盖率的数据。
3.1 立即断言
测试平台的过程代码可以检查待测设计的信号值和测试平台的信号值,并且在存在问题的时候采取相应的行动。
例如,如果产生了总线请求,期望在两个时钟周期后产生应答,可以使用一个if语句来检查这个应答。
// 使用一个if语句检查一个信号
bus.cb.request <= 1;
repeat(2) @bus.cb;
if(bus.cb.grant != 2'b01)
$display("Error, grant != 1");
// 简单的立即断言
bus.cb.request <= 1;
repeat(2) @bus.cb;
a1: assert (bus.cb.grant == 2'b01);
// ----------------------------------
// 如果正确地产生了grant信号,那么测试继续执行,如果信号不符合期望值,仿真器将会给出一个如下所示的信息:
// "test.sv",7:top.t1.a1:started at 55ns failed at 55ns
// offending '(bus.cb.grant == 2'b1)'
// 该消息指出,在test.sv文件中的第七行,断言top.t1.a1在55ns开始检查信号bus.cb.grant,但是立即检查出了错误。
3.2 定制断言行为
一个立即断言有可选的then和else分句,如果想改变默认的消息,可以添加自己的输出信息。
// 在立即断言中创建一个定制的错误消息
a1: assert (bus.cb.grant == 2'b01)
else $error("Grant not asserted");
SystemVerilog 有四个输出消息的函数:$info
, $warning
, $error
和 $fatal
。这些函数仅允许在断言内部使用,而不允许在过程代码中使用。
// 使用then子句来记录断言何时成功完成
a1: assert (bus.cb.grant == 2'b01)
grants_received++; // 另一个成功的结果
else
$error("Grant not asserted");
3.3 并发断言
另一种断言就是并发断言,可以认为它是一个连续运行的模块,它为整个仿真过程检查信号的值。需要在断言内指定一个采样时钟。下面是一个检查仲裁器request信号的断言。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; // 确保没有Z或者X值存在
endproperty
assert_request_2state: assert property(request_2state);
endinterface
3.4 语法之序列
序列由sequence… endsequence
声明
功能特性经常由有序的行为构建,sequence
功能提供了一种能力来构建和处理有序的行为。
在一系列的布尔逻辑表达式中,如果每个布尔表达式的计算都为真,那么关于这个序列的断言为真,否则为假。
sequence s1; //无参数
@(posedge clk) a ##1 b ##1 c; //##1指的是延时一个周期
endsequence //上面的意思是先a是1,一个周期后b是1,一个周期c是1
sequence s2(data,en); //带参数
@(posedge clk)(!a && (data == data_bus) ##1 c[0:3] == en )
endsequence
SVA也内嵌了边缘表达式,以便用户监视信号值从一个时钟周期到另一时钟周期的跳变。这使得用户能检查边沿敏感的信号(前后信号不一致才是属于跳变)
$rose(boolean expression or signal name)
当信号/表达式为1时返回真。$fell(boolean expression or signal_ name)
当信号/表达式为0时返回真。$stable(boolean"expression or signal_ name)
当信号/表达式不发生变化时返回真。
SVA语法—时钟关系的序列
很多时候,我们关心的是检查需要几个时钟周期才能完成的事件。也就是所谓的“时序检查”。在SVA中,时钟周期延迟用"#" 来表示。例如,#3表示3个时钟周期。举个例子:
sequence s1;
@(posedge clk) a ##2 b;
endsequence
序列s1检查信号"a"在一一个给定的时钟上升沿是否为高电平:
如果信号"a"不是高电平,序列失败,断言失败。
如果信号"a"在任何一一个给定的时钟上升沿为高电平,信号"b” 在两个时钟周期后为高电平,则断言成功。
如果信号"a"在任何一一个给定的时钟上升沿为高电平,信号"b" 在两个时钟周期后为不为高电平,断言失败。
在仿真后结果显示中,成功的序列总是标注在序列开始的位置
3.5 语法之属性
许多序列可以有序地组合起来生成更复杂的序列。SVA 提供了一个关键词property
来表示这些复杂的有序行为。属性(property)的基本语法是:
property name_ of_ property;
; or
;
endproperty
属性是在模拟过程中被验证的单元。它必须在模拟过程中被断言来发挥作用。 SVA提供了关键"assert"来检查属性。断言(assert)的基本语法是:
assertion_ name: assert property (property_ name);
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
3.6 语法之时钟定义
// 定义一个序列并不能发挥作用,它必须被断言才能发挥作用
// SVA在序列、 属性甚至一个断言的语句中都可以定义时钟。
sequence s5;
a ##2 b;
endsequence
property p5;
@(posedge clk) s5;
endproperty
a5: assert property(p5);
通常情况下在sequence描述行为,在property描述时钟
3.7 语法之禁止属性
属性可以禁止发生。即我们期望属性永远为假,当属性为真时,断言失败。
序列s6检查当信号"a"在给定的时钟上升沿为高电平,那么两个时钟周期以后,信号"b"不允许是高电平。关键词"not" 用来表示属性应该永远不为真。
sequence s6;
@(posedge clk) a ##2 b;
endsequence
property p6
not s6;
endproperty
a6: assert property(p6);
3.8 语法之执行块
SystemVerilog 语言被定义成每当一个断言检查失败,模拟器在默认情况下都会打印出一条错误信息。模拟器不需要对成功的断言打印任何东西。读者同样可以打印自定义自定义的成功或失败信息。
property p7
@(posedge clk) a ##2 b;
endproperty
a7: assert property(p7)
$display("Property p7 succeeded\n");
else
$display("Property p7 failed\n");
3.9 语法之蕴含操作
对于@(posedge clk) a #2 b
这样的属性,它在每个时钟上升沿检查信号"a" 是否为高。寻找是否为一个断言的有效开始。
如果信号"a"在给定的任何时钟上升沿不为高,检验器将产生一个错误信息。这并不是一个有效的错误信息因为我们不只关心a,更加关心a和b的关系。这个错误只表明这个时钟周期没有得到有效起始点。它们会在一段时间内产生大量的错误信息。
蕴含
基于以上问题,SVA提供了蕴含操作。
蕴含等效于一个if-then结构。蕴含的左边叫作“先行算子”,右边叫作"后续算子”。当先行算子成功时,后续算子才会被计算。如果先行算子不成功,那么整个属性就默认地被认为成功。这叫作"空成功”
蕴含结构只能被用在属性定义中,不能在序列中使用。
蕴含分为两类:交叠蕴含和非交叠蕴含。
交叠蕴含用符号"|->” 表示。如果先行算子匹配,在同一个时钟周期计算后续算子表达式。例如:
property p8;
@(posedge clk) a |-> b;
endproperty
a8: assert property(p8);
当信号"a" 为高,而且信号"b" 在同一个时钟沿也为高,这是一个真正的成功。若信号"a"不为高,断言默认自动成功,称为空成功。信号"a" 为高且,并且在同一个时钟沿信号"b" 未能检测为有效的高电平,则断言失败。
非交叠蕴含用符号(“I=>”)表示。如果先行算子匹配,那么在下-一个时钟周期计算后续算子表达式。后续算子表达式的计算总是有一个时钟周期的延迟。 例如:
property p9;
@(posedge clk) a|=> b;
endproperty
a9: assert property(p9);
3.10 语法之时序窗口
SVA的延迟可以支持固定的正延迟,也可以支持一个时间窗口。例如:
property p10
@(posedge clk) (a && b) |-> ##[1:3] c;
endproperty
// 等价于
// (a && b) |-> ##1 c 或
// (a && b) |-> ##2 c 或
// (a && b) |-> ##3 c
p10先行算子在任何给定的时钟上升沿为真,那么在接下去的1~3周期内,信号"c" 应该至少在一个时钟周期为高,SVA允许使用时序窗口来匹配后续算子。时序窗口表达式左手边的值必须小于右手边的值。每声明一个时序窗口,就会在每个时钟沿上触发多个线程来检查所有可能的成功。p10 实际上展开了三个线程。
3.11 语法之ended
结构
默认情况下,多重sequence的组合是以sequence的起始时间作为同步标志的,就是以序列的起始点作为同步点,来组合成时间上连续的检查。
SVA提供ended
结构以sequence的结束时间作为序列同步点。关键字ended
存储一个反映在指定时钟处序列是否匹配成功的布尔值。
ended
代表匹配的完成,是匹配的结束点,而不是匹配的起点。
默认情况下,多重sequence的组合是以sequence的起始时间作为同步标志的,就是以序列的起始点作点作为同步点,来组合成时间上连续的检查。SVA还提供了另一种使用序列的结束点作为同步点的连接机制。
sequence s11a;
@(posedge clk) a ##1 b;
endsequence
sequence s11b;
@(posedge clk) c ##1 d;
endsequence
property p11a;
s11a |=> s11b;
endproperty
property p11b;
s11a.ended |-> ##2 s11b.ended;
endproperty
a11a: assert property(p11a);
a11b: assert property(p11b);
3.12 语法之$past
构造
SVA提供了一个内嵌的系统任务$past
, 它可以得到信号在几个时钟周期之前的值。在默认情况下,它提供信号在前一个时钟周期的值。结构的基本语法如下:
$past (signal name, number of clock cycles)
这个任务能够有效地验证设计到达当前时钟周期的状态所采用的通路是正确的。
property p12;
@(posedge clk) (c && d) |-> ($past ((a && b),2)==1'b1);
endproperty
a12: assert property(p12);