最近在做数字的东西,因此一直在学习verilog的语法,看的是夏宇闻老师的《verilog数字系统设计教程》这本书,在看到第14章深入理解阻塞与非阻塞赋值的不同时,结合书后面的誓言RISC_CPU,关于时序问题,产生了一些疑问,因此写了一个简单的程序,探索一下相关的内容,文笔拙劣,理解也并不完全正确,想写出来与大家分享一下,希望能够得到一些指点。
先引用书上的两个例子:
1.采用阻塞赋值,不能自行触发的振荡器
module osc(clk);
output clk;
reg clk;
initial #10 clk = 0;
always @(clk)
begin
$display("At%tns, be excuted.", $time);//just for test.
#10;
clk = ~clk;
end
endmodule
在initial块中,经过10个单位的延迟,clk被立即阻塞赋值为0。当clk电平从不定态变为0的事件发生时,使always块的@(clk)条件触发,经过10个单位时间的延迟,计算RHS表达式(~clk)得到1,并立即更新LHS的值,clk立即被赋予1。由于在此期间不允许其他语句的干扰,即使always循环回到判断触发条件@(clk),由于此时clk电平已经为1,无法感知从0到1曾经发生过的变化,所以就阻塞在那里,只有等待clk变为0才能进入该always块,因此,这是一个不能自触发的振荡器,不能产生时钟波形。
2. 采用非阻塞赋值的自触发振荡器
module osc(clk);
output clk;
reg clk;
initial #10 clk = 0;
always @(clk)
begin
$display("At%tns, be excuted.", $time);//just for test.
#10;
clk <= ~clk;
end
endmodule
与上例相比,只是赋值方式有"="变为"<="。@(clk)的第一次触发之后,非阻塞赋值的RHS表达式便计算出来,并把值赋给LHS的事件安排在更新事件队列中。在非阻塞赋值更新事件队列被激活之前,又遇到@(clk)触发语句,并且always块再次对clk的变化产生反应。当非阻塞LHS的值在同一时刻被更新时,@(clk)再一次触发。该例是自触发方式,虽例中的代码能产生周期时钟信号,但在编写仿真测试模块时,不推荐该写法。
关于阻塞与非阻塞的实践:
1. 一个简单的时钟分频模块
module delayornot(fetch, clk, rst);
output fetch;
input clk, rst;
reg fetch;
integer i;
always @(posedge clk or negedge rst)
begin
if (!rst)
begin
fetch <= 0;
i <= 0;
end
else
begin
if (i == 4)// divided by 5.
begin
fetch <= ~fetch;
i <= 0;
end
else
i <= i+1;
end
end
endmodule
2. 测试模块
module delay_tb();
wire fetch;
reg clk, rst;
reg [3:0] num1;
reg [3:0] num2;
reg [3:0] num3;
initial
begin
num1 = 4'b0;
num2 = 4'b0;
num3 = 4'b0;
clk = 0;
rst = 1;
#80;
rst = 0;
#80;
rst = 1;
#500;
end
always #40 clk = ~clk;
delayornot mydelay(fetch, clk, rst);
always @(posedge fetch)
begin
num1 <= num1 + 1;
end
always @(posedge fetch)
begin
if (fetch)
num2 <= num2 + 1;
end
always @(posedge clk)
begin
$display("At%tns, be excuted.",$time);//just for test.
if (fetch)
begin
num3 <= num3 + 1;
end
end
3. 仿真结果
从仿真图中可以看到,delayornot模块实际上是一个5分频的模块,fetch信号采用非阻塞赋值方式。
在当fetch信号上升沿到来时,由于num1和num2所在always块触发条件为fetch的上升沿,因此在fetch上升沿,触发了num1和num2所在的always块,因此各自加1。而num3所在的always块触发条件为clk信号的上升沿,注意图中fetch信号上升沿到来的位置。此时,clk信号为上升沿,因此触发了num3的always块。但注意,此时,由于fetch信号在其模块中为非阻塞赋值,即等fetch信号相应always块结束时,才进行赋值操作。因此,此时fetch信号为低电平,即if (fetch)不成立,因此num3未进行加1操作。num3在下一个clk上升沿时才加1,并且在fetch下降时来临时依然加1,原因类似。
可能有人会疑问,那为什么num2中的if (fetch)就成立了呢?因为,num2所在always块检测的是fetch信号的上升沿,当其上升沿到来时,fetch已经为高电平,因此条件成立。verilog里很重要的一点是,若不加#10等延时命令,则指令执行往往有概念上的先后,而并无实质上的先后。通俗的讲,fetch信号上升沿到来时,num1和num2所在always块才被触发,而num3所在always块检测的是clk的上升沿,也就是说在fetch信号被赋值之前该always块就已经被触发执行了,因此与fetch信号的上升沿无关。
4. 修改num3所在always块代码
always @(*)
begin
$display("At%tns, be excuted.",$time);//just for test.
if (fetch)
begin
num3 <= num3 + 1;
end
end
always @(*)的作用与always @(fetch or num3)的作用一致。(*)里面的敏感变量由综合器根据always块里面的输入变量自动添加。
此时,进行仿真,发现modelsim报错。# ** Error: (vsim-3601) Iteration limit reached at time 520 ns.
根据$display语句在屏幕的输出发现,程序在520ns的时候一直在触发always块。从上面的波形图可以发现,520ns为fetch信号第一次出现上升沿的位置。此时,通过代码可以发现,fetch信号变化则触发该always块,并且if (fetch)成立,因此num3的值发生变化,进而又触发该always块,陷入死循环。
如果把代码中的非阻塞赋值改为阻塞赋值,则波形图如图所示。为何不会出现死循环,因为它被阻塞了,原理参考于开篇所给出的采用阻塞赋值不能自行触发振荡器。
此时由于触发条件为fetch信号或者num3的变化,所以num3与num1和num2同步。如果这里采用always @(posedge clk)以及阻塞赋值的话,则不会同步。
5. 针对出现死循环的问题
module osc(clk);
output clk;
reg clk;
initial #10 clk = 0;
always @(clk)
begin
$display("At%tns, be excuted.", $time);//just for test.
//#10;
clk <= ~clk;
end
endmodule
修改采用非阻塞赋值的自触发振荡器的代码,将#10注释掉。仿真发现modelsim报同样的错误。# ** Error: (vsim-3601) Iteration limit reached at time 10 ns.
在同一时间,一直进入该always块,陷入死循环,因而报错。而加上#10延时命令,就可以避免该问题。