前言
我宣布,前面的心得少部分有问题!
今天发现一个重大问题,就是在编写tb文件时,使用了非阻塞赋值来产生时钟,这使得所有赋值都时同时进行的,会导致与硬件仿真不符的情况。
以后需要在时钟产生的时候进行阻塞赋值
把:
always #10 sys_clk <= ~sys_clk;
修改成:
always #10 sys_clk = ~sys_clk;
下面举一些例子来说明这两者的区别:
边沿检测电路:
源文件:
module edge_detect(
input sys_clk ,
input sys_rst_n ,
input signal ,
output sig_edge ,
output sig_posedge,
output sig_negedge
);
reg signal_d1;
//signal_d1
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
signal_d1 <= 1'b0;
else
signal_d1 <= signal;
end
assign sig_edge = signal ^ signal_d1;
assign sig_posedge = signal && !signal_d1;
assign sig_negedge = !signal && signal_d1;
endmodule
tb文件:
`timescale 1ns/1ns
module tb_edge_detect();
parameter CLK_PERIOD = 20;
reg sys_clk ;
reg sys_rst_n ;
reg signal ;
wire sig_edge ;
wire sig_posedge;
wire sig_negedge;
initial begin
sys_clk <= 1'b0;
sys_rst_n <= 1'b0;
signal <= 1'b0;
#(CLK_PERIOD/2)
sys_rst_n <= 1'b1;
signal <= 1'b1;
#(CLK_PERIOD )
signal <= 1'b0;
#(CLK_PERIOD*2)
signal <= 1'b1;
#(CLK_PERIOD*2)
signal <= 1'b0;
end
always #(CLK_PERIOD/2)
sys_clk <= !sys_clk; //注意这里的非阻塞赋值
edge_detect u_edge_detect(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.signal (signal ),
.sig_edge (sig_edge ),
.sig_posedge(sig_posedge),
.sig_negedge(sig_negedge)
);
endmodule
波形图:
分析:
问题1:出现了毛刺
非阻塞赋值导致了,时钟信号和信号同时到达,但是signal_d1是一个内部变量,这样问题就来了,因为assign了:
assign sig_edge = signal ^ signal_d1;
所以这是一个连续信号,sig_edge 在时钟上升沿这一瞬间是这样判断的:
1.时钟上升沿,数据signal也变化了,因为数据signal是输入的信号,所以取后值,signal是1
2.signal_d1是always语句赋值的内部信号,所以取前值,取0
3.所以这一时刻的值为0^1 = 1,出现了毛刺
问题2:signal_delay没有延迟一周期
在第一个时钟到来的时候,因为时钟使用的是非阻塞赋值,所以时钟是和信号一起变化的,这样一来信号会取变化之后的值,所以当执行:
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
signal_d1 <= 1'b0;
else
signal_d1 <= signal;
end
这个语句块中的else语句时,signal已经取后面的值了,所以是同步变化的。信号没有延后一周期
如果时钟产生信号使用阻塞赋值,则时钟始终是第一个产生的,不存在这个问题:
修改一下tb代码:
`timescale 1ns/1ns
module tb_edge_detect();
parameter CLK_PERIOD = 20;
reg sys_clk ;
reg sys_rst_n ;
reg signal ;
wire sig_edge ;
wire sig_posedge;
wire sig_negedge;
initial begin
sys_clk <= 1'b0;
sys_rst_n <= 1'b0;
signal <= 1'b0;
#(CLK_PERIOD/2)
sys_rst_n <= 1'b1;
signal <= 1'b1;
#(CLK_PERIOD )
signal <= 1'b0;
#(CLK_PERIOD*2)
signal <= 1'b1;
#(CLK_PERIOD*2)
signal <= 1'b0;
end
always #(CLK_PERIOD/2)
sys_clk = !sys_clk; //注意这里的阻塞赋值
edge_detect u_edge_detect(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.signal (signal ),
.sig_edge (sig_edge ),
.sig_posedge(sig_posedge),
.sig_negedge(sig_negedge)
);
endmodule
现在看一下前面的问题:
1.延后成功一周期,因为第一个时钟上升沿到来的时候,时钟先变化signal后变化,所以d1取值为signal跳变之前的值,没有问题
2.尖峰毛刺,仍然存在一个小缺口,这是由于组合逻辑电路导致的,无法避免,assign赋值是电路的联通,如果输入同时变化,结果输出又回到原始状态就会这样丢失一瞬间的值,但是always就不会,因为always在这种突变时刻取到的是很稳定的突变之前的值,所以不会有波形的丢失
寄存器延迟电路:
源代码:
module reg_1(
input sys_rst_n,
input sys_clk ,
input a ,
output reg a_delay
);
reg a_1;
//a_1
always @(posedge sys_clk or negedge sys_rst_n) begin //当时钟上升沿到来的时候,把输入a的值赋给a_1
if(!sys_rst_n) begin
a_1 <= 1'b0;
a_delay <= 1'b0;
end
else begin
a_1 <= a;
a_delay <= a_1;
end
end
endmodule
tb文件:
`timescale 1ns/1ns
module tb_reg_1();
parameter CLK_PERIOD = 5'd20;
reg sys_rst_n;
reg sys_clk ;
reg a ;
wire a_delay ;
initial begin
sys_clk <= 1'b0;
sys_rst_n <= 1'b0;
a <= 1'b0;
#(CLK_PERIOD/2)
sys_rst_n <= 1'b1;
a <= 1'b0;
#(CLK_PERIOD)
a <= 1'b1;
#(CLK_PERIOD*2)
a <= 1'b0;
end
always #(CLK_PERIOD/2)
sys_clk <= !sys_clk;
reg_1 u_reg_1(
.sys_rst_n(sys_rst_n),
.sys_clk (sys_clk ),
.a (a ),
.a_delay (a_delay )
);
endmodule
波形图:
分析:
问题:方法太复杂
用非常复杂的方法实现了延迟一拍,非常麻烦,首先用一个内部寄存器a_1,把非阻塞输入信号a转为了一个内部信号a_1,然后利用a_1在时钟沿到来的时候取前值的特点,让a_delay=a,从而实现了延迟一拍的寄存器,非常不科学。
修改一下tb代码:
always #(CLK_PERIOD/2)
sys_clk = !sys_clk;
修改后的波形图:
这样其实就延时了两拍
分析:第一个时钟上升沿到来的时候,因为时钟是阻塞赋值,所以时钟先变化,执行:
a_1 <= a;
语句的时候,a取变化前的值,即0,所以直接就已经延后一周期了,a_delay则又延后了一周期。这样就重复了,所以我们需要修改一下源代码,不需要a_1了:
module reg_1(
input sys_rst_n,
input sys_clk ,
input a ,
output reg a_delay
);
//a_delay
always @(posedge sys_clk or negedge sys_rst_n) begin //当时钟上升沿到来的时候,把输入a的值赋给a_delay
if(!sys_rst_n)
a_delay <= 1'b0;
else
a_delay <= a;
end
endmodule
修改后产生的波型图:
正常完成延迟一周期的功能
总结
通过上面两个例子我们可以看出,如果不把sys_clk在tb中写成阻塞赋值就会带来大量的麻烦,时钟和其他信号总是同时到来,需要反复思考上升下降沿,信号的值到底是什么,这样非常费脑子,而且会导致大量的错误,所以我们要把时钟写成阻塞赋值,总是时钟第一个到,其他突变的量全部取值为之前的值,这样就很好判断了。
疑问
我是跟着正点原子的教程来的,教程里面就是非阻塞赋值,但是我又看了野火和其他的一些教程,都是阻塞赋值,这就很奇怪,是正点搞错了吗?
然后我去看为什么正点原子的例程是对的,延后都正常,结果正点tb文件全是这样写的:
后面复杂的工程也是一样:
都搞个延迟201ns避开问题,延迟201就可以避免在上升沿进行跳变,从而忽略刚刚我们讨论的问题,然后我就觉得这应该是一种习惯问题,可以用非阻塞赋值配合延迟多一点避开上升沿跳变,也可以写阻塞赋值从而不用考虑避免上升沿跳变这一问题。
这是目前的探究结果。
然而问题还没有结束,最终仿真都是为了能在硬件实现,这两种都能实现功能所以都没有问题。所以硬件仿真里面到底是哪种情况呢,这个等学了ILA调试再回来看看这一篇文章,到时候试一试。