接口
什么是接口?
- 接口 主要用作验证 ,国外有些团队会使用sv进行设计,那么接口就会用作设计。
- 验证环境中,接口可以 使连接变得简洁而不易出错 。
- interface和module的使用性质很像, 可以定义端口,也可以定义双向信号,可以使用initial和always,也可以定义function和task 。
- interface可以 在硬件域和软件域间传递信息 ,也就是可以作为module的端口列表,也可以作为软件方法的形式参数。
- 对于interface的初步认识,可以看作“插排”,DUT与TB之间的数据驱动就是靠这个“插排”来完成的。
接口的定义与使用
-
interface的定义结构与module类似。
-
interface的 端口列表只需定义时钟、复位等公共信号 ,或者不定义任何端口信号,而在变量列表中定义DUT与TB连接的各个变量, 建议用logic来定义 。
-
interface也可以依靠参数化方式提高复用性。
-
interface在例化时, 与module例化方式相同 。
-
对于有对应interface的DUT和TB组件,在例化时,传递匹配的interface变量名也就完成了interface内变量的传递,换句话说就是两者打通,对应interface的不同组件之间变量信号实时传递。
白话一刻
在System Verilog中,interface是一个用于封装一组相关信号并简化模块间连接的重要特性。它类似于一条总线或“插排”,可以将零碎的线(即信号)整合在一起,提供给需要的模块使用。
-
DUT (Device Under Test):这是指正在被测试或验证的硬件设备或软件组件。在System Verilog的验证环境中,DUT是被测试的对象,测试人员会为其编写测试用例以检查其是否按预期工作。
-
TB (Testbench):测试平台或测试基准。在System Verilog中,TB是用于验证DUT正确性的代码集合。它通常包括DUT的实例化、接口的实例化、验证环境的实例化以及实际的测试用例。 TB的主要任务是产生激励信号并观察DUT的响应,以验证DUT是否满足预期的行为。
在interface的上下文中,从TB的角度来看,interface中的信号输入输出方向与TB中信号的输入输出方向是一致的。而从DUT的角度来看,DUT中信号的输入输出方向与interface中信号的输入输出方向是相反的。这是因为interface在TB和DUT之间起到了一个中介或桥梁的作用,确保两者之间的数据流动是正确和高效的。
总结来说,interface在System Verilog的验证环境中扮演了关键角色,它简化了DUT和TB之间的连接和数据交换,使得验证工作更加高效和准确。而DUT是被测试的对象,TB则是用于验证DUT的测试代码集合。
interface arb_interface(input bit clk);
logic [1:0] grant, request;
logic reset_n;
endinterface
module arb(arb_interface arb_if);
//......
always@(posedge arb_if.clk or negedge arb_if.reset_n)begin
if(!arb_if.reset_n)
arb_if.grant <= 2'b00;
else
arb_if.grant <= next_grant;
end
//......
endmodule
module testbench(arb_interface arb_if);
//......
initial begin
@(posedge arb_if.clk) arb_if.request <= 2'b01;
$display("@%0t: Drove req=01", $time);
repeat(2) @(posedge arb_if.clk);
if(arb_if.grant != 2'b01) $display("@%0t: a1: grant != 2'b01", $time);
$finish;
end
endmodule
module tb_top;
bit clk;
initial clk = 1'b0;
always #5 clk = ~clk;
arb_interface arb_if(clk);
arb a1(arb_if);
test t1(arb_if);
endmodule
这段代码是使用SystemVerilog语言编写的,主要涉及到接口(interface)和模块(module)的定义。我会为你解释每个部分的功能。
-
- arb_interface 定义
interface arb_interface(input bit clk);
logic [1:0] grant, request;
logic reset_n;
endinterface
这里定义了一个名为arb_interface的接口,它有一个输入信号clk。接口内部定义了两个2位的逻辑信号grant和request,以及一个逻辑信号reset_n。
-
- arb 模块
module arb(arb_interface arb_if);
//......
always@(posedge arb_if.clk or negedge arb_if.reset_n)begin
if(!arb_if.reset_n)
arb_if.grant <= 2'b00;
else
arb_if.grant <= next_grant;
end
//......
endmodule
arb模块使用arb_interface作为它的接口。它有一个始终块,该块在clk的上升沿或reset_n的下降沿触发。当reset_n为假时,grant信号被设置为2’b00;否则,它被设置为next_grant的值(这里next_grant`在代码段中没有定义,可能是在模块的其他部分定义的)。
-
- testbench 模块
module testbench(arb_interface arb_if);
//......
initial begin
@(posedge arb_if.clk) arb_if.request <= 2'b01;
$display("@%0t: Drove req=01", $time);
repeat(2) @(posedge arb_if.clk);
if(arb_if.grant != 2'b01) $display("@%0t: a1: grant != 2'b01", $time);
$finish;
end
endmodule
testbench模块也是一个使用arb_interface的模块。在initial块中,它首先等待clk的上升沿,然后将request信号设置为2’b01。接着,它等待clk的另外两个上升沿,并检查grant信号是否为2’b01`。如果不是,它会打印一条错误消息,并结束仿真。
-
- tb_top 模块
systemverilog
module tb_top;
bit clk;
initial clk = 1'b0;
always #5 clk = ~clk;
arb_interface arb_if(clk);
arb a1(arb_if);
test t1(arb_if);
endmodule
tb_top是顶层模块,它定义了时钟信号clk,并通过always块每5个时间单位反转clk的值。然后,它实例化了arb_interface、arb和testbench模块,并将它们连接在一起。这样,testbench模块可以向arb`模块发送请求,并检查其响应。
总结:这段代码是一个简单的SystemVerilog验证环境,其中包含一个接口、一个仲裁器模块和一个测试基准模块。顶层模块将它们连接在一起,以测试仲裁器的行为。
这里着重解释一下tb_top 模块:
在 tb_top 模块中,test t1(arb_if); 这一行代码是实例化 testbench 模块(在这里被重命名为 test)的一个实例 t1,并将之前定义的 arb_interface 实例 arb_if 作为参数传递给 test 模块的接口。
详细解释如下:
-
实例化模块:在SystemVerilog中,模块可以被多次实例化,以创建多个独立的模块实例。每个实例可以有其自己的内部状态和行为,但它们都基于相同的模块定义。在这里,test t1(arb_if); 就是创建 testbench 模块的一个新实例 t1。
-
接口传递:arb_interface arb_if(clk); 这行代码在 tb_top 模块中定义了一个 arb_interface 的实例 arb_if,并将 tb_top 中定义的 clk 信号连接到该接口的 clk 输入。然后,在实例化 test 模块时,arb_if 被作为参数传递给 test 模块,这样 test 模块就可以通过 arb_if 接口与 arb 模块进行通信。
-
连接性:由于 arb_if 被传递给了 test 和 arb 两个模块,这两个模块可以通过 arb_if 接口来交换信号。在 test 模块中,它可以通过 arb_if 发送请求(request)并观察响应(grant)。同时,arb 模块也可以通过 arb_if 接收请求并发送响应。
-
封装性:通过使用接口,test 和 arb 模块都不需要知道彼此的内部实现细节。它们只需要知道如何通过 arb_if 接口来交换信号。这种封装性使得模块的设计和测试更加灵活和可维护。
-
测试环境:在这种上下文中,test 模块通常用作测试环境或测试台(testbench),用于验证 arb 模块的行为是否符合预期。通过发送不同的请求并观察响应,测试人员可以验证 arb 模块的功能正确性。
综上所述,test t1(arb_if); 这行代码在 tb_top 模块中创建了一个 testbench 模块的实例 t1,并通过 arb_interface 实例 arb_if 将它与 arb 模块连接起来,从而构建了一个完整的测试环境。
module testbench(arb_interface arb_if);
//......
initial begin
@(posedge arb_if.clk) arb_if.request <= 2'b01;
$display("@%0t: Drove req=01", $time);
repeat(2) @(posedge arb_if.clk);
if(arb_if.grant != 2'b01) $display("@%0t: a1: grant != 2'b01", $time);
$finish;
end
endmodule
接口的优势
- 将有关信号封装在接口,对于设计和验证环境都便于维护,如需修改、添加、删除信号,只需修改interface文件即可。
- 接口在硬件域(module)和软件域(class)都可以使用,是 硬件域和软件域交互的唯一媒介 。
- 接口可例化,对于多组相同总线,通过例化可灵活使用,简化代码且便于维护。
- 每一个agent使用对应的interface,简化验证平台结构,便于维护。
- tb顶层例化时,无需定义信号连线,只需例化interface。
采样和数据驱动
竞争问题
-
为了避免RTL仿真行为中发生信号竞争问题, 建议使用非阻塞赋值(<=) (简单来说阻塞赋值是顺序执行,非阻塞赋值是并发执行,硬件电路行为是并发执行)。
-
在仿真行为中,为了避免时序电路中时钟和驱动信号的时序竞争,我们需要 尽量明确驱动时序和采样时序 。
-
默认情况下,时钟对于组合电路的驱动会添加一个 无限最小时间(delta-cycle)的延迟 ,而该延迟无法用绝对时间单位衡量,它要比最小时间单位精度还要小。这是 仿真工具为了符合硬件电路真实行为(建立时间保持时间以及线延迟)而做出的处理 。(注意: #0并不代表0延迟,而是指延迟delta-cycle)
为了说明delta-cycle的概念,举例如下:
`timescale 1ns/1ps
module top_module ();
bit clk1,clk2;
bit rstn;
logic [7:0] d1;
initial begin
clk1 = 0;
forever #5 clk1 <= ~clk1;
end
always@(clk1) clk2<=clk1;
initial begin
#0 rstn <= 0;
#10 rstn <= 1;
#20 $finish;
end
always@(posedge clk1 or negedge rstn)
if(!rstn) d1 <= 0;
else d1 <= d1+1;
always@(posedge clk1)
$display("clk1: %0t ns d1 value is 0x%0x", $time, d1);
always@(posedge clk2)
$display("clk2: %0t ns d1 value is 0x%0x", $time, d1);
endmodule
Running Icarus Verilog simulator...
VCD info: dumping is suppressed.
clk1: 5000 ps d1 value is 0xxx
clk2: 5000 ps d1 value is 0x0
clk1: 15000 ps d1 value is 0x0
clk2: 15000 ps d1 value is 0x1
clk1: 25000 ps d1 value is 0x1
clk2: 25000 ps d1 value is 0x2
Hint: Total mismatched samples is 0 out of 0 samples
Simulation finished at 30000 ps
Mismatches: 0 in 0 samples
白话一刻:
这是一个简单的Verilog模块,它描述了一个带有复位功能的计数器。下面是这段代码的详细解释:
时序单位
`timescale 1ns/1ps
这一行定义了时间单位和精度。在这里,时间单位是1纳秒(ns),精度是1皮秒(ps)。
模块定义
module top_module ();
定义了一个名为top_module的模块。
信号定义
bit clk1,clk2;
bit rstn;
logic [7:0] d1;
clk1 和 clk2 是位(bit)类型的信号,用作时钟。
rstn 是复位信号,低电平有效。
d1 是一个8位宽的逻辑信号,用作计数器。
时钟生成
initial begin
clk1 = 0;
forever #5 clk1 <= ~clk1;
end
这个initial块生成了一个时钟信号clk1,它的周期是10个时间单位(即10ns)。
时钟同步
always@(clk1) clk2<=clk1;
这个always块确保clk2与clk1同步。每当clk1变化时,clk2都会获得clk1的当前值。
复位逻辑
initial begin
#0 rstn <= 0;
#10 rstn <= 1;
#20 $finish;
end
这个initial块设置了复位信号rstn。
在仿真开始时,rstn为0(有效复位)。
在10个时间单位后,rstn变为1(解除复位)。在20个时间单位后,仿真结束。
计数器逻辑
always@(posedge clk1 or negedge rstn)
if(!rstn) d1 <= 0;
else d1 <= d1+1;
这个always块描述了一个计数器。每当clk1的上升沿发生或rstn的下降沿发生时,都会执行这个块。
如果rstn为0(有效复位),则d1被清零。否则,d1增加1。
显示逻辑
always@(posedge clk1)
$display("clk1: %0t ns d1 value is 0x%0x", $time, d1);
always@(posedge clk2)
$display("clk2: %0t ns d1 value is 0x%0x", $time, d1);
这两个always块在每次clk1和clk2的上升沿时显示当前时间和d1的值。
总结
这个模块生成了两个同步的时钟信号clk1和clk2,以及一个复位信号rstn。它实现了一个简单的计数器,该计数器在复位时清零,并在每个clk1的上升沿时递增。同时,它还使用$display系统任务来显示时钟信号和计数器的值。
为什么同样在25000ps时刻,d1在clk1和clk2下的采样值不同?
首先clk2是在clk1下驱动的,也就是clk2比clk1延迟一个delta-cycle时间,clk1驱动了d1,d1也比clk1延迟一个delta-cycle时间。
25000ps,当在clk1下采样时,d1的值还未更新,所以采到的是0x1;
而在clk2下采样时,d1已由clk1驱动更新,所以采到的是0x2。
如果还未完全理解,可以打开波形窗口,不过要打开delta-cycle的开关,可以看到delta-cycle的存在,对电路的数据驱动和采样时序会有更直观的理解。
,clk2 是通过 always@(clk1) 块中的语句 clk2 <= clk1; 驱动的。这个语句意味着每当 clk1 发生变化时,clk2 将被赋值为 clk1 的当前值。但是,由于 <= 是非阻塞赋值,它不会立即更新 clk2 的值,而是在当前仿真时间槽的末尾更新。
非阻塞赋值的一个关键特性是,在一个给定的仿真时间槽内,所有非阻塞赋值语句都看起来是同时执行的,即它们都在该时间槽的末尾更新其目标。因此,尽管 clk2 的赋值是基于 clk1 的变化,但由于非阻塞赋值的特性,clk2 的更新实际上会延迟一个delta-cycle。
-
总结:
- 如果处于各种原因,clk与被采样数据之间存在若干个delta-cycle的延迟,那么对数据的采样会存在问题。
- 采样数据的竞争问题会成为潜在困扰仿真采样准确性的问题。
- 避免采样的竞争问题:
- 1)在驱动时,添加相应的人为延迟,使clk与驱动变量之间的延迟加大,提高DUT使用驱动信号时的准确度;
- 2)在采样时,依靠采样前某段时刻进行采样,来模拟建立时间的采样要求,确保采样的可靠性。
图示:
在仿真过程中,delta-cycle的延迟存在主要是由于以下几个原因:
首先,delta-cycle是一个无限小的时间单位,其精度比最小的时间单位还要小,无法用绝对时间单位来描述。在仿真中,当运行一个delta-cycle时,实际上是让仿真器在一个极短的时间内执行一系列的操作。这种极短的延迟有助于理解在“同一时刻”的采样或驱动的先后逻辑关系。
其次,在RTL(寄存器传输级)仿真中,一些信号变化可能在少于单位时间(如纳秒级)内发生,这使得信号的变化过程变得不确定。通过引入delta-cycle作为额外的时刻,可以更清晰地描述这些细微变化,使仿真过程更加逻辑化。
此外,delta-cycle的延迟还用于解决同一时间点的信号竞争问题。在组合逻辑电路中,由于信号竞争状态的存在,可能会导致冒险的情况产生。为了避免这种情况,通常在组合逻辑电路的驱动输出中添加一个delta-cycle的延迟。这种延迟可以确保信号在发生变化时,按照预期的先后顺序进行处理,从而避免信号竞争带来的问题。
需要注意的是,由于delta-cycle的延迟非常小,它通常不会对整体仿真结果产生显著影响。然而,在某些特定情况下,如采样数据中的竞争问题,这种微小的延迟可能会导致采样结果的不一致。因此,在进行仿真时,需要仔细考虑delta-cycle的延迟对仿真结果的影响,并采取相应的措施来避免潜在的问题。
仿真时间槽的具体长度在Verilog中并没有一个固定的值,因为它取决于当前仿真时刻发生的事件数量以及仿真器的实现。
clocking
- 在接口中声明clocking(时序块)和采样的时钟信号,可以用来 实现信号的同步和采样 。
- clocking块基于时钟周期对信号进行驱动或采样的方式,使testbench不再苦恼于如何准确及时地对信号驱动或采样,消除了信号竞争的问题。
clocking bus @(posedge clk1);
default input #10ns output #2ns;
input data, ready, enable;
output negedge ack;
input #1step addr;
endclocking
这行定义了一个输出信号ack,并且指定了它的有效边沿为下降沿(negedge)。这意味着ack信号的变化(从高电平到低电平)将在clk1的上升沿之后的某个时刻被输出,具体取决于可能存在的其他延迟设置或操作。
其中定义了一个名为addr的输入信号,并给它指定了一个特殊的延迟#1step。#1step通常用于描述跨时钟域的传输,表示该信号的值将在下一个时钟边沿(在本例中是clk1的下一个上升沿)时被采样。这通常用于避免亚稳态问题,确保信号在稳定后才被采样。
对上述clocking描述代码进行说明 :
- 第一行定义clocking块bus,使用上升沿来驱动和采样。
- 第二行指出输入信号在clk1上升沿之前5ns采样,输出信号在clk1上升沿之后2ns驱动(输入为采样,输出为驱动)。
- 第三行声明输入信号,采用默认的输入事件(clk1上升沿5ns前采样)。
- 第四行声明输出信号,并且指明为clk1下降沿驱动,覆盖了原有的clk1上升沿后2ns驱动。
- 第五行定义了输入信号addr,采用了自定义的采样事件,clk1上升沿后的1 step,覆盖了原有的clk1上升沿前5ns采样,这里1 step使得采样发生在clk1上升沿的上一个时钟片采样区域,即可以保证采样到的数据是上一个时钟周期数据。
clocking块的总结 :
- clocking块不仅可以定义在interface中,也可以定义在module和program中。
- clocking中列举的信号不是自己定义的,而是interface或其他声明clocking的模块定义的。
- clocking在声明完后,应该伴随着定义默认的采样事件,也就是“default input/output event”,如果没有定义,会默认使用时钟上升/下降沿前1step进行采样,时钟上升/下降沿后#0进行驱动。
- 除了定义默认的采样和驱动事件,定义信号方向时同样可以用新的采样/驱动事件对默认事件进行覆盖。
在Verilog中,clocking块用于描述在特定时钟边沿下输入和输出信号的行为。当声明一个clocking块时,确实应该伴随着定义默认的输入和输出事件,即指定输入信号的采样时刻和输出信号的驱动时刻。
如果没有在clocking块中显式定义默认的输入和输出事件,Verilog会采用一些默认的规则来确定这些事件。具体来说:
-
默认输入事件:如果没有明确指定输入信号的采样事件,**则默认会在时钟边沿前的1个时间步长(#1step)进行采样。**这样做是为了确保在时钟边沿到来之前,输入信号的值已经稳定,从而避免可能的亚稳态问题。
-
默认输出事件:对于输出信号,如果没有明确指定驱动事件,则默认会在时钟边沿后立即(#0)进行驱动。这意味着输出信号的新值会紧跟在时钟边沿之后被更新。
这种默认行为确保了即使在没有明确指定的情况下,clocking块内的信号也有一个清晰且合理的行为模型。然而,在实际应用中,为了增加代码的可读性和准确性,通常建议显式地定义输入和输出事件的采样和驱动时刻,而不是依赖于这些默认行为。
通过显式定义输入和输出事件,您可以更精确地控制信号的行为,确保它们符合硬件设计的预期。例如,您可以指定输入信号在时钟上升沿后的某个延迟后进行采样,或者指定输出信号在时钟下降沿前的某个时刻进行驱动。这些自定义的设置可以帮助您更准确地模拟硬件的行为,并有助于发现和解决潜在的问题。
modport
modport本身目的就是将信号分组和指明输入输出方向,在连接时工具会进行端口方向检查,防止连接出错,不过掌握clocking之后,可以忽略modport,clocking已经对信号输入输出方向进行了声明。
结论
- 为了避免采样竞争问题,验证工程师应该在验证环境的驱动环节添加固定延迟,使得在仿真波形中更容易体现出时钟与被驱动信号之间的时序前后关系,同时这样也便于对DUT的准确处理和TB的准确采样。
- 如果TB在采样从DUT送出的数据,在时钟与被驱动信号之间存在delta-cycle时,应该考虑在时钟采样沿的更早时间端段去模拟建立时间要求,这种方法也可以避免由于delta-cycle问题带来的采样竞争问题。
- 当我们把clocking运用到interface中,用来声明各个接口与时钟的采样和驱动关系后,可以大大提高数据驱动和采样的准确性,从根本上消除采样竞争的可能性。
测试的开始和结束
写在前头
- 各个设计自身可以认为是一个大的线程,内部有包含多个并行的线程,而模块之间连接即线程的通信,主要依靠信号的变化。
- 可以想象,对于一个设计,如果在仿真开始没有任何激励,那么仿真不具备执行条件,也可以认为已经结束,因为在设计内部没有产生任何新的事件,也不会触发组合逻辑和时序逻辑。
- 如果仿真开始后仅提供时钟和复位信号,验证会持续下去,而对设计不会产生实质的功能影响。从设计角度来看,复位信号是为了让设计进入一个确定的初始状态,而时钟就是脉搏跳动。
- verilog测试中,可以通过系统函数 “ f i n i s h ( ) ”来结束仿真,也可以通过“ finish()” 来结束仿真,也可以通过 “ finish()”来结束仿真,也可以通过“stop()” 来暂停仿真。
program
- program是作为验证而提出的,可以有效控制仿真的进程,但是目前验证平台更多基于UVM,UVM有独特的控制机制,所以program在实际项目中使用并不多。
- program的提出, 将验证部分和设计部分进行有效隔离 ,每一个program作为一个独立测试, 当testbench中所有program中最后一个initial块完成后,结束仿真 。这是program的隐式结束。
- 有些program内的initial块无法正常结束,这时候需要使用 显示结束,使用“$exit()” 来结束program。
- program被看做软件域,所以不可以出现always、module、interface等硬件相关语句,并且不可以例化其他program。
- program被看做软件域,可以在program内部定义变量和发起多个initial块,并且建议使用阻塞赋值(软件方式的顺序执行)。
- program对于数据采样也可以消除delta-cycle竞争问题,详细内容可见红宝书“SV环境构建篇之程序和模块”。(待了解)
总结
-
硬件域(module)、软件域(program)、中间域(interface) 。
-
不仅可以使用interface clocking来消除采样竞争问题,可以使用program(建议使用clocking)。
program可以控制仿真的结束。 -
使用“ s t o p ( ) ”和“ stop()”和“ stop()”和“finish()”可以结束仿真。
调试方法
调试工具
大多工程师选择使用verdi作为调试工具,主要有三个窗口:层级列表窗口、源代码窗口、波形窗口。verdi的具体使用方法,请参考另一篇帖子【Verdi使用总结】 。
打印消息
打印消息是调试循环语句、顺序执行语句等查看路径和当前变量值的简便方式,除此之外,由于验证平台更多变量是动态的,无法在调试工具查看动态变量值,所以对与验证环境的调试,更多使用打印消息。
打印消息命令“$display()”:
- $time代表仿真时间变量。
- 显示格式: %x(十六进制)、%d(十进制)、%b(二进制)、%s(字符串)、%t(时间)。
- d i s p l a y (消息级别)、 display(消息级别)、 display(消息级别)、warning(警告级别)、 e r r o r (错误级别)、 error(错误级别)、 error(错误级别)、fatal(严重错误级别)
- 字符串变量格式化:string s = $sformatf(“Hello, %s!", name_s);
设置断点
- 可以通过调试工具为程序设置断点。
- 通过设置断点(breakpoint)可以查看程序执行到断点处(程序暂停)的变量数值,而设置断点要求验证工程师对程序执行顺序足够了解。
- 设置断点可以便于查看软件程序(function、task、object)中局部变量的数值。注意:动态变量是无法添加到波形查看的。
- 设置断点还可以方便调试程序执行的顺序,例如在顺序执行语句执行的多个位置设置断点,通过仿真执行,查看程序是否在断点处暂停,如果没有,那么程序的挂起(hang-on)原因就在上一个断点和此断点之间。通过此方法可定位可疑程序的范围。
- 如果查看局部变量,需要使用局部变量窗口(Local Windows),继而通过断点查看变量(暂时不知verdi是否支持,待学习)。
白话一刻:
在Verilog硬件描述语言(HDL)中,理解阻塞赋值(blocking assignment)和非阻塞赋值(nonblocking assignment)是非常重要的,特别是在描述并发行为时。这两种赋值方式会影响仿真中信号值的更新顺序,并可能导致所谓的“竞争条件”。
- 竞争条件
竞争条件(Race Condition)指的是在仿真过程中,当多个语句或操作试图在同一仿真时间槽(time-slot)内修改同一个信号或变量时,如果这些操作的执行顺序不同,可能会导致不同的执行结果。换句话说,竞争条件是由于多个操作之间的时间依赖关系不明确导致的。
在硬件设计中,并发性是一个关键概念,因为许多事件(如信号变化)可能几乎同时发生。如果Verilog代码中的赋值操作不能正确反映这种并发性,那么仿真结果可能与实际硬件行为不一致。
- 阻塞赋值
阻塞赋值使用等号(=)作为操作符。当执行阻塞赋值时,赋值语句会立即计算右侧表达式(RHS)的值,并将其赋给左侧表达式(LHS)。在这个赋值完成之前,仿真器会暂停执行任何其他语句。换句话说,阻塞赋值是顺序执行的。
这种赋值方式在描述组合逻辑时很有用,因为组合逻辑的输出直接依赖于其当前输入。但是,在描述时序逻辑时,如果不恰当地使用阻塞赋值,可能会导致竞争条件。
- 非阻塞赋值
非阻塞赋值使用小于等于号(<=)作为操作符。与阻塞赋值不同,非阻塞赋值不会立即更新LHS的值。相反,它会在当前仿真时间槽结束时更新LHS的值。这意味着在当前时间槽内,即使已经计算了RHS的值,其他语句仍然可以继续执行,并可能修改LHS的值。
非阻塞赋值允许模拟时序逻辑中的并发行为,因为所有非阻塞赋值语句在同一时间槽内看起来是同时执行的。这使得非阻塞赋值在描述寄存器传输级(RTL)时序逻辑时非常有用。
- 使用建议
为了避免在RTL仿真行为中发生信号竞争问题,建议在描述时序逻辑时使用非阻塞赋值。这是因为时序逻辑通常涉及状态寄存器和时钟信号,这些信号的行为在硬件中是并发的。使用非阻塞赋值可以更好地模拟这种并发行为,并减少竞争条件的风险。
相反,在描述组合逻辑时,由于输出直接依赖于输入,使用阻塞赋值通常是合适的。但是,即使在组合逻辑中,也要小心避免潜在的竞争条件,特别是当存在多个路径可以修改同一信号时。
总的来说,正确选择使用阻塞赋值还是非阻塞赋值是确保Verilog代码正确模拟硬件行为的关键。
参考资料
- Wenhui’s Rotten Pen
- SystemVerilog
- chipverify