跳转至

案例分析

在本小节中,我们将为大家介绍一些常见的组合逻辑电路,包括加法器、编码器、译码器等。这些电路在数字电路中应用极为广泛,是数字电路设计的基础。 此外,我们还会介绍一些使用组合逻辑电路解决的问题,从需求出发,一步步设计组合逻辑电路,并最终编程实现。


2.1 ★ 组合逻辑元件

2.1.1 编码器

2.1.1.1 简单编码器

编码,是信息从一种形式或格式转换为另一种形式的过程,简单来讲就是语言的翻译过程。在计算机领域,我们需要将日常使用的自然语言翻译成计算机能够理解的二进制编码,这就是编码过程。能够实现该功能的数字电路我们称之为编码器。

例子:国王与电话线

某位国王想要与自己王国的四个区域建立电话联系,于是便为这四个区域各拉了一条电话线,它们汇总在皇宫的信号接收机。我们假定同一时间至多只有一个区域与国王通话。现在国王的需求是:当有电话进来时,接收机能够显示当前通话的区域编号。

我们把上面的场景形式化描述一下:输入信号有四位,分别记为 \(I_3\sim I_0\);为了区分四个不同的区域,我们需要 \(\log_24=2\) bits 的二进制编码,也就是说输出信号有两位,分别记为 \(Y_1\sim Y_0\)。我们可以列出下图所示的真值表:

Image title

注意

上面的真值表仅描述了最简单的情况,没有涵盖所有可能的输入。事实上输出仅有 \(Y_1Y_0\) 是不够的,因为现在的设计无法区分来自 0 号地区的电话响起没有电话响起这两个事件。为了编码器功能的完整性,我们还需要额外的信号来输出当前是否有电话响起,也即当前是否有合法的输入。

你将在实验练习部分解决这个问题。

如何用 Verilog 语言描述编码器呢?我们给出如下的行为级描述代码:

Encode.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module Encode(
    input       [3:0]                   I,
    output reg  [1:0]                   Y
);
always @(*) begin
    case (I)
        4'b1000: Y = 2'b11;
        4'b0100: Y = 2'b10;
        4'b0010: Y = 2'b01;
        4'b0001: Y = 2'b00;
        default: Y = 2'b00;
    endcase
end
endmodule

这里我们假定对于真值表列出情况之外的输入,译码器统一输出 0。

2.1.1.2 优先编码器

例子:国王与电话线 2.0

现在,国王与四个地区的联系更为频繁了,同一时间与国王通话的区域数目没有限制,此时我们必须要有手段处理电话同时接入的情况。国王希望按照地区编号由大到小的顺序设定优先级,当多个区域同时接入电话时,国王会选择接听编号最大的那一个。

普通编码器虽然能实现编码的功能,但它仍有不少局限性,其中之一就表现为:普通编码器的输入端只能同时存在一个高电平信号,当我们不小心输入了多个高电平信号,比如输入 I = 4'b1111,根据代码编码器输出的结果为 2'b00, 与正常输入 I = 4'b0001 的输出结果相同,但我们无法判断此时输入了一个错误的信号。

为了消除这种弊端,我们设想一种新的编码器:它的每个输入端有着不同的重要程度(更专业地说,有着不同的优先级),只要更重要的输入端输入了有效信号,我们就不再考虑来自其他输入端的信号。例如:当 \(I_3\) 输入有效信号时,就不再考虑来自 \(I_2 \sim I_0\) 输入的信号,而在输出端直接输出 2'b11。仅有 \(I_3\) 输入无效信号(为 0)时才会检查 \(I_2\sim I_0\) 的内容。这就是优先编码器的思想。

一个符合要求的优先编码器真值表如下:

Image title

Tips

这里我们使用 x 代表输入为 0/1 均可。不难看出,上面的真值表涵盖了所有 16 种可能的输入。

这里,我们引入了输入有效信号 valid。如果输入为 I = 4'b0000valid 输出 0,表示当前并没有电话接入。

那么,如何使用 Verilog 语言描述呢?我们知道 case 语句是没有优先级顺序的,只能为每一种情况指定其对应的输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
always @(*) begin
    case(I)
        4'b0000: begin 
            Y = 2'b00; valid = 0; 
        end
        4'b1000, 4'b1001, 4'b1010, 4'b1011, 
        4'b1100, 4'b1101, 4'b1110, 4'b1111: begin
            Y = 2'b11; valid = 1;
        end
        4'b0100, 4'b0101, 4'b0110, 4'b0111: begin
            Y = 2'b10; valid = 1;
        end
        4'b0010, 4'b0011: begin
            Y = 2'b01; valid = 1;
        end
        4'b0001: begin
            Y = 2'b00; valid = 1;
        end
    endcase
