必考。

阻塞赋值对应的电路结构往往与触发沿没有关系,只与输入电平的变化有关系。而非阻塞赋值对应的电路结构往往与触发沿有关系,只有在触发沿时才有可能发生赋值的变化。

阻塞赋值

在描述组合逻辑的always块中用阻塞赋值,则综合成组合逻辑的电路结构。

为什么称这种赋值为阻塞赋值呢? 这是因为在赋值时先计算等号右手方向( RHS )部分的值,这时赋值语句不允许任何别的 Verilog 语句的干扰,直到现行的赋值完成时刻,即把 RHS 赋值给LHS 的时刻,它才允许别的赋值语句的执行。

一般可综合的阻塞赋值操作在 RHS 不能设定有延迟(即使是零延迟也不允许)。从理论上讲,它与后面的赋值语句只有概念上的先后,而无实质上的延迟。若在 RHS上加延迟,则在延迟期间会阻止赋值语句的执行,延迟后才执行赋值,这种赋值语句是不可综合的,在需要综合的模块设计中不可使用这种风格的代码.

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
input A, B, Cin;
output Cout;
reg T1 , T2 , T3 ; //内部信号
reg Cout;
always@ (A or B or Cin)
begin
T1 = A&B;
T2 = B & Cin;
T3 = A & Cin;
Cout = T1 | T2 | T3;
end

/* T1赋值首先发生,计算T1
接着执行第二条语句,T2被赋值;
然后执行第三条语句,T3被赋值;
最后才执行Cout赋值语句。
*/

如果在一个过程块中阻塞赋值的 RHS 变量正好是另一个过程块中阻塞赋值的 LHS 变量,这两个过程块又用同一个时钟沿触发,这时阻塞赋值操作会出现问题,即如果阻塞赋值的顺序安排不好,就会出现竞争

不能用同一个时钟沿触发两个阻塞赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module fbosc1 (y1, y2, clk, rst);
output y1, y2;
input
clk, rst;
reg
y1, y2;
always @(posedge clk or posedge rst)
if (rst)
y1 = 0;
// reset
else
y1 = y2;
always @(posedge clk or posedge rst)
if (rst)
y2 = 1;
// preset
else
y2 = y1;
endmodule

两个always块是并行执行的。

若复位信号已从1 到 0,且例中第一个always块的有效时钟沿比下面的always块的时钟沿早几个皮秒(由时钟偏差造成)到达,则 y1 和 y2 都会取 1;

而若下面的那个always 块的有效时钟沿早几个皮秒到达,则 y1 和 y2 都会取 0。

说明这个 Verilog 模块是不稳定的, 必定会产生冒险和竞争的情况。

非阻塞赋值

在赋值操作时刻开始时计算非阻塞赋值符的 RHS 表达式,赋值操作结束时刻才更新LHS。在计算非阻塞赋值的RHS表达式和更新LHS期间,其他的Verilog 语句,包括其他的Verilog 非阻塞赋值语句都能同时计算 RHS 表达式和更新LHS。

使用赋值号<=的过程赋值,指过程语句的RHS执行完,立刻执行下一个语句,而预定在将来某个时刻完成赋值(有延时),没有延时的话就在当前时间步完成赋值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module evaluates2(out);
output out;
reg a, b, c;
initial
begin
a = 0;
b = 1;
c = 0;
end
always c = #5 ~c;
always @(posedge c)
begin
a <= b;
b <= a;
end
endmodule
  1. 仿真器对RHS表达式求值,安排新值的赋值在过程时间控制指定的时间发生;在5时刻,在c的上升沿前,a=0,b=1
  2. 在时间步结束后、指定的延时已经过去或者适当的事件发生的时刻,仿真器对LHS赋值,更新LHS的值。在c的上升沿后,a=1,b=0

时序与逻辑的编程要点

  • 时序电路建模时,用非阻塞赋值。
  • 锁存器电路建模时,用非阻塞赋值。//不推荐用锁存器
  • 用 always 块建立组合逻辑模型时,用阻塞赋值。
  • 在同一个 always 块中建立时序和组合逻辑电路时,用非阻塞赋值
  • 在同一个 always 块中不要既用非阻塞赋值又用阻塞赋值
  • 不要在一个以上的 always 块中为同一个变量赋值
  • $strobe系统任务来显示用非阻塞赋值的变量值。
  • 在赋值时不要使用 #0 延迟。

