目录
前言
一、可综合Verilog模块编写8条原则
二、阻塞赋值定义?非阻塞赋值定义?
1.为什么叫做阻塞赋值?
2.为什么叫非阻塞赋值?
3.代码波形验证
三、为什么Verilog代码会出现冒险和竞争现象?
四、Verilog的层次化事件队列
五、阻塞赋值和非阻塞赋值的8条编码原则
1.时序电路建模时,用非阻塞赋值?锁存器电路建模时,用非阻塞赋值?
2.用alwasy模块描述组合逻辑时,应采用阻塞赋值?????
3.在同一个always块中描述时序和组合逻辑混合电路时,用非阻塞赋值?????
不要在同一个always块中同时使用阻塞赋值和非阻塞赋值?????????
4.不要在多个always块中为同一个变量赋值???
5.用$strobe系统任务来显示用非阻塞赋值的变量值?????
6.在赋值时不要使用#0延迟???
在夏宇闻老师的书中有一章节《第14章 深入理解阻塞和非阻塞赋值的不同》详细的讲诉了阻塞和非阻塞赋值的异同,其实该章节是翻译以下这篇论文--Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!
,可以将书和论文一起对比照看能更加深入理解 ~~~
但对于刚踏入FPGA门的学习人来说,看完后绝的似懂非懂,对于何时需要使用阻塞赋值以及何时需要使用非阻塞赋值,认为只要把最后的原则记住应用使用就好,但是具体为什么这样使用的原因还是一头雾水。本篇文章根据代码、综合电路以及代码仿真一起来理解阻塞赋值和非阻塞赋值。
我们就对以下这几个问题慢慢展开深入了解:
1.Verilog代码编写的8条原则?
2.关于阻塞赋值和非阻塞的赋值是怎样定义??
3.为什么Verilog代码会出现冒险和竞争现象??
4.Verilog层次化时间队列是怎样排序??它是怎样理解阻塞赋值和非阻塞赋值??
5.从代码中是否可以理解以上8条原则???
原则1:时序电路建模时,用非阻塞赋值。
原则2:锁存器电路建模时,用非阻塞赋值。
原则3:用always块写组合逻辑时,采用阻塞赋值。
原则4:在同一个always块中同时建立时序和组合逻辑电路时,用非阻塞赋值。
原则5:在同一个always块中不要同时使用非阻塞赋值和阻塞赋值。
原则6:不要在多个always块中为同一个变量赋值。
原则7:用$strobe系统任务来显示用非阻塞赋值的变量值。
原则8:在赋值时不要使用 #0 延迟。
在进行解释这两个赋值定义之前先看以下两个缩写名:
RHS----赋值等号右边的表达式或变量;
LHS----赋值等号左边的表达式或变量;
阻塞赋值操作符用“=”表示,它可看作只有一个步骤的操作:
(1)在赋值开始时,先计算阻塞赋值操作RHS部分的值,这时赋值语句不允许任何别的Verilog语句的干扰,直到现行的赋值完成时刻,把RHS赋值给LHS,它才允许别的赋值语句的执行。
用大白话讲就是:当前的阻塞赋值语句未完成之前,后面的语句是不执行的。
非阻塞赋值操作符用“<=”表示,可以看作是两个步骤的操作:
(1)在赋值开始时刻,计算非阻塞赋值RHS表达式;
(2)在赋值结束时刻,更新非阻塞赋值LHS表达式;
非阻塞赋值允许其他的Verilog语句同时进行操作。
用大白话讲就是:所有的非阻塞赋值语句之间顺序没有关系,前面是否执行完不影响后面的语句,所有的语句都是并发执行。
也就是说在赋值开始时刻,所有的阻塞赋值语句要按照编写的顺序依次赋值,直到赋值结束时刻;但对于非阻塞赋值,无论非阻塞赋值的语句编写顺序如何,大家在赋值开始时刻都要先计算非阻塞赋值RHS的值,最后值的更新都是赋值结束时刻才完成。
以上都是语言去描述阻塞赋值和非阻塞赋值,觉得还是一头雾水,不明白~~~那直接拿代码仿真验证下是否和上述一致呢?
在下面的仿真代码中,a1、b1、a2和b2这4个寄存器在初始化时(第0ns)都直接赋值为0。a1,b1使用阻塞赋值,a2,b2使用非阻塞赋值。
reg a1 = 0, b1 = 0;
reg a2 = 0, b2 = 0;
initial begin
a1 = #5 1; //在第 5 ns赋值
b1 = #2 1; //在第 5+2 ns赋值
end
initial begin
a2 <= #10 1; //在第 10 ns赋值
b2 <= #9 1; //在第 9 ns赋值
end
initial begin
$monitor("a1 = %b,b1 = %b,a2 = %b,b2 = %b at%0dns",a1,b1,a2,b2,$time);
#20;
$stop;
end
通过仿真波形中可以看到:
1)a1赋值做了5ns的延时,所以在第5ns后a1赋值为1;
2)b1是在a1赋值语句之后,由于b1赋值做了2ns的延时,所以在b1在第7ns赋值为1;
3)a2的赋值做了10ns延时,那么a2在第10ns被赋值为1;
4)b2是在a2赋值语句之后,b2的赋值做了9ns延时,那么b2就是在第9ns被赋值为1。
仿真打印信息:
从以上波形及仿真打印信息可以看到,阻塞赋值实现的语句按照代码先后顺序执行,而阻塞赋值的语句则同时并行执行,没有先后顺序。
让我们先从一段代码中理解冒险和竞争:
module fboscl(y1,y2,clk,rst);
output y1,y2;
input clk,rst;
reg y1,y2;
always @(posedge clk or posedge rst)
if(rst) y1 = 0;
else y1 = y2;
always @(posedge clk or posedge rst)
if(rst) y2 = 1;
else y2 = y1;
endmodule
按照IEEE Verilog的标准,例子中的两个always块是并行执行的,若复位信号已从1到0,如果上面always块比下面的always块的时钟沿早几个皮秒(时钟偏差造成),则y1和y2都会取值1;如果上面的always块比下面的always块的时钟沿晚几个皮秒,则y1和y2都会取值0。
这说明这个Verilog模块是不稳定的,一定会产生竞争和冒险的情况,不仅不同的仿真器会可以会仿真出不同的结果,在最后的实际测试结果中也是不稳定的。
仿真波形如下:
如果我们将上述代码中的阻塞赋值改变成非阻塞赋值,那么仿真波形是如何呢??
module fboscl(y1,y2,clk,rst);
output y1,y2;
input clk,rst;
reg y1,y2;
always @(posedge clk or posedge rst)
if(rst) y1 <= 0;
else y1 <= y2;
always @(posedge clk or posedge rst)
if(rst) y2 <= 1;
else y2 <= y1;
endmodule
仿真波形如下:
上述两个always模块是并行执行的,在复位信号回到0后,y1的值就是1,y2的值就是0,无论哪个always块的有效时钟沿早到几皮秒,都要在赋值开始时刻计算RHS的值,而在结束时刻才更新LHS的值,因此在第一个时钟有效沿y1被赋的y2的值,y2被赋y1的值,即y1为1,y2为0;之后随着时钟信号不断重复,则每次赋值的y1,y2都是由上一个周期的时钟有效沿确定。
什么是Verilog的层次化事件队列??
书中说层次化事件队列是用于调度仿真事件的不同的Verilog事件队列。我个人理解是在仿真中对不同Verilog事件队列的执行顺序进行排序执行。
在IEEE 1364-1995 Verilog的5.6章节中定义了层次化事件队列在逻辑上分为当前仿真事间的4各不同队列,和用于下一个仿真事件的若干附加队列,如下图所示。
从上图中我们可以看到在当前的时间点上有四个不同的队列,他们是按照顺序在仿真中依次执行:
(1)Active events queue(动态事件队列):
blocking assignments(阻塞赋值)、continuous assignments(连续赋值)、$display commands($display 命令)、evaluation of nonblocking RHS expressions(计算非阻塞赋值右变表达式)、计算源语的输入和输出的变化。
这些事件可以任意顺序执行;
(2)Inactive events queue(无效事件队列)
#0延时阻塞赋值;
在代码中经常看见有加#0延时,主要是为了消除Verilog可能产生的竞争和冒险,但是完全没有必要,后续章节会解释为什么在代码中使用#0延时时时错误的。
(3)NBA events queue(非阻塞事件队列)
更新非阻塞赋值语句LHS的值;
该队列用于更新非阻塞赋值左端表达式,而右端表达式计算是在Active events queue中,在仿真开始时刻束随机顺序执行。
(4)postponed events queue(监控事件队列)
$monitor命令、$strobe命令
该队列是在仿真时刻的结束时(即其他所有赋值都完成时)对所需要的变量进行显示。
以上就是对不同的时间队列在仿真中执行的顺序进行排序说明,各大EDA仿真厂商也是按照以上的事件队对代码进行仿真出波形。
我们同样利用代码例程来解释说明:
我们打算设计如下的电路,实现一个简单的移位寄存:
//=======================================================================//
示例1:
在该代码中always的时序模块中使用的是阻塞赋值
module pipeb(
output [7:0] q3,
input clk,
input [7:0] d
);
reg [7:0] q1,q2,q3;
always @(posedge clk)
begin
q1 = d;
q2 = q1;
q3 = q2;
end
endmodule
综合后的电路图:
输入d信号经过一个触发器后直接输出给q3;
仿真波形:
在时钟沿到来时所有的寄存器输出的值都等于输入值d,在每个时钟上升沿,输入值d将无延时直接输出到q3。
//=======================================================================// 示例2:
在该代码中always的时序模块中使用的是阻塞赋值,但是与示例1比较发现阻塞赋值的顺序改变了;
module pipeb(
output [7:0] q3,
input clk,
input [7:0] d
);
reg [7:0] q1,q2,q3;
always @(posedge clk)
begin
q3 = q2;
q2 = q1;
q1 = d;
end
endmodule
综合后的电路图:
看到输入信号d,经过寄存器q1,q2,q3输出到q3,与我们需要的设计功能一致;
仿真波形:
虽然该代码从综合和仿真结果看,都与我们设计需求结果一样,但是代码的阻塞赋值的顺序是必须要仔细确定安排的。无论时我们自己写代码或是给其他人阅读都会造成困扰,如果设计比较复杂的逻辑,那么代码的顺序变得更加重要。
//=======================================================================//
示例3:
在该代码中always的时序模块中使用的是阻塞赋值,但是每条阻塞赋值放在不同的always的模块中;
module pipeb(
output [7:0] q3,
input clk,
input [7:0] d
);
reg [7:0] q1,q2,q3;
always @(posedge clk)
begin
q1 = d;
end
always @(posedge clk)
begin
q2 = q1;
end
always @(posedge clk)
begin
q3 = q2;
end
endmodule
综合后的电路图:
看到输入信号d,经过寄存器q1,q2,q3输出到q3,与我们需要的设计功能一致;
仿真波形:
在时钟沿到来时所有的寄存器输出的值都等于输入值d,在每个时钟上升沿,输入值d将无延时直接输出到q3。
综合电路和仿真波形对比发现前后实现的功能不一致,前后仿真的结果可能会不一致;而且上述这些always块执行的顺序都是随机的,就会遇到我们之前说Verilog中的竞争和冒险,可能遇到不同仿真器或是每次仿真执行都会有不同的结果。
//=======================================================================//
示例4:
在该代码中always的时序模块中使用的是阻塞赋值,但是每条阻塞赋值放在不同的always的模块中;相对于示例3,只是always块的顺序发生了变化;
module pipeb(
output [7:0] q3,
input clk,
input [7:0] d
);
reg [7:0] q1,q2,q3;
always @(posedge clk)
begin
q2 = q1;
end
always @(posedge clk)
begin
q3 = q2;
end
always @(posedge clk)
begin
q1 = d;
end
endmodule
综合后的电路图:
看到输入信号d,经过寄存器q1,q2,q3输出到q3,与我们要设计的功能一致;
仿真波形:
同样我们发现综合电路和仿真波形对比功能仍不一致,前后仿真的结果不一致。
//=======================================================================//
示例5:
我们将上述的代码中的阻塞赋值都改成非阻塞赋值后发现,综合电路与仿真波形都一样:
仿真波形:
所以在建立时序电路或是锁存电路时,要使用非阻塞赋值,不仅可以避免竞争和冒险,也可以在前后仿真结果保持一致。
我们还是以代码例程来解释说明:
示例1:
module ao4(
output y,
input a,b,c,d
);
reg y,temp1,temp2;
always@( a or b or c or d)
begin
temp1 <= a & b;
temp2 <= c & d;
y <= temp1 |temp2;
end
endmodule
该always块使用a,b,c,d为敏感列表,在always块内部的语句使用非阻塞赋值,由于非阻塞赋值的特性是在更新LHS变量之前先计算RHS的表达式,所以当首次敏感列表中的信号有任何一个发生变化时,temp1和temp2的两个值更新都是之前进入always块之前的值;y的值则是反应之前temp1和temp2之前的值,而不是在always块中计算后得到的值。
仿真波形:
在仿真波形中我们看到有5处输入a,b,c,d发生变化的时刻,由于a,b,c,d都在敏感列表,则他们任意一个信号的变化都会触发该always模块;下面我们就分析这5处时刻temp1,temp2,y的值的变化。
仿真波形图中(1)标记:
a)在该仿真开始时刻a的值0,b的值由0变化到1,a&b计算后得到0;c的值0,d的值0,c&d计算后得到0;
b)在该仿真结束时刻,则temp1的值更新的是0,temp2的值更新的是0,y的值更新得到是0,是因为取的是temp1和temp2之前计算的值都是0;
仿真波形图中(2)标记:
a)在该仿真开始时刻a的值由0变化到1,b的值1,a&b计算后得到1;c的值0变化到1,d的值由0,c&d计算后得到0;
b)在该仿真结束时刻,则temp1的值更新的是1,temp2的值更新的是0,y的值更新得到是0,是因为取的是temp1之前计算的值都是0;
仿真波形图中(3)标记:
a) 在该仿真开始时刻a的值1,b的值由1变化到0,a&b计算后得到0;c的值1,d的值为0,c&d计算后得到0;
b)在该仿真结束时刻,则temp1的值更新的是0,temp2的值更新的是0,y的值更新得到是1,是因为取的是temp1之前计算的值1,temp2之前计算的值0;
仿真波形图中(4)标记:
a) 在该仿真开始时刻a的值由0变化到1,b的值1变化到0,a&b计算后得到0;c的值1,d的值为0,c&d计算后得到0;
b)在该仿真结束时刻,则temp1的值更新的是0,temp2的值更新的是0,y的值更新得到是1,是因为取的是temp1和temp2之前计算的值都是0;
注意:在(3)~(4)这个时间段y的值始终保持为1,在这期间敏感列表中未有任何信号状态改变,所以不会触发always块,则y的值也就不会改变。
仿真波形图中(5)标记:
a) 在该仿真开始时刻a的值0,b的值1,a&b计算后得到0;c的值1,d的值0变化到1,c&d计算后得到1;
b)在该仿真结束时刻,则temp1的值更新的是0,temp2的值更新的是0,y的值更新得到是1,是因为取的是temp1和temp2之前计算的值都是0;
之后只要a,b,c,d信号没有状态变化,那么就不会在更新y的值。
我们还是以代码例程来解释说明:
module bate(
input clk,rst_n,
input a,b,
output q
);
reg q,temp=0;
always@(posedge clk or negedge rst_n)
if(!rst_n)
q <=1'b0;
else
begin
temp <= a & b;
q = temp;
end
endmodule
综合电路:
仿真波形:
从上面的综合电路看到,我们的需求是q的取值是tmp上个时钟周期的数据,但实际综合出来结果看到q的取值是当前tmp的值,而且在综合编译器中将tmp信号给优化掉了;但在仿真时看到的结果与我们所要的需求一致,这就会造成了前后仿真结果不一致。
当我们将代码q的赋值改成非阻塞赋值后,下面的综合电路与仿真结果都一致。
module bate(
input clk,rst_n,
input a,b,
output q
);
reg q,temp=0;
always@(posedge clk or negedge rst_n)
if(!rst_n)
q <=1'b0;
else
begin
temp <= a & b;
q <= temp;
end
endmodule
综合电路:
仿真波形 :
之前的章节我们了解到,always块执行的顺序都是随机的,下面的代码中我们将同一变量q,在两个always模块分别进行赋值,那么必定会产生竞争冒险:
module bate(
input clk,rst_n,
input a,b,
output q
);
reg q=0;
always@(posedge clk or negedge rst_n)
if(!rst_n)
q <=1'b0;
else
q <= a;
always@(posedge clk or negedge rst_n)
if(!rst_n)
q <=1'b0;
else
q <= b;
endmodule
综合电路:
从 综合的电路看到q的输出值直接由两个寄存器同时输出,没有再经过任何逻辑就直接输出,那么结果是未知的。
仿真波形:
从仿真结果看q的输出只与输入b有关系,而与输入a无任何关系;但可能在不同的仿真器中q的输出只与输入a有关系。
所以禁止不同的always模块中对同一变量进行多次的赋值。同时会对这一条的原则会有一些误解。
误解:在Verilog的语法标准中未定义,可在同一个always块中对同一变量进行多次非阻塞赋值;
实际上:Verilog标准定义了在同一个always块中,可对某同一变量进行多次非阻塞赋值,但是在多次赋值中,只有最后一次赋值对该变量起作用。
同样用代码来说明:
initial
begin
sys_clk = 1'b0;
sys_rst = 1'b1;
#1000
sys_rst = 1'b0;
#10
d1 <= 0;
d1 <= 1;
end
仿真波形:
从仿真波形看到d1最终的值是1。在代码中对d1的赋值是非阻塞赋值,则在仿真中会被添加到非阻塞更新事件队列中,在前面章节中了解到,在同一事件队列中按照顺序进入,并按源顺序取出执行。所以,在仿真第一步结束的时刻,变量d1值为0,然后再为1。所以在一个块中对同一变量多次非阻塞赋值都是最后一个赋值起作用。
为什么不能使用$display命令来显示非赋值语句的赋值,而是用$srobe来命令来显示非赋值的变量值?
我们仿真打印显示查看:
initial $monitor("\ $monitor:d1 = %b",d1);
initial
begin
$strobe ("\ $strobe:d1 = %b",d1);
d1 =0;
d1 <=1;
$display ("\ $display:d1 = %b",d1);
#1 $finish;
end
从仿真打印看到,$display命令的执行是安排在活动事件队列中,但是排在非阻塞赋值数据更新事件之前。所以并不是$display命令不能显示非阻塞赋值,而是非阻塞赋值语句的赋值在所有的$display命令执行后才更新数据。
该章节会在后续的系列中详细讲解。