end

此外,我们也可以尝试使用 if-else-if 结构完成模块描述。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
always @(*) begin
    valid = 1;
    if (I[3]) 
        Y = 2'b11;
    else if (I[2])
        Y = 2'b10;
    else if (I[1])
        Y = 2'b01;
    else if (I[0])
        Y = 2'b00;
    else begin
        Y = 2'b00;
        valid = 0;
    end
end

这样自然能实现需求,但当输入信号位宽较多,例如为 64bits 时,代码编写起来就十分繁琐了。此时我们可以使用 casez 语法等效替代:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
always @(*) begin
    valid = 1;
    casez (I)
        4'b1???: Y = 2'b11;
        4'b01??: Y = 2'b10;
        4'b001?: Y = 2'b01;
        4'b0001: Y = 2'b00;
        default: begin
            Y = 2'b00;
            valid = 0;
        end
    endcase
end
思考

上面三种写法在 Vivado 生成的 RTL 电路中是什么样子的呢?你可以创建一个项目自己尝试一下。

2.1.2 译码器

译码是编码的逆过程。编码过程将自然语言「翻译」成机器能理解的二进制语言, 而译码则是将二进制代码所代表的特定含义『翻译』成对应的自然语言。

例子:国王与电话线 3.0

在你学习《数字电路实验》的这段时间,国王将自己的领土扩展到了八个区域,并连接好了对应的电话线和优先编码器。现在,国王希望你能够帮助他设计一款译码器,根据输入的 3bits 编号 \(A_2\sim A_0\) 接通对应区域的电话线。

简而言之,我们需要将输入的信号 \(A_2\sim A_0\) 翻译为对应的阿拉伯数字。同样地,我们使用 \(Y_7\sim Y_0\) 表示 8 种可能的阿拉伯数字。因此译码器的真值表如下图所示:

Image title

这里由于输入只有 3 bits,因此总可能的输入只有 \(2^3=8\) 种。

译码器的 Verilog 代码如下:

Decoder
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module Decoder (
    input [2:0]             A,
    output reg [7:0]        Y
);
always @(*) begin
    case (A)
        3'b000: Y = 8'b0000_0001;
        3'b001: Y = 8'b0000_0010;
        3'b010: Y = 8'b0000_0100;
        3'b011: Y = 8'b0000_1000;
        3'b100: Y = 8'b0001_0000;
        3'b101: Y = 8'b0010_0000;
        3'b110: Y = 8'b0100_0000;
        3'b111: Y = 8'b1000_0000;
    endcase
end
endmodule

2.1.3 加法器

接下来的例子是关于加法器的。常用的加法器分类如下图所示。

Image title

加法器的分类

2.1.3.1 1bit 半加器

我们首先从最简单的一位半加器开始说起。半加运算是指不考虑来自低位的进位的加法,实现半加运算的电路被称之为半加器。

我们知道,二进制加法与十进制加法本质上是一致的,只是数字变成了二进制。这样的好处是每一位上可能的情况只有 \(2^2=4\) 种而不是 \(10^2=100\) 种。四种情况就是 1 bit 加法的四种可能:0+0、0+1、1+0、1+1。假定输入的两个 1bit 数为 a 和 b,由于结果可能是两位,我们引入两个 1bit 变量 s 和 c 表示结果,其中 s 表示低位,c 表示高位。据此,我们可以列出下面的真值表:

Image title

一位半加器真值表
你知道吗

我们常常使用 s 代表求和,用 c 代表进位。其中 s 是 sum 的缩写,c 是 carry 的缩写。

从上面的真值表中,我们不难得到 s、c 关于 a、b 的逻辑表达式。

\[ s=a\text{ XOR }b \]
\[ c=a\text{ AND }b \]

由此便可以得到半加器的逻辑电路图:

Image title

半加器电路图

对应的 verilog 代码如下:

module HalfAdder(
    input           a, b, 
    output          s, c
);
    assign s = a ^ b;
    assign c = a & b;
endmodule

2.1.3.2 1bit 全加器

当涉及到多位加法时,除了最低位外,每一位都需要考虑来自低位的进位。因此半加器就需要进行一定的改进,支持两个加数以及低位进位三个数的想加。这种运算称为全加运算,实现电路被称为全加器。