层次化事件队列

所谓层次化事件队列指的是用于调度仿真事件的不同的Verilog 事件队列。在 IEEE1364-1995Verilog标准的 5.3 节中定义了层次化事件队列在逻辑上分为用于当前仿真时间的 4 个不同的队列,和用于下一段仿真时间的若干个附加队列。

层次化事件模型

从图中可知,$display所在的事件层次是活跃事件,而其中的事件发生顺序是随机的。因此$display不能用于监视被非阻塞赋值的变量。

相对于$display$monitor$strobe所在的事件层次是监控事件,和非阻塞赋值更新事件及活跃事件独立,因此可以很好的监控被非阻塞赋值的变量。

自触发的always块

有关always块的内容,请参阅:

数字系统设计复习笔记:第二篇

一般而言,Verilog的always不能触发自己,如下面关于使用阻塞赋值的非自触发振荡器等。

我不触发我自己

1
2
3
4
5
6
7
8
module osc1 (clk);
output clk;
reg
clk;
initial
#10 clk = 0;
always @(clk) #10 clk = ~clk;
endmodule

上例在 initial 块中,经过 10 个单位时间的延迟,clk被立即阻塞赋值为 0 。

当 clk 电平从不定态变为 0 的事件发生时,使always块的@(clk)条件触发,经过10个单位时间的延迟,计算 RHS 表达式~clk得到 1,并立即更新 LHS 的值,clk立即被赋予 1 。

由于在此期间不允许其他语句的干扰,即always循环回到判断触发条件@(clk),由于此时 clk 电平已经为 1,无法感知从 0 到 1 曾经发生过的变化,所以就阻塞在那里,只有等待 clk 变为 0 才能进入下一句。

因此,这是一个不能自触发的振荡器,不能产生时钟波形。

我触发我自己

1
2
3
4
5
6
7
8
module osc2 (clk);
output clk;
reg
clk;
initial
#10 clk = 0;
always @(clk) #10 clk <= ~clk;
endmodule

@(clk)的第一次触发之后,非阻塞赋值的 RHS 表达式便计算岀来,并把值賦给 LHS 的事件并安排在更新事件队列中。

在非阻塞赋值更新事件队列被激活之前,又遇到了@(clk)触发语句,并且always块再次对clk 的值变化产生反应。

当非阻塞 LHS的值在同一时刻被更新时,@(clk)再一次触发。

该例是自触发式,虽例中的代码能产生周期时钟信号, 但在编写仿真测试模块时不推荐使用这种写法的时钟信号源。

还是用initial+forever吧。

线性反馈移位寄存器

线性反馈移位寄存器( LinearFeedbackShift-Register , LFSR )是带反馈回路的时序逻辑。常用于内建自测试,伪随机数生成,加密解密,数据完整性检验,数据压缩等。

1
2
3
4
5
6
7
8
9
10
11
12
module lfsrb2 (q3, clk, pre_n);
output q3;
input
clk, pre_n;
reg
q3, q2, q1;
always @(posedge clk or negedge pre_n)
if (!pre_n)
{q3,q2,q1} = 3'b111;
else
{q3,q2,q1} = {q2,(q1^q3),q3};
endmodule

时序和组合的混合逻辑

将简单的组合逻辑和时序逻辑写在一起很方便。当把组合逻辑和时序逻辑写入到一个always块中时,应遵从时序逻辑建模的原则,使用非阻塞赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module nbex2 (q, a, b, clk, rst_n);
output q;
input
clk, rst_n;
input
a, b;
reg
q;
always @(posedge clk or negedge rst_n)
if (!rst_n)
q <= 1'b0; // 时序逻辑
else
q <= a ^ b;// 异或,为组合逻辑
endmodule

Verilog 语法并没有禁止将阻塞和非阻塞赋值自由地组合在一个always块里。 虽然 Verilog 语法是允许这种写法, 但不建议可综合模块的编写中采用这种风格。

