视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Verilog中的延时、阻塞与非阻塞赋值仿真
2025-09-25 17:35:27 责编:小OO
文档
从仿真语义的角度看Verilog中的延时、阻塞与非阻塞赋值

1  Verilog中的延时

Verilog没有和VHDL中类似的最小延时概念,所有的延时都由符号“#”来定义,如果没有这个符号就意味着没有延时,清单1中描述了一个有关延时的简单例子。

清单1  简单的延时

wire #5 Y = A & B; 

清单1 中使用持续赋值语句描述了一个两输入端与门逻辑,并且在表达式前插入了5ns(#5)的延时,意义为Verilog仿真器会在5ns的延时后将A和B相与赋值给Y。通过这个例子可以看出,延时的插入只需要在原本的语句中加入“#”关键字即可,但在实际的使用中却经常产生错误,实际中的延时时间是由具体的硬件电路来决定的。使我们更深入的理解Verilog中的延时,更加关注描述的电路意义而不是描述语句本身,Verilog也是一种机于硬件的语言。

1.1  实际中的延时

在实际的电路中,只存在着两种延时行为,一个是惯性延时,另一个是传输延时。

1.1.1  惯性延时(Inertial Day)

惯性延时通常在信号通过逻辑门的时候发生,图1所示是信号通过一个具有5ns延迟的非门时的行为。

图1 惯性延时

输入信号WireIn有两个高电平脉冲,一个宽度为3ns,另一个宽度为9ns。当第一个3ns的脉冲到达非门时,因为其宽度小于非门的本身延时(5ns),输出还来不及建立低电平,输入脉冲就已经过去,所以在输出信号WireOut上没有体现出第一个3ns脉冲的响应。第二个脉冲宽度为9ns,大于非门的本身延时,所以在脉冲上升沿5ns之后,WireOut输出了一个宽度为9ns的低脉冲,这个脉冲与输入脉冲等宽、反向而且延迟了5ns。

这种延时称为惯性延时或惰性延时。如果输入的变化过快,小于逻辑门本身的延时,就不会被体现在输出上。

1.1.2  传输延时(Transport Delay)

传输延时相对于惯性延时更容易理解,相当于信号通过了一条拥有固定延时的传输线。如图2所示是信号通过一条5ns的延时线地示意图与波形。

图2 传输延时

容易看出来,WireOut实际上就是被延迟了5ns的WireIn。所以传输延时的意义就是将输入信号延迟一定时间后体现在输出上,而且输入信号上的所有细节都不会丢失。

1.2 持续赋值语句中的延时

在持续赋值语句只有一种合法的延时描述,如清单2所示:

清单2  持续赋值语句中的延时

assign #5 WireOut = ~ WireIn;

这种描述用语表示电路中的惯性延时,任何小于5ns的输入变化都会被过滤而不会体现在输出上。

1.3  过程赋值语句中的延时

过程赋值语句中的延时情况比较复杂,但是结论很简单:

●在持续赋值语句中使用正规延时,可以描述惯性延时。

●在非阻塞赋值语句中使用内定延时,可以描述传输延时。

1.3.1 正规延时和内定延时

正规延时和内定延时的定义见清单3。

清单3  正规延时和内定延时

#N sum = a+b;  //正规延时

sum = #N a+b;  //内定延时

定义于赋值语句前面的延时称为正规延时,其意义是:若赋值语句的执行条件在T时刻得到满足,该语句并不会立即执行,而是在延时N时间后,也就是在T+N时刻将T+N时刻的a+b赋值给sum。

内定延时定义于赋值语句的右式之前,其意义是:若赋值语句的执行条件在T时刻得到满足,立即将T时刻的a与b相加,并不是立即赋值给sum,而是在延时N时间后,也就是在延时N时间后将a+b赋值给sum。

了解了正规延时和内定延时的概念,不难想象出,对应Verilog中的持续性赋值、阻塞性赋值和非阻塞赋值这三种赋值形式,一共有六种插入延时的方法。但是在持续赋值中插入内定延时是非法的,这是因为内定延时需要将T时刻的结果保持到T+N时刻进行赋值,表现出记忆特性,与持续赋值的意义相冲突。

下文介绍阻塞赋值和非阻塞赋值中的延时。

1.3.2 阻塞赋值中的延时

在阻塞赋值中可以插入正规延时和内定延时,示例如清单4所示。由Quartus II综合后得到时间戳report和RTL图形分别如图3和图4所示,由Modelsim仿真产生的仿真波形如图5所示。

清单4阻塞赋值语句中的延时

module DelayDemo(A,B,C,D);

    output A,B,C,D;

    reg [3:0] A,B,C,D;

initial

   begin

       A=4'd0;B=4'd0;

    #4 A=4'd2;B=4'd4;

    #2 A=4'd3;

    #2 A=4'd4;

    #9 A=4'd3;

    #2 A=4'd5;B=4'd5;

    #5 B=4'd8;

   end   

always@(A or B)

begin

      C =#3 A+B;                //阻塞赋值中的内定延时

end

always@(A or B)

begin

      #3 D= A+B;               //阻塞赋值中的正规延时

end

endmodule

图3清单4的message

图4清单4的RTL

图5清单4的仿真波形

在图5的仿真图中,对于不断变化的输入A和B,C为插入了3ns内定延时的A+B,D为插入了3ns正规延时的A+B。

先讨论在阻塞赋值中插入内定延时的效果:

●0ns时刻(Start1):always进程启动,仿真器计算0时刻A+B的值后进程挂起,等待3ns后赋值给C。

●3ns时刻(Display1):C接受赋值更新,由未知出跳变为0。

●4ns时刻(Start2):A和B同时变化,启动进程,仿真器计算A+B的值,并在等待3ns后赋值给C。     

●6ns时刻:由于阻塞赋值的特性,A由2到3的跳变被忽略,不会反应在C上。

●7ns时刻(Start2):C由0跳变为4ns时刻A+B的值6

再来讨论在阻塞赋值中插入正规延时的效果:

●0ns时刻(Start1):always进程启动,由于设定了3ns的正规延时,进程被挂起等待3ns后执行。

●3ns时刻(Display1):执行赋值,将该时刻的A+B=0赋值给C。

●4ns时刻(Start2):A和B同时变化,启动进程,仿真器计算A+B的值,并在等待3ns的延时,在7ns时刻再次执行。     

●6ns时刻:由于阻塞赋值的特性,A由2到3的跳变被忽略,不会反应在C上。

●7ns时刻(Start2):执行赋值,将该时刻的A+B=7赋值给C。

由上面的分析可知,在阻塞赋值语句中插入延时的效果是;在语句启动后延时的一段时间输出当前时刻(正规延时)或语句启动时刻(内定延时)的逻辑结果,并且会忽略这段时间内所有的输入改变事件。但是这种行为不能模拟实际电路中的惯性延时或者传输延时,因此不适合在阻塞赋值中插入延时。    

1.3.3 非阻塞赋值中的延时

与阻塞赋值一样,非阻塞赋值也可以插入正规延时和内定延时,示例如清单5,图6和图7分别是Quartus II综合产生的时间戳report和RTL图形,图8是由Modelsim仿真产生的波形。

清单5  非阻塞赋值语句中的延时

always@(A or B)

begin

C <=#3 A+B;                //非阻塞赋值中的内定延时

end

always@(A or B)

begin

#3 D<= A+B;               //非阻塞赋值中的正规延时

end

/*其余部分与清单4相同*/

图6清单5的时间戳report

图7清单5的RTL图形

图8清单5的仿真波形

在图8的仿真图中,对于不断变化的输入A和B,C为插入了3ns内定延时的A+B,D为插入了3ns正规延时的A+B。

先讨论在非阻塞赋值中插入内定延时的效果:

●0ns时刻(Start1):always进程启动,计算A+B的值,然后进程挂起,等待3ns后赋值给C。

●3ns时刻(Display1):C接受赋值更新,由未知出跳变为0。

●4ns时刻(Start2):A和B同时变化,启动进程,仿真器计算A+B的值,并在等待3ns后赋值给C。     

●6ns时刻:由于非阻塞赋值的左式更新操作在仿真事件中的优先级要低于阻塞赋值的右式计算、左式更新,也低于由于输入改变而启动的非阻塞赋值本身的右式计算,所以在该时刻C的更新操作会被放入执行队列,并在3ns后执行赋值。

●7ns时刻(Display2):C由0跳变为4ns时刻A+B的值6

●8ns时刻(Start3):进程再次启动,并会在3ns以后将C赋值为8ns的A+B值

●9ns时刻:执行6ns时刻放入队列中的赋值操作,C被赋值为7

通过上面的分析可以看出在非阻塞赋值中插入内定延时可以很好的描述实际电路行为中的传输延时,这也是在过程赋值中唯一推荐使用的延时描述。

而非阻塞赋值中插入正规延时的效果大致与阻塞赋值中相同,会在当前的语句启动以后,延时一段时间输出当前时刻的逻辑结果,并且会忽略这段时间内的所有输入改变事件,不符合惯性延时和内定延时的行为特点。

2 阻塞赋值与非阻塞赋值

2.1说明

阻塞赋值与非阻塞赋值统称为过程赋值。

2.2组合逻辑

Verilog中使用等号“=”来表示阻塞赋值,被赋值的变量放在等号的左边,计算赋值的表达式置于等号右边。阻塞赋值可以很好的建模电路中的数据流,请考虑清单6中的代码片断。

清单6 组合逻辑的阻塞赋值

reg temp1,temp2;

always @(X or Y or CIN)

begin 

temp1 = X ^ Y;

temp2 = temp1 & CIN;

SUM = temp1 ^ CIN;

COUT = temp2 | (X & Y);

End

清单6是一个全加器的描述,其中所有的赋值语句都是阻塞赋值,特点是:  等号“=”右边表达式的结果计算和将计算结果赋值给左边变量的操作,是一个统一、连续的过程,不允许在其中插入其他动作;阻塞赋值语句会阻塞其后代码中语句的执行,也就是说Verilog仿真器在完成一句阻塞赋值语句前,不会响应其他事件。 

由此可见清单6中各语句的意义是:首先将X异或Y的结果赋值给temp1,接着执行第二条语句将temp1和CIN相与的结果赋值给temp2,并最终计算出SUM和COUT。很好的反映了组合逻辑中的数据流动顺序。

如果将这里的阻塞赋值替换为非阻塞赋值,如清单7所示。 

清单7 组合逻辑的非阻塞赋值1

reg temp1,temp2; 

always @(X or Y or CIN) 

begin 

temp1 <= X ^ Y;

temp2 <= temp1 & CIN;

SUM <= temp1 ^ CIN;

COUT <= temp2 | (X & Y);

end 

Verilog中使用小于等号“<=”来表示非阻塞赋值,被赋值的变量放在等号的左边,计算赋值的表达式置于等号的右边。

 与阻塞赋值不同,非阻塞赋值不能反映电路的数据流。因为在Verilog仿真语义中规定:非阻塞赋值对于左边赋值变量的更新操作的优先级要低于阻塞赋值,也要低于非阻塞赋值本身等号右边的表达式计算,需要等到当前仿真周期结束时才被执行。所谓的当前仿真周期结束,指的是某一时刻的所有仿真事件全部完成。

 在清单7中,某时刻当敏感事件表中的信号发生变化导致always进程启动,进程中的赋值语句在该时刻被依次执行。由于非阻塞赋值的特点,X异或Y首先计算,但是所得的结果并不会立刻赋值给temp1,所以第二句中引用的是temp1的原值,不是第一句中X与上Y的结果。同样,第二句中temp1和CIN相与的结果也不会立刻用于更新temp2。所有的左侧变量在该仿真时刻的事件执行完毕,也就是5条赋值语句的右侧表达式全部计算完毕后才被更新。

 这显然不是我们所期望的逻辑结果,不过可以通过将中间变量temp1和temp2放入always语句的敏感事件表中来解决这个问题,如清单7所示。这样在每次temp1或temp2发生变化的时候,always语句都会重新启动,从而将temp1和temp2的更新代入计算得到正确的SUM和COUT。

清单8 组合逻辑的非阻塞赋值2

reg temp1,temp2; 

always @(X or Y or CIN or temp1 or temp2)

begin 

temp1 <= X ^ Y;

temp2 <= temp1 & CIN;

SUM <= temp1 ^ CIN;

COUT <= temp2 | (X & Y);

end 

通过上面的示例可以看出,非阻塞赋值不能反映组合逻辑中的数据流,除非将所有的中 间变量都列入敏感事件表中。所以建议在对组合逻辑建模时采用阻塞赋值。

2.3时序逻辑

时序逻辑中也存在变量更新与读取的冲突和顺序问题,当更新与读取分别在两个always进程中时,请考虑清单9中的代码片断。

清单9 双进程阻塞更新

always @(posedge CLK)         // 进程A 

begin 

        temp = X ^ Y; 

end 

always @(posedge CLK)         // 进程B 

    begin 

        XxorY = temp; 

End

从仿真语义的角度来看,这两个always进程置于相同时钟CLK的控制下,进程A更新寄存器变量temp的值,进程B读取temp的值。当CLK的上升沿到来时两个进程被同时启动,习惯性的误认为:由于语句的先后顺序,进程A中的阻塞赋值首先执行。当X异或Y赋值给temp后,才会执行进程B中的读取语句,最后XxorY得到X异或Y的结果(D触发器)。

但是如果进程A和进程B的顺序颠倒过来,读取便会阻塞更新,XxorY得到的就不是该时钟上升沿时刻X异或Y的结果,而是上一周期temp的原值(2位移位寄存器)。 实际上,在Verilog的仿真语义中,always进程的执行顺序仅仅和敏感事件表相关,并不取决于代码的先后顺序。在CLK上升沿的控制下,进程A和进程B同时启动,temp的更新与读取操作都作为当前仿真时刻的事件被列入事件队列。由于他们同为阻塞赋值,所以拥有相同的优先级,仿真器的执行顺序是随机的,有的仿真器会按照代码的先后顺序执行,有的则会按照随机顺序执行。 

这种仿真语义的不确定性会带来综合结果的不确定性,目前业界还没有Verilog的综合标准,综合的结果取决于综合工具对于仿真语义的理解。虽然对于清单9中的代码,似乎任何一种执行顺序都有一种仿真语义与之对应,但是设计者撰写这段代码所希望得到的硬件结构确是一定的:希望以temp作为中间变量,用CLK时钟将X异或Y的结果同步后再打一拍赋值给XxorY,硬件电路相当于两个D触发器的串联,可以看作是一个2位的移位寄存器,如图9所示。

图9 希望得到的硬件电路

从硬件结构中可以看出,必须保证中间变量temp先被读取再被赋值。这种保证在实际的电路中是通过触发器的建立延时来实现的,即当CLK同步变化时,虽然两个D触发器被同时启动,但是由于输入到输出的信号建立需要一定的时间,所以触发器XxorY得到的是触发器temp输出的原值,从而使XxorY在两个周期后得到X异或Y的结果。 

在仿真语义中需要显式的表明这种顺序可以对temp使用非阻塞赋值。非阻塞赋值的优先级较阻塞赋值低,temp的更新会被安排到仿真时刻的所有事件完成后执行。无论语句的先后顺序如何,作为阻塞赋值的temp读取事件都会先于temp的更新事件执行,保持了仿真语义与综合结果的一致性。 

因此,建议当对变量的赋值与读取不在同一个always进程时,使用非阻塞赋值对该变量赋值,如清单10所示。

清单10 双进程非阻塞更新

always @(posedge CLK) // 进程A

begin 

     temp <= X ^ Y;

end 

always @(posedge CLK) // 进程B 

begin 

        XxorY = temp; 

End

仿真语义是有顺序的,即使多个事件可能都包含在同一个的仿真时刻下。这是因为硬件电路是并行的,同一时刻可能有多个事件发生,但是仿真工具一般都是运行在通用计算机上的软件程序,CPU的处理机制是顺序的。可以认为仿真语义是为了平衡并行的硬件电路与顺序的仿真工具之间的一种桥梁。 

综合结果是综合软件对仿真语义的理解,但是并不局限于仿真语义。实际的综合工具会根据更多的元素来进行综合,比如Synplify Pro对于清单8和9中的代码均可综合出正确的结果,但是有些综合工具对于7中的描述,就会将中间变量temp 当作是冗余逻辑而优化掉(XxorY在一个时钟周期内就得到了X异或Y的值)。

如果对变量的赋值与读取都出现在同一个时钟控制的always进程中,可以认为这是在一个时钟节拍内的组合逻辑块,示例代码如清单11所示。

清单11 单进程变量更新

always @(posedge CLK) 

begin 

        temp = X ^ Y; // 阻塞赋值 

if(~temp) 

     XequalY <= 1’b1;

Else

     XequalY <= 1’b0;

End

always @(posedge CLK) 

begin 

    temp <= X ^ Y; // 非阻塞赋值 

if(~temp) 

        XequalY <= 1’b1;

Else 

        XequalY <= 1’b0;

End

建议对组合逻辑采用阻塞赋值,对时序逻辑采用非阻塞赋值的目的是为了避免Verilog仿真模型与最终综合结果的不一致。但是这并不是一条规定,只要能够理解阻塞赋值与非阻塞赋值之间的语义差别,在很多情况下灵活的运用它们往往能够得到意想不到的结果。

2.4 阻塞赋值与非阻塞赋值的总结

以下是就使用“阻塞赋值”或“非阻塞赋值”的一个概括和总结:

●当为时序逻辑建模时,使用“非阻塞赋值”;

●当为锁存器(latch)建模时,使用“非阻塞赋值”;

●当用always块为组合逻辑建模时,使用“阻塞赋值”;

●当在同一个always块里面既为组合逻辑又为时序逻辑建模时,使用“非阻塞赋值”;

●不要在同一个always块里面混合使用“阻塞赋值”和“非阻塞赋值”;

●不要在两个或两个以上的always块里面对同一个变量进行赋值;

●使用$strobe以显示已被“非阻塞赋值”的值;

●不要使用#0延时的赋值。下载本文

显示全文
专题