Image title

一位全加器真值表

从真值表中得到逻辑表达式如下

\[ s = \overline{a\cdot b\cdot \overline{cin}+a\cdot \overline{b}\cdot cin+\overline{a}\cdot b\cdot cin+\overline{a}\cdot \overline{b}\cdot \overline{cin}} \]
\[ c = \overline{\overline{a}\cdot \overline{b}+\overline{b}\cdot \overline{cin}+\overline{a}\cdot \overline{cin}} \]

这个式子就复杂了一些,但也可以用逻辑门直接搭出,

Image title

一位全加器电路图

我们也可以直接使用先前的半加器搭建全加器。考虑半加器的功能是实现不考虑进位的加法,全加器在此基础上额外引入了前位进位 cin,我们可以将三个数的加法拆解为两次两个数的加法。示意图如下:

Image title

使用半加器搭建全加器

用 verilog 的描述如下:

module FullAdder (
    input       a, b, cin,
    output      s, cout
);
wire temp_s, temp_c_1, temp_c_2;
HalfAdder ha1(
    .a(a),
    .b(b),
    .s(temp_s),
    .c(temp_c_1)
);

HalfAdder ha2(
    .a(temp_s),
    .b(cin),
    .s(s),
    .c(temp_c_2)
);
HalfAdder ha3(
    .a(temp_c_1),
    .b(temp_c_2),
    .s(cout),
    .c()
);
endmodule

对于大多数的应用场景,Verilog 描述加法器最合适的方式是行为描述而不是门级描述。我们完全可以使用 Verilog 的 "+" 运算符描述加法。前提是你需要明白加法器的构造与基本原理!

module FullAdder 
#(
    parameter WIDTH = 8 # 默认宽度为8bit
)(
    input [WIDTH-1: 0]      a, b,
    input                   cin,
    output [WIDTH-1: 0]     s,
    output                  cout
);
assign {cout, s} = a + b + cin;
endmodule

2.2 实际应用

2.2.1 日期电路

我们考虑下面这个例子。

例子:明天的日期?

某同学想要在开发板上搭载一个日历模块,其中一项功能需求是根据当前的日期计算明天的日期。模块的输入输出描述如下:

module Tomorrow (
    input [3:0]         todayMonth,         // 今天的月份,范围 1~12
    input [4:0]         todayDayInMonth,    // 今天的日期,范围 1~31
    input [2:0]         todayDayInWeek,     // 今天星期几,范围 1~7
    output [3:0]        tomorrowMonth,
    output [4:0]        tomorrowDayInMonth,
    output [2:0]        tomorrowDayInWeek
);
......
endmodule

我们可以将功能拆解成两部分:计算日期以及计算星期,因为二者是独立的。

