跳转至

再遇 Verilog

在 Lab1 中,我们初步学习了 Verilog 硬件描述语言的语法知识。现在的你应当具有了阅读与编写基本的 Verilog 代码的能力。

Image title

小测试

你还记得下面这些概念吗?

  • 连续赋值与过程赋值
  • 阻塞赋值与非阻塞赋值
  • 结构化、数据流、行为级描述

本小节我们将对 Verilog 语法进行一些补充,帮助大家更好地理解 Verilog 的特性。


1.1 硬件层面的并行

我们知道,不同 always 块和 initial 块之间是并行执行的,而同一个模块里的 always 语句和 initial 语句执行顺序与其在模块中的位置无关。always 块和 initial 块内的过程赋值语句可以是阻塞赋值 =,也可以是非阻塞赋值 <=。其中阻塞赋值用于组合逻辑电路,为串行执行;非阻塞赋值用于时序逻辑电路,为并行执行。

然而,在同一个 always 块里的阻塞赋值语句也可以是并行执行的,只要其内部的信号不会产生冲突。我们来看下面这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
module Test (
    input                       sel,
    output reg [1:0]            out1,
    output reg [1:0]            out2
);
always @(*) begin
    // Part 1
    if (sel)
        out1 = 2'd2;
    else
        out1 = 2'd0;

    // Part 2
    if (sel)
        out2 = 2'd2;
    else
        out2 = 2'd1;
end
endmodule

这段代码包含了一个 always @(*) 语句块,表明当 selout1out2 任何一个发生变化时,就执行内部的语句。不难发现,out1out2 的逻辑是依赖于 sel 信号的,因此我们只需要关注 sel 信号的变化即可。

现在我们来思考下面的问题:

思考

sel 信号从 1'b0 变为 1'b1 的时候,out1out2 谁先发生变化?

按照 C 语言的理解,程序会首先执行 Part 1 中的 if 语句,得到 out1 的值为 2'd2,然后执行 Part 2 中的 if 语句,得到 out2 的值为 2'd2。因此 out1 应当比 out2 先发生变化。

与上面对应的 C 语言代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void FunctionOfAlways (int sel, int *out1, int *out2) {
    // Part 1
    if (sel)
        *out1 = 2;       // 这一句先执行
    else
        *out1 = 0;

    // Part 2
    if (sel)
        *out2 = 2;       // 这一句后执行
    else
        *out2 = 1;

    return;
}

但是在 Verilog 中,结果并不是这样。作为一门硬件描述语言,我们一直在强调:Verilog 中的每一条语句都对应着一种实际的硬件结构,例如 if 语句对应的是选择器。这段代码对应的硬件电路如下图所示:

Image title

不难看出,Part 1 和 Part 2 两部分是分离的,描述了两个不同的选择器。因此在硬件电路层面上,out1out2 信号是同时生成的,二者之间不存在逻辑延迟。

那么,什么时候 Verilog 中的阻塞赋值会串行执行呢?答案是:在信号出现依赖与冲突时,自然就会串行执行了。例如下面这段 Verilog 代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module Test (
    input [1:0]             num1,
    input [1:0]             num2,
    input [1:0]             sel,
    output reg [1:0]        out
);
always @(*) begin
    out = 0;
    // Part 1
    if (sel[0])
        out = num1;

    // Part 2
    if (sel[1])
        out = num2;
end
endmodule

它对应了怎样的硬件结构呢?我们试着照葫芦画瓢画出它的结构:

Image title

Tips

不要忘记第 8 行的 out = 0; 语句!它也对 out 进行了赋值。

很明显,这里上下两部分的结果会产生冲突,因此这样的结构是错误的。在这里,always 中的语句是串行执行的,因为它们都对 out 变量进行了赋值操作。

我们来分析一下这一段 Verilog 代码的逻辑:当 always 语句执行时,首先执行第 8 行的赋值语句,out 的值为 0;接下来执行第 10 行的 if 语句:当 sel[0] 为 1 时将 out 赋值为 num1;然后执行第 14 行的 if 语句:当 sel[1] 为 1 时将 out 赋值为 num2。所以整段逻辑对应的硬件结构为:

Image title

我们将上面的讨论结果总结如下:如果两个 if 的赋值对象没有冲突,那么两个 if 描述的多选器是并行的,否则是串行的。

当然,我们也可以使用 if-else-if 语句实现上面的结构,而 else-if 将显式指出多选器的串行执行顺序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module Test (
    input [1:0]             num1,
    input [1:0]             num2,
    input [1:0]             sel,
    output reg [1:0]        out
);
always @(*) begin
    out = 0;
    if (sel[0])
        out = num1;
    else if (sel[1])    // <- 串行执行
        out = num2;
end
endmodule

1.2 ★ 避免锁存器