不建议的风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//(应尽量避免使用这种风格的代码,在可综合模块中应严禁使用)
module ba_nba2 (q, a, b, clk, rst_n);
output q;
input
a, b, rst_n;
input
clk;
reg
q;
always @(posedge clk or negedge rst_n)
begin: ff
reg tmp;
if (!rst_n)
q <= 1'b0;
else
begin
tmp = a & b;
q <= tmp;
end
end
endmodule

禁止对同一变量既进行阻塞赋值,又进行非阻塞赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module ba_nba6 (q, a, b, clk, rst_n);
output q;
input
a, b, rst_n;
input
clk;
reg
q, tmp;
always @(posedge clk or negedge rst_n)
if (!rst_n)
q = 1'b0;
// 对 q进行阻塞赋值
else
begin
tmp = a & b;
q <= tmp;
// 对 q进行非阻塞赋值
end
endmodule

上面的代码会产生综合错误

严禁在多个always块中对同一个变量赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module badcode1 (q, d1, d2, clk,
rst_n);
output q;
input
d1, d2, clk, rst_n;
reg
q;
always @(posedge clk or negedge
rst_n)
if (!rst_n)
q <= 1'b0;
else
q <= d1;
always @(posedge clk or negedge
rst_n)
if (!rst_n)
q <= 1'b0;
else
q <= d2;
endmodule

上面的代码会产生竞争冒险

对非阻塞赋值的误解

$display

关于$strobe/$display,可以参阅之前的笔记:

数字系统设计复习笔记:第四篇

非阻塞语句的赋值所有的$display命令执行以后更新数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module display_cmds;
reg a;
initial
$monitor("\$monitor: a = %b", a);
initial
begin
$strobe("\$strobe : a = %b", a);
a = 0;
a <= 1;
$display("\$display: a = %b", a);
#1 $finish;
end
endmodule
/*
$display:a=0
$monitor:a=1
$strobe :a=1
*/

#0延时

若延时表达式值为0,称为显式零延时

零延时使当前模拟时间里并行执行的其他语句都执行完了以后,才执行0延时的语句。尽量不要使用零延时控制。

#0延时会将赋值事件强制加入停止运行事件队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module nb_schedule1;
reg a, b;
initial begin
a = 0;
b = 1;
a <= b;
b <= a;
$monitor("%0dns: \$monitor: a=%b b=%b", $stime, a, b);
$display("%0dns: \$display: a=%b b=%b", $stime, a, b);
$strobe("%0dns: \$strobe : a=%b b=%b\n", $stime, a, b);

#0 $display("%0dns: #0 : a=%b b=%b", $stime, a, b);

#1 $monitor("%0dns: \$monitor: a=%b b=%b", $stime, a, b);
$display("%0dns: \$display: a=%b b=%b", $stime, a, b);
$strobe("%0dns: \$strobe : a=%b b=%b\n", $stime, a, b);
$display("%0dns: #0 : a=%b b=%b", $stime, a, b);

#1 $finish;
end
endmodule
运行结果

对同一变量进行多次非阻塞赋值

Verilog 标准定义了在同一个always块中,可对某同一变量进行多次非阻塞赋值,但在多次赋值中,只有最后一次赋值对该变量起作用。

常见错误

在for循环下标中使用++、--

SystemVerilog支持这种写法,但是VHDL2001不支持考试不许写

在多个门原语实例时,实例之间用分号

正确的写法是:

1
2
3
not NT1(NA,A),
NT2(NB,B),
NT3(NC,C);

不在initial或always语句中就使用过程语句

多个过程语句没有用begin...end

在always语句中少了事件控制敏感量表

1
2
3
4
5
6
7
8
9
10
11
12
module mux(out, a, b, select);
output out;
reg out;
input a, b, select;
always
case(select)
1'b0:
out=a;
1'b1:
out=b;
endcase
endmodule

上面的代码中,always语句由于没有时序控制,会进入死循环,仿真器时间永远不会向前走

在always语句描述组合逻辑时,应该把所有的输入变量(always中所有被读到的变量)放在事件控制敏感量表中。