计算星期很简单,每一周都有 7 天,因此只需要判断当天是否是星期日即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module NextDayInWeek (
    input [2:0]         today,
    output reg [2:0]    tomorrow
);
always @(*) begin
    if (today == 3'd7)
        tomorrow = 3'd1;
    else
        tomorrow = today + 3'd1;
end
endmodule

计算日期则相对复杂一些。我们暂时先不考虑闰年的影响,每个月可能有 28、30 或 31 天,此时就需要使用分支语句进行描述

 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
28
29
30
31
32
33
module NextDayInMonth (
    input [4:0]         today,
    input [3:0]         month,
    output reg [4:0]    tomorrow,
    output reg [3:0]    next_month
);
reg [4:0] DaysOfMonth; // 本月有多少天
always @(*) begin
    case (month)
        2: DaysOfMonth= 5'd28;
        4, 6, 9, 11: DaysOfMonth = 5'd30;
        default: DaysOfMonth = 5'd31;
    endcase
end

always @(*) begin
    if (today == DaysOfMonth) 
        tomorrow = 5'd1; 
    else
        tomorrow = today + 5'd1;
end

always @(*) begin
    if (today == DaysOfMonth) begin
        if (month == 4'd12)
            next_month = 4'd1;
        else
            next_month = month + 4'd1;
    end
    else
        next_month = month;
end
endmodule

最后,将两个模块组合起来,即可得到最终的模块。

Tomorrow
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
module Tomorrow (
    input [3:0]         todayMonth,         // 今天的月份,范围 1~12
    input [4:0]         todayDayInMonth,    // 今天的日期,范围 1~31
    input [2:0]         todayDayInWeek,     // 今天星期几,范围 1~7
    output [3:0]        tomorrowMonth,
    output [4:0]        tomorrowDayInMonth,
    output [2:0]        tomorrowDayInWeek
);

NextDayInWeek nday (
    .today(todayDayInWeek),
    .tomorrow(tomorrowDayInWeek)
);

NextDayInMonth nmonth (
    .today(todayDayInMonth),
    .month(todayMonth),
    .tomorrow(tomorrowDayInMonth),
    .next_month(tomorrowMonth)
);
endmodule

2.2.2 6bits 5 的倍数检测器

如题,我们需要设计一个电路,检测输入的 6bits 二进制数是否为 5 的倍数。检验一个数是不是5的倍数,我们可以将其转换为十进制数,然后判断是否能被 5 整除。但是还有一个最为朴素的做法:枚举!

2.2.2.1 暴力枚举

6bit 二进制数的取值范围为 0~63,我们可以枚举所有的情况,然后判断是否为 5 的倍数。我们可以使用 Verilog 的 case 语句来实现这一功能。

BruteForce
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module BruteForce (
    input           [5:0]    x,             // 输入的 6bits 二进制数
    output   reg             isMultipleOf5
);
always@(*) begin
    case(x)
        000000: isMultipleOf5 = 1'b1;
        000101: isMultipleOf5 = 1'b1;
        001010: isMultipleOf5 = 1'b1;
        001111: isMultipleOf5 = 1'b1;
        010100: isMultipleOf5 = 1'b1;
        011001: isMultipleOf5 = 1'b1;
        011110: isMultipleOf5 = 1'b1;
        100011: isMultipleOf5 = 1'b1;
        101000: isMultipleOf5 = 1'b1;
        101101: isMultipleOf5 = 1'b1;
        110010: isMultipleOf5 = 1'b1;
        110111: isMultipleOf5 = 1'b1;
        111100: isMultipleOf5 = 1'b1;
        default: isMultipleOf5 = 1'b0;
    endcase
end

endmodule

显然,上述方法在面对更大的数时就不太适用了,毕竟我们不太可能枚举所有的情况。那么我们该如何设计一个通用的电路呢?

一个自然的想法是构建一个有限状态机,通过状态机的状态来记录当前的余数,并通过结束时落到的状态确定最终余数。由于一个数除以 5 的余数只可能是 0~4 ,因此我们可以设计一个 3bits 的状态机,用于记录当前的余数。

很可惜,这并不属于组合逻辑的范畴,因为状态机需要记录当前的状态,而状态的改变是需要时间的。因此我们需要引入时钟信号,将其变成时序逻辑——这也是我们将在 Lab5 中介绍的内容。

而现在,我们只能另辟蹊径,寻找另一种自然的方法。这就是我们接下来要介绍的长除法。

2.2.2.2 长除法

由于位数已经固定为 6bits,因此我们可以直接使用 Verilog 的除法运算符。但是这样的做法并不符合我们的初衷,我们希望能够自己设计一个电路,而不是直接使用现成的运算符。

我们以 6bits 二进制数除以 5 为例,假设我们要计算 011011 除以 101,我们可以模拟除法竖式的过程。

竖式除法

011011,取前三位的 011,余 11,向下一位借位,得到 110,再余 01,向下一位借位,得到 011,再余11,向下一位借一位,得到111,取101,余10,结束。

LongDivision
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
module LongDivision(
    input           [5:0]       num, 
    output reg                  isMultipleOf5
);
    reg             [2:0]       lend_1;
    reg             [2:0]       lend_2;
    reg             [2:0]       lend_3;
    reg             [2:0]       lend_4;

    always@(*) begin
        lend_1 = num[5:3] >= 3'b101 ? num[5:3] - 3'b101 : num[5:3];
        lend_2 = {lend_1, num[2]} > 3'b101 ? {lend_1, num[2]} - 3'b101 : {lend_1[1:0], num[2]};
        lend_3 = {lend_2, num[1]} > 3'b101 ? {lend_2, num[1]} - 3'b101 : {lend_2[1:0], num[1]};
        lend_4 = {lend_3, num[0]} > 3'b101 ? {lend_3, num[0]} - 3'b101 : {lend_3[1:0], num[0]};
        if (lend_4 == 3'b0)
            isMultipleOf5 = 1'b1;
        else
            isMultipleOf5 = 1'b0;
    end
endmodule

最后更新: November 21, 2023

评论