锁存器(Latch)是最为基本的时序元件,我们将在后面的实验中对其进行详细介绍。简单来说,锁存器可以存储电路当前的状态保持不变,仅在外部控制信号的驱动下才能更改内部的信息。

Image title

锁存器,但是 Minecraft

在 Verilog 中,一个变量如果声明为寄存器类型(reg),它既可以被综合成组合逻辑的导线,也可能被综合成时序逻辑中的寄存器或锁存器。在使用 always @(*) 语句时,我们会希望变量被综合成导线,但是有时候由于代码书写问题,它会被综合成我们不期望的锁存器结构,进而对电路带来危害。主要有:

  • 电路的输出状态可能发生多次变化,增加了下一级电路的不确定性;
  • 在大部分 FPGA 的设计里,锁存器结构会消耗更多的电路资源;
  • 锁存器导致电路不能按照我们预期的方式工作,在调试时带来额外的问题。

因此,我们在代码书写时需要格外注意,应当避免出现锁存器。一个简单且好记的原则是:组合逻辑中不应出现记忆电路,即电路不能保存自身的状态。违反了这一原则的组合逻辑电路往往就会产生锁存器

我们来看下面的这些例子。

1.2.1 if-else 逻辑缺陷

在组合逻辑电路中,不完整的 if-else 结构会产生锁存器。例如下面这段 Verilog 代码:

if-else 产生的锁存器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module Latch1(
    input               data,
    input               en,
    output reg          q
);

always @(*) begin
    if (en) 
        q = data;
end
endmodule

我们来分析一下。当 en 信号为 1 时,q 会被赋值为 data 的值;当 en 信号不变时,由于 always 语句里的 if 缺少对应的 else 分支,因此编译器默认 else 的分支下寄存器 q 的值保持不变。此时电路应当具有存储数据的功能,所以变量 q 会被综合成锁存器结构。

避免此类锁存器的方法主要有 2 种,一种是补全 if-else 结构,另一种是对信号赋初值。例如,上面的代码可以改为以下两种形式:

if-else 产生的锁存器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 补全 if-else 分支结构    
always @(*) begin
    if (en)  
        q = data;
    else
        q = 1'b0;
end

// 为 q 赋初值
always @(*) begin
    q = 1'b0;
    if (en)
        q = data;
end
有点疑惑?

我们来说明一下赋初值的 always 语句执行逻辑。

由于内部都是对同一个变量的阻塞赋值,因此 always 中的语句是顺序执行的。执行时首先将 q 赋值为 0。如果信号 en 有效,则改写 q 的值为 data,否则 q 会保持为 0(而不是自己先前的值)。因此,这里 q 要么取值为 data,要么取值为 0,不会出现保持自身数值不变的情况,所以不会产生锁存器。

提醒

在时序逻辑中,不完整的 if-else 结构不会产生锁存器,例如下面这段 Verilog 代码:

时序逻辑的 if
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module module_ff(
    input           clk,
    input           en,
    input           data, 
    output reg      q
);
always @(posedge clk) begin
    if (en)
        q <= data;
end
endmodule
这是因为,时序逻辑中的变量 q 会被综合生成寄存器,而寄存器是具有存储功能的,其数值仅在时钟的边沿到来时才会改变,这正是寄存器的特性。

另外一种比较隐蔽的情况是:当条件语句中有很多条赋值语句时,每个分支条件下的逻辑不完整也是会产生锁存器的。例如:

if-else 产生的锁存器 2.0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module Latch2(
    input               data1,
    input               data2,
    input               en,
    output reg          q1,
    output reg          q2
);

always @(*) begin
    if (en)
        q1 = data1;
    else
        q2 = data2;
end
endmodule

这段代码看起来 if 和 else 都有了,但 if 部分里没有对 q2 赋值,else 部分里没有对 q1 赋值。从每个信号各自的的逻辑来看,这实际上也相当于是 if-else 结构不完整,相关信号缺少在其他条件下的赋值行为。

这种情况也可以通过补充完整赋值语句或赋初值来避免产生锁存器。例如:

if-else 产生的锁存器 2.0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 补全 if-else 分支结构    
always @(*) begin
    if (en)  begin
        q1 = data1;
        q2 = 1'b0;
    end
    else begin
        q1 = 1'b0;
        q2 = data2;
    end
end

// 为 q1、q2 赋初值
always @(*) begin
    q1 = 1'b0;
    q2 = 1'b0;
    if (en)
        q1 = data1;
    else
        q2 = data2;
end

1.2.2 case 逻辑缺陷

case 语句产生锁存器的原理几乎和 if 语句一致。在组合逻辑中,当 case 选项列表不全且没有加 default 关键字,或有多个赋值语句不完整时,也会产生锁存器。例如:

case 产生的锁存器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module Latch3(
    input               data1,
    input               data2,
    input [1:0]         sel,
    output reg          q
);

