http://mp.weixin.qq.com/mp/homepage?__biz=MzU3OTczMzk5Mg==&hid=7&sn=ad5d5d0f15df84f4a92ebf72f88d4ee8&scene=18#wechat_redirect
--------------------------------------------------------------------------------------------------------------------------
据说阻塞赋值和非阻塞赋值是Verilog 语言中最难理解的知识点之一,我也觉得,从网上翻阅了资料,也看过一些视频。
以下从两个典型的例子以及多个角度去分析得到与验证阻塞赋值与非阻塞赋值的区别,以及各自的特点。
非阻塞赋值与阻塞赋值特性
1、非阻塞赋值的特性:
赋值语句的流程:
a)计算右边的表达式得到结果
b)将结果赋值到左边的变量
这个过程中允许来自任何其他Verilog语句的干扰,也就是说其他Verilog语句都可以在这个时候执行。
2、阻塞赋值特性:
赋值语句的流程:
c)计算右边的表达式得到结果
d)将结果赋值到左边的变量
这个过程中不允许有来自任何其他Verilog语句的干扰,这就导致其他的Verilog语句会等待(阻塞)该赋值语句执行完毕后才能被执行,这就是阻塞的含义。
一、非阻塞赋值分析
1、非阻塞赋值源程序
/* 实验名称:非阻塞赋值 */
module mytest(clk, rst_n, a, b, c, out);
input clk, rst_n, a, b, c;
output reg [1:0] out;
reg[1:0] d;
always@(posedge clk or negedge rst_n)
if(!rst_n)
out <= 2'b0;
else begin
d <= a + b;
out <= d + c;
end
endmodule
从源码中分析,根据非阻塞语句的特性, 假设 a = 0,b = 1, c = 0;
当rst_n为高时,由于d<=a+b、out<=d+c; 是同时执行,所以:
1、先右边表达式的结果:a + b = 0 + 1 = 1、d + c = 0 + 0 = 0
2、再将结果赋值给左边: d = a + 1就等于 1,out = d + c 就等于 0
2、RTL视图
其实RTL视图中也能看出来,假设a = 0,b = 1, c = 0; d寄存器为0,out寄存器为0 ;
那么当一个时钟周期到来:(以下两步会同时执行,即d与out寄存器会同时从加法器中取值)
1、Add0寄存器会将a(0) + b(1) = 1的值给d寄存器,d寄存器由0变为1
2、Add1寄存器会将d(0) + c(0) = 0的值给out寄存器,out寄存器仍是0
3、仿真测试源程序
`timescale 1ns/1ns
`define clock_period 20
module mytest_tb;
reg clk, rst_n, a, b, c;
wire [1:0] out;
mytest u1(clk, rst_n, a, b, c, out);
initial clk = 1;
always#(`clock_period / 2) clk = ~clk;
initial begin
rst_n = 1'b0;
a = 0;
b = 0;
c = 0;
#(`clock_period * 200 + 1);
rst_n = 1'b1;
#(`clock_period * 20);
a = 0; b = 0; c = 0;
#(`clock_period * 200);
a = 0; b = 0; c = 1;
#(`clock_period * 200);
a = 0; b = 1; c = 0;
#(`clock_period * 200);
a = 0; b = 1; c = 1;
#(`clock_period * 200);
a = 1; b = 0; c = 0;
#(`clock_period * 200);
a = 1; b = 0; c = 1;
#(`clock_period * 200);
a = 1; b = 1; c = 0;
#(`clock_period * 200);
a = 1; b = 1; c = 1;
#(`clock_period * 200);
#(`clock_period * 200);
$stop;
end
endmodule
4、波形图
这是 out 从 0 变成 1 前一个时钟周期的情况,由上图可以看到,当时钟上升沿,采样得到的 a、b、d、c都是等于0,而c是在时钟上升沿之后才被拉高,所以要在下一个时钟周期的上升沿才能采样到c的状态,
如下图:
然后我们再往后看看。
首先我们看到 01 与 01 直接夹着 0,那么01对应的应该是如下几行的测试代码,从代码可以看到不应该有00的存在。
a = 0; b = 0; c = 1;
#(`clock_period * 200);
a = 0; b = 1; c = 0;
#(`clock_period * 200);
因为c变为0,b变为1是在时钟周期的上升沿之后才变化,这里暂时不考虑是在上升沿或下降沿时变化。
而out和d的状态因为过程中需要采样和赋值操作,会有一个逻辑延迟的现象,也就是说也是在时钟上升沿之后才会变化。以下为后仿真的波形图,根据接近实际状况。
即图中A上升沿时:(以下两步的采样是在同一个时钟上升沿同时进行)
1、d的状态:旧状态是0,经过以下操作新的状态是0
同时采样a的状态得到是0,b的状态得到的是0。所以d的状态变为0
2、out的状态:旧状态是1,经过以下操作新的状态变为1
同时到d是0,c的状态得到的是1,所以导致out变为1。
3、在时钟为稳定电平期间c变为0,b变为1,即在上升沿之后。
那么到了B上升沿时:(以下两步的采样是在同一个时钟上升沿同时进行)
1、d的状态:旧状态是0,经过以下操作新的状态是1
同时采样a的状态得到是0, b的状态得到的是1。所以d的状态变为1
2、out的状态:旧状态是1,经过以下操作新的状态变为0
同时采样到d是0,c的状态得到的是0,所以导致out变为0。
当到C上升沿的时:(以下两步的采样是在同一个时钟上升沿同时进行)
1、d的状态:旧状态是1,经过以下操作新的状态是1
同时采样a的状态得到是0,b的状态得到的是1。所以d的状态变为1
2、out的状态:旧状态是0,经过以下操作新的状态变为1
同时采样到d是1,c的状态得到的是0,所以导致out变为0。
6、解决出现0的情况。
分析原因:
从RTL视图中可以看到是因为多了几个d寄存器导致out寄存器慢了一个时钟。
从波形视图中可以看到也是因为多了d,导致out寄存器出现了0状态。
结论:
将d去掉即可,以后写程序过程中尽量不要采用中间变量,避免出现多余的寄存器出现,导致不同步。
源程序如下:
`timescale 1ns/1ns
/* 实验名称:非阻塞赋值 */
module mytest(clk, rst_n, a, b, c, out);
input clk, rst_n, a, b, c;
output reg [1:0] out;
//reg[1:0] d;
always@(posedge clk or negedge rst_n)
if(!rst_n)
out <= 2'b0;
else begin
//d <= a + b;
//out <= d + c;
out <= a + b + c;
end
endmodule
RTL视图:(从下图可以看到,来一个时钟周期,那么out直接就取Add0和Add1的值)
波形图:(下图比较小,可以对比上面的波形图)
二、阻塞赋值分析
1、源程序(为了更加清晰的理解,我修改了视频作者的源码)
`timescale 1ns/1ns
/* 实验名称:非阻塞赋值 */
module mytest(clk, rst_n, a, b, c, out);
input clk, rst_n, a, b, c;
output reg [1:0] out;
reg[1:0] d;
reg run; // 这个是用来指示当前执行的位置
always@(posedge clk or negedge rst_n)
if(!rst_n)
out = 2'b0;
else begin
d = a + b;
run =#1 1; // 准备执行out赋值时
out = d + c;
run =#1 0; // 执行out赋值后
end
endmodule
从源码中分析,根据阻塞语句的特性, 假设 a = 0,b = 1, c = 0;
当rst_n为高时,由于d=a+b、out=d+c; 是顺序执行,所以:
1、先执行:d = a + b = 0 + 1 = 1
2、在执行:out = d + c = 1 + 0 = 1;
2、RTL视图(没看错,就和去掉了d寄存器的非阻塞赋值代码生成的电路一样)
从这里就能看出与非阻塞赋值的不同之处,此处不再解释。
3、仿真测试程序
与非阻塞仿真测试程序一样,略。
4、波形图
5、波形图分析
根据代码我们知道run信号是执行完d = a + b 之后为高电平,执行完 out = d + c 为低电平。
通过run信号知道,每次都是在out变化之后才出现低电平。因为如果是同时执行的话,速度非常快,由于run原先就是低电平,变为高电平和变为低电平都是延时1ns,也就是说同时执行1ns之后run依然会是低电平,如下图,同样的延时,改为非阻塞的方式,out一直为低。
A时钟上升沿到来:(以下三个步骤为顺序执行)
1、d的状态:当前状态0,经过采样,新的状态更新为0
同时采样a的状态为0、 b的状态为0,d = a + b = 0 + 0 = 0;
2、run信号:
被拉高
3、out的状态:当前状态0,经过采样,新的状态更新为0
同时采样d的状态0、c的状态为0,out = d + c = 0 + 0 = 0;
4、run信号:
被拉低
B时钟上升沿到来:(以下三个步骤为顺序执行)
1、d的状态:当前状态0,经过采样,新的状态更新为0
同时采样a的状态为0、 b的状态为0,d = a + b = 0 + 0 = 0;
2、run信号:
被拉高
3、out的状态:当前状态0,经过采样,新的状态更新为1
同时采样d的状态0、c的状态为1,out = d + c = 0 + 1 = 1;
4、run信号:
被拉低
C时钟上升沿到来:(以下三个步骤为顺序执行)
1、d的状态:当前状态0,经过采样,新的状态更新为0
同时采样a的状态为0、 b的状态为0,d = a + b = 0 + 0 = 0;
2、run信号:
被拉高
3、out的状态:当前状态1,经过采样,新的状态更新为1
同时采样d的状态0、c的状态为1,out = d + c = 0 + 1 = 1;
4、run信号:
被拉低
通过以上步骤可以发现,执行的时间、采样的时间不一样,会导致不一样的结果。
一、非阻塞赋值分析
1、非阻塞赋值例程
/* 实验名称:阻塞与非阻塞赋值差异实验
* 程序功能:非阻塞赋值 - 观看 RTL-View 以及波形 */
module mytest(o_y1, o_y2, i_clk, i_rst);
output reg o_y1, o_y2;
input wire i_clk, i_rst;
//异步复位 alwaysA
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y1 <= 0; // 低电平复位
else
o_y1 <= o_y2;
//异步复位 alwaysB
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y2 <= 1; // 低电平复位
else
o_y2 <= o_y1;
endmodule
2、RTL 视图
3、仿真源程序
`timescale 1ns/1ns
`define clock_period 20
module mytest_tb;
reg clk, rst;
wire y1, y2;
mytest u1(y1, y2, clk, rst);
initial clk = 1'b1;
always #(`clock_period / 2) clk = ~clk;
initial begin
rst = 1'b0; // 复位
#(`clock_period * 5);
rst = 1'b1; // 开始执行
#(`clock_period * 20);
rst = 1'b0;
#(`clock_period * 5);
rst = 1'b1;
#(`clock_period * 20);
rst = 1'b0;
#(`clock_period * 5);
rst = 1'b1;
#(`clock_period * 20);
$stop;
end
endmodule
4、波形图
5、波形分析
//异步复位 alwaysA
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y1 <= 0; // 低电平复位
else
o_y1 <= o_y2;
//异步复位 alwaysB
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y2 <= 1; // 低电平复位
else
o_y2 <= o_y1;
当 i_rst == 0 :o_y1 被赋值为 0, o_y2 被赋值为 1,由于非阻塞赋值特性,所以无先后顺序
当 i_rst == 1 :由于两个 always 块会被同时执行。即 fpga 会在同一个时钟上升沿进行采样,
第一个时钟周期:
alwaysA 会对 o_y2 采样得到为高电平,所以给 o_y1 赋值为高电平。
alwaysB 会对 o_y1 采样得到为低电平,所以给 o_y2 赋值为低电平。
第二个时钟周期:
alwaysA 会对 o_y2 采样,因为在第一个周期中 o_y2 因 o_y1 而赋值为低电平,所以采样得到为低电平,所以给 o_y1 赋值为低电平。
alwaysB 会对 o_y1 采样,因为在第一个周期中 o_y1 因 o_y2 而赋值为高电平,所以采样得到为高电平,所以给 o_y2 赋值为高电平。
之后的时钟周期都是根据这样的规律进行变化,所以我们看到的波形 o_y1、o_y2 是相反状态。
二、阻塞赋值分析
1、阻塞赋值源程序
/* 实验名称:阻塞与非阻塞赋值差异实验
* 程序功能:阻塞赋值 - 观看 RTL-View 以及波形
*/
module mytest(o_y1, o_y2, i_clk, i_rst);
output reg o_y1, o_y2;
input wire i_clk, i_rst;
//异步复位
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y1 = 0; // 低电平复位
else
o_y1 = o_y2;
//异步复位
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y2 = 1; // 低电平复位
else
o_y2 = o_y1;
endmodule
2、RTL 视图(居然和阻塞一样的 RTL 视图)
3、仿真源程序
和非阻塞仿真源程序一样。略
3、波形图
4、波形分析
//异步复位alwaysA
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y1 = 0; // 低电平复位
else
o_y1 = o_y2;
//异步复位 alwaysB
always@(posedge i_clk or negedge i_rst)
if(!i_rst)
o_y2 = 1; // 低电平复位
else
o_y2 = o_y1;
当 i_rst == 0 :o_y1 被赋值为 0, o_y2 被赋值为 1, 由于阻塞赋值特性,所以有先后顺序,但无法确定谁先谁后。
当 i_rst == 1 :由于阻塞赋值的特性,在执行阻塞赋值语句时其他语句均不能得到执行。由于两个 always 块会被同时执行,而且 o_y1 和 o_y2 的取值是互相影响,所以 o_y1、o_y2 值取决于那个 always 块的赋值语句先执行。从波形来看,明显是 alwaysA 最新得到执行。
第一个时钟周期:
alwaysA 会对 o_y2 采样得到为高电平,所以给 o_y1 赋值为高电平。
alwaysB 会对 o_y1 采样,因在 alwaysA 程序块 o_y1 因 o_y2 而赋值为高电平,所以给 o_y2 赋值为高电平。
第二个时钟周期:
alwaysA 会对 o_y2 采样,因为在第一个周期中 o_y2 因 o_y1 而赋值为高电平,所以采样得到为高电平,所以给 o_y1 赋值为高电平。
alwaysB 会对 o_y1 采样,因在 alwaysA 程序块 o_y1 因 o_y2 而赋值为高电平,所以给 o_y2 赋值为高电平。
也就是说 o_y1 = o_y2; o_y2 = o_y1; 是顺序执行的,所以我们看到的波形 o_y1、o_y2 都是高电平状态.
最后附上非阻塞赋值的一个很有意思的例子。