从零开始的FPGA探索之旅—阻塞赋值与非阻塞赋值
文章内容是基于作者学习过程中的个人理解和总结,记录这些内容是为了自己后续复习或参考。笔者致力于理清学习脉络,书写更全面的学习手册,以后会不定时更新。如果觉得有用,不妨点点关注和大拇指,读者的认可也是笔者前进的动力。
1.定义
写在前面:笔者这一篇文章对于阻塞赋值与非阻塞赋值都是建立在时序逻辑always
语句块下的讨论,assign
只能使用阻塞赋值或者说连线笔者感觉更贴切一点,always@(*)
组合逻辑下讨论没有意义。而我们又为何要讨论它呢,肯定是我们在做设计的时候,使用两种赋值方式会对我们的设计造成影响,到底是什么影响呢,往下看!
阻塞赋值:赋值符号为=
,在同一个 always 块中,按照顺序执行,后续语句必须等待当前赋值完成才能执行(前面的语句阻挡了后面的语句执行),赋值操作会立即更新左侧变量的值,后续操作直接使用新值。
非阻塞赋值:赋值符号为<=
,并行执行,所有右侧表达式在赋值开始时同时计算,被赋值变量在时钟上升沿之后同时更新,上方的变量所赋值的结果不能立即为下面的语句所用。
从它们的定义是不是比较难以看出区别,对我们的设计到底有怎样的影响,下面我们直接举例分析。
2.阻塞赋值与非阻塞赋值举例
阻塞赋值与非阻塞赋值,两个被赋值变量写在同一个语句块和不同语句块情况有所不同,在这里分开进行讨论。
2.1在不同语句块下阻塞赋值与非阻塞赋值讨论
首先我们来讨论一下被赋值语句在不同的always
语句块下进行赋值,它的仿真时序和综合后的电路情况。整个实验的代码功能非常简单,就实现一个三个输入的加法运算,仅仅是增加了一个中间变量add
中转了一下,随着我们的写法改变,仿真时序图也会改变,综合后的电路图也会改变。
2.1.1在不同语句块下非阻塞赋值
不同语句块下的非阻塞赋值程序源码如下:
module block_top(
input wire sys_clk ,
input wire reset_n ,
input wire a ,
input wire b ,
input wire c ,
output reg [1:0] out
);
reg [1:0] add;
always@(posedge sys_clk or negedge reset_n)begin
if(!reset_n)begin
add <= 2'd0;
end
else begin
add <= a + b;
end
end
always@(posedge sys_clk or negedge reset_n)begin
if(!reset_n)
out <= 2'd0;
else
out <= add + c;
end
不同语句块下的非阻塞赋值仿真文件代码如下:
module tb_block_top();
reg sys_clk ;
reg reset_n ;
reg a ;
reg b ;
reg c ;
initial begin
sys_clk = 0;
end
always #10 sys_clk = ~sys_clk;
initial begin
reset_n = 0;
a = 0 ;
b = 0;
c = 0;
#102;
reset_n = 1;
#20;
a = 0 ;
b = 1;
c = 1;
#40;
a = 1 ;
b = 1;
c = 1;
end
block_top block_top_inst(
.sys_clk(sys_clk) ,
.reset_n(reset_n) ,
.a (a ) ,
.b (b ) ,
.c (c )
);
不同语句块下的非阻塞赋值仿真波形图如下:
波形图中,在a、b、c值发生改变后:
- 第一个时钟上升沿add为1,执行的是a +b = 0+1=1;out为1,执行的是add +c = 0+1=1;
- 第二个时钟上升沿add为1,执行的是a +b = 0+1=1;out为2,执行的是add +c = 1+1=2;
- 第三个时钟上升沿add为2,执行的是a +b = 1+1=2;out为2,执行的是add +c = 1+1=2;
- 第四个时钟上升沿add为2,执行的是a +b = 1+1=2;out为3,执行的是add +c = 2+1=3;
这样分析下来,是不是符合我们时序电路的波形图,并且两条语句是并行执行的在时钟上升沿到来互补干扰是我们理想的时序。
再来看综合后生成的电路图:
综合后生成的电路图也是符合我们预期的,和仿真波形图能够匹配得上。
2.1.2在不同语句块下阻塞赋值
我们来换一种写法,将<=
修改为=
:
always@(posedge sys_clk or negedge reset_n)begin
if(!reset_n)begin
add = 2'd0;
end
else begin
add = a + b;
end
end
always@(posedge sys_clk or negedge reset_n)begin
if(!reset_n)
out = 2'd0;
else
out = add + c;
end
看看这样写的仿真电路图又是怎样的呢:
从波形图中很明显的我的输出out的时序改变了,第一个时钟上升沿就等于2。在a、b、c值发生改变后:
- 第一个时钟上升沿add为1,执行的是a +b = 0+1=1;out为2,执行的是add +c =1+1=2;
- 第二个时钟上升沿add为1,执行的是a +b = 0+1=1;out为2,执行的是add +c = 1+1=2;
- 第三个时钟上升沿add为2,执行的是a +b = 1+1=2;out为3,执行的是add +c = 2+1=3;
- 第四个时钟上升沿add为2,执行的是a +b = 1+1=2;out为3,执行的是add +c = 2+1=3;
也就是说,在第一个时钟上升沿到来的时候,add的值改变后,立马将新值给到 out = add + c;
参与运算,第四个时钟上升沿同理,而这,并不是我们想要的波形。
再来看看综合后生成的电路图又是什么情况呢?
与上方的电路图对比,使用阻塞赋值和非阻塞赋值综合后的电路图是一模一样的。现在就面临一个问题,电路图相同,仿真图却不同,到底哪个的仿真图是对的呢,在我们心中肯定早就有了答案。别急,我们再来看看阻塞赋值综合后的功能仿真(仔细看变量名称,不是非阻塞赋值的仿真图)。
阻塞赋值的综合后的功能仿真与非阻塞赋值的仿真是相同的。发现第一个问题了,分两个模块写阻塞赋值综合前仿真和综合后仿真不一致。
2.2在相同语句块下阻塞赋值与非阻塞赋值讨论
如果将两个赋值语句合并为一个赋值语句,情况又是如何呢?
2.2.1在相同语句块下非阻塞赋值
将两个always
语句块合并为一个always
语句块程序如下:
always@(posedge sys_clk or negedge reset_n)begin
if(!reset_n)begin
add <= 2'd0;
out <= 2'd0;
end
else begin
add <= a + b;
out <= add + c;
end
end
合并为一个always
语句非阻塞赋值和分两个语句块其仿真波形图和综合后的电路图是相同的,并且将out <= add + c;
语句提到add <= a + b;
前面,电路图和仿真图都是一样的。为了减小篇幅,就不贴图了。
2.2.2在相同语句块下阻塞赋值
使用阻塞赋值的源码如下:
always@(posedge sys_clk or negedge reset_n)begin
if(!reset_n)begin
add = 2'd0;
out = 2'd0;
end
else begin
add = a + b;
out = add + c;
end
end
仿真图如下:
仿真图与之前的分开写仿真图是相同的,同样是在时钟上升沿到来的时候,add的值改变后,立马将新值给到 out = add + c;
进行运算。那么综合后的电路图呢,来看看:
综合后的电路图发生了非常明显的改变,变量add
直接被优化了,仅存两个out
的D触发器,用于缓存数据,在时钟上升沿到来时更新,其他部分全部变成了组合逻辑。
再将 out = add + c;
写到前面呢,下面是代码:
always@(posedge sys_clk or negedge reset_n)begin
if(!reset_n)begin
add = 2'd0;
out = 2'd0;
end
else begin
out = add + c;
add = a + b;
end
end
我们再来看它的仿真波形图:
仿真波形图又变了,变得和非阻塞赋值相同了。
- 第一个时钟上升沿add为1,执行的是a +b = 0+1=1;out为1,执行的是add +c = 0+1=1;
- 第二个时钟上升沿add为1,执行的是a +b = 0+1=1;out为2,执行的是add +c = 1+1=2;
- 第三个时钟上升沿add为2,执行的是a +b = 1+1=2;out为2,执行的是add +c = 1+1=2;
- 第四个时钟上升沿add为2,执行的是a +b = 1+1=2;out为3,执行的是add +c = 2+1=3;
而它综合后的电路图也和非阻塞赋值一样,也就是说,在同一个always
语句块下, add = a + b;
和 out = add + c;
这两条赋值语句,顺序不同,仿真结果不同,综合后的电路图也不同。
3.总结
通过以上的分析,我们知道了:
- 分两个
always
模块写阻塞赋值综合前仿真和综合后仿真不一致。 - 同一个
always
模块中,add = a + b;
和out = add + c;
这两条赋值语句,所放顺序不同,仿真结果不同,综合后的电路图也不同。
以上的情况在做设计的时候,我们是不想让它出现的,如何避免呢,记住以下要点(摘自Verilog 数字系统设计教程(第2 版)-夏宇闻 -第14章深入理解阻塞和非阻塞赋值的不同):
- 时序电路建模时,用非阻塞赋值。
- 锁存器电路建模时,用非阻塞赋值。
- 用 always 块建立组合逻辑模型时,用阻塞赋值。
- 在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值。
- 在同一个 always块中不要既用非阻塞赋值又用阻塞赋值。
- 不要在一个以上的 always 块中为同一个变量赋值。
- 用$strobe系统任务来显示用非阻塞赋值的变量值。
- 在赋值时不要使用 #0延迟。
4.参考文献
1.Verilog 数字系统设计教程(第2 版)-夏宇闻
2.视频:小梅哥FPGA-08阻塞赋值与非阻塞赋值详解