always @(*) begin
    case(sel)
        2'b00:  q = data1;
        2'b01:  q = data2;
    endcase
end
endmodule

这段代码会产生锁存器。因为在 sel 为 2'b10、2'b11 时,case 语句中并没有给出 q 的赋值结果,进而会被默认为保持原先的值不变。

思考

下面这段代码会产生锁存器吗?

1
2
3
4
5
6
always @(*) begin
    case(sel)
        1'b0:  q = data1;
        1'b1:  q = data2;
    endcase
end

同样地,消除此种锁存器的方法也是 2 种:将 case 选项列表补充完整,或对信号赋初值。补充完整 case 选项列表时,可以罗列所有的选项结果,也可以用 default 关键字来代替其他选项结果。

例如,上面的 always 语句有以下 3 种修改方式。

case 产生的锁存器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 使用 default 补充逻辑
always @(*) begin
    case(sel)
        2'b00:    q = data1;
        2'b01:    q = data2;
        default:  q = 1'b0;
    endcase
end

// 枚举补充逻辑
always @(*) begin
    case(sel)
        2'b00:  q = data1;
        2'b01:  q = data2;
        2'b10, 2'b11:  
                q = 1'b0;
    endcase
end

// 使用默认赋值
always @(*) begin
    q = 1'b0;
    case(sel)
        2'b00:  q = data1;
        2'b01:  q = data2;
    endcase
end

更特别地,当 if 和 case 组合起来时,我们往往就容易出现逻辑遗漏。例如下面这段 Verilog 代码:

if 与 case 产生的锁存器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
reg [3:0] value;
always @(*) begin
    case (state)
        2'b00: value = 1;
        2'b01: begin
            if (signal)
                value = 2;
        end
        2'b10: begin
            if (signal)
                value = 3;
        end
        default: value = 0;
    endcase
end

这段代码中尽管有 endcase 语句,但依然会产生锁存器,因为 case 分支中的 if 逻辑不完整。一种比较好的策略是:在 always 语句块的一开始就进行默认赋值。这样可以避免潜在的逻辑不完整风险。

if 与 case 产生的锁存器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
reg [3:0] value;
always @(*) begin
    value = 0;
    case (state)
        2'b00: value = 1;
        2'b01: begin
            if (signal)
                value = 2;
        end
        2'b10: begin
            if (signal)
                value = 3;
        end
        default: value = 0;
    endcase
end

1.3.3 自赋值与判断

在组合逻辑中,如果一个信号的赋值源头有其信号本身,或者判断条件中有其信号本身的逻辑,也会产生锁存器。因为此时的信号也需要具有存储功能,能够获得先前时刻该信号的数值。此类问题在 if 语句、case 语句、问号表达式中都可能出现,例如:

自己作为判断条件
1
2
3
4
5
6
7
reg a, b;
always @(*) begin
    if (a & b)  
        a = 1'b1;   // a 会生成锁存器
    else 
        a = 1'b0;
end
自增?
1
2
3
4
5
6
7
reg a, en;
always @(*) begin
    if (en)
        a = a + 1;  // a 会生成锁存器
    else
        a = 1'b0;
end
条件表达式也要小心
1
2
wire d, sel;
assign d = (sel2 && d) ? 1'b0 : 1'b1;  // d 会生成锁存器

避免此类锁存器的方法只有一种,就是在组合逻辑中避免这种写法。时刻提醒自己:信号不要给信号自己赋值,且不要用赋值信号本身参与判断条件逻辑

Tips:另一种解决方法

如果不要求下一时刻信号立刻输出,我们可以将信号进行一个时钟周期的延时后再接入组合逻辑。例如:

自己作为判断条件
1
2
3
4
5
6
7
reg a, b;
always @(*) begin
    if (a & b)  
        a = 1'b1;   // a 会生成锁存器
    else 
        a = 1'b0;
end

上面这段代码可以更改为:

增加一个周期的延时
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
reg a, b;
reg a_r;

always @(posedge clk)
    a_r <= a;

always @(*) begin
    if (a_r & b)
        a = 1'b1;
    else 
        a = 1'b0;
end

这段代码不会生成锁存器,因为我们人为引入了 a_r 作为寄存变量,用于存储变量 a 先前的值。


休息一会儿

本部分内容到此结束!你理解了多少呢?

首先,我们补充介绍了 Verilog 中语句的串行与并行执行的情况,强调了硬件描述语言与硬件结构的对应关系。

接下来,我们介绍了 reg 型变量的多种电路形式、锁存器的危害与避免方式。为避免锁存器的产生,在组合逻辑设计中,我们需要注意以下几点:

  • if-else 或 case 语句的结构一定要完整;
  • 不要将赋值信号放在赋值源头或条件判断中;
  • 养成使用默认赋值的好习惯。

参考资料


最后更新: October 9, 2023

评论

Authors: wintermelon008