📝

第二阶段:组合逻辑

掌握连续赋值、组合逻辑过程块、选择结构、锁存器规避以及常见组合电路的设计与验证。

返回 FPGA 与 Verilog 学习目录

第二阶段:组合逻辑讲义

对应课程目录第 4~6 章。建议学习时间:4~6 天。

本阶段学习目标

完成本阶段后,你应该能够:

  • 判断一段逻辑是否属于组合逻辑
  • 使用 assign 和条件运算符描述组合电路
  • 使用 always @(*)if-elsecase 描述组合电路
  • 正确选择阻塞赋值 =,并说明它与非阻塞赋值 <= 的使用场景
  • 解释锁存器产生的原因,并通过完整赋值避免锁存器
  • 独立实现多路选择器、编码器、译码器和比较器
  • 理解半加器、全加器以及多位加减法器的连接关系
  • 编写自检式 Testbench,覆盖组合电路的主要输入情况

第 4 章:连续赋值与组合逻辑

4.1 什么是组合逻辑

组合逻辑的输出只取决于当前输入,不保存过去的状态:

1
y = f(a, b, select, ...)

组合逻辑通常具有以下特征:

  • 没有时钟输入
  • 不需要记住上一次的结果
  • 输入变化后,输出经过传播延时随之变化
  • 同一组输入总是对应同一组输出

常见组合电路包括逻辑门、多路选择器、编码器、译码器、比较器和加法器。

组合逻辑在真实电路中仍然存在传播延时。Verilog 的功能级 RTL 代码通常不写门延时,但综合后的门级电路以及 FPGA 实际硬件都不会瞬间完成传播。

4.2 assign 连续赋值

assign 用于持续驱动网络类型信号:

1
2
wire y;
assign y = a & b;

它表示 y 始终由表达式 a & b 驱动。ab 变化后,表达式会重新求值。

assign 不是从上到下执行一次的软件语句。下面三个逻辑关系会同时存在:

1
2
3
assign and_y = a & b;
assign or_y  = a | b;
assign xor_y = a ^ b;

连续赋值的目标通常声明为 wire

1
output wire y;

同一根普通 wire 不应同时由多个 assign 或多个模块输出驱动,否则可能产生驱动冲突。

4.3 多位信号操作

assign 可以直接描述多位总线之间的组合关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module bus_operations (
    input  wire [7:0] data,
    output wire [3:0] high,
    output wire [3:0] low,
    output wire [7:0] reversed_nibbles,
    output wire       any_one
);

assign high             = data[7:4];
assign low              = data[3:0];
assign reversed_nibbles = {data[3:0], data[7:4]};
assign any_one          = |data;

endmodule

这里使用了:

  • 范围选择:data[7:4]
  • 拼接:{data[3:0], data[7:4]}
  • 归约或:|data

每个输出都只取决于当前的 data,因此它们都是组合逻辑。

4.4 条件运算符 ?:

条件运算符的格式是:

1
条件 ? 条件成立时的值 : 条件不成立时的值

例如:

1
assign y = select ? a : b;

含义如下:

select y
0 b
1 a

综合后,这段代码通常对应一个二选一多路选择器。条件运算符两侧的数据可以是多位信号,但位宽最好保持一致。

4.5 实践:参数化二选一多路选择器

新建 mux2.v

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
module mux2 #(
    parameter WIDTH = 4
) (
    input  wire [WIDTH-1:0] a,
    input  wire [WIDTH-1:0] b,
    input  wire             select,
    output wire [WIDTH-1:0] y
);

assign y = select ? a : b;

endmodule

WIDTH 默认值为 4。例化时可以保持 4 位,也可以覆盖参数形成其他位宽的多路选择器。参数化设计将在第 11 章进一步学习。

4.6 自检式 Testbench

新建 mux2_tb.v

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
`timescale 1ns/1ps

module mux2_tb;

reg  [3:0] a;
reg  [3:0] b;
reg        select;
wire [3:0] y;

integer errors;

mux2 #(
    .WIDTH(4)
) dut (
    .a(a),
    .b(b),
    .select(select),
    .y(y)
);

initial begin
    errors = 0;

    a = 4'h3; b = 4'hC; select = 1'b0; #10;
    if (y !== 4'hC) begin
        $display("ERROR: select=0, expected C, got %h", y);
        errors = errors + 1;
    end

    select = 1'b1; #10;
    if (y !== 4'h3) begin
        $display("ERROR: select=1, expected 3, got %h", y);
        errors = errors + 1;
    end

    a = 4'hA; b = 4'h5; select = 1'b0; #10;
    if (y !== 4'h5) begin
        $display("ERROR: select=0, expected 5, got %h", y);
        errors = errors + 1;
    end

    select = 1'b1; #10;
    if (y !== 4'hA) begin
        $display("ERROR: select=1, expected A, got %h", y);
        errors = errors + 1;
    end

    if (errors == 0)
        $display("PASS: all mux2 test cases passed");
    else
        $display("FAIL: %0d mux2 test case(s) failed", errors);

    $finish;
end

endmodule

4.7 波形分析

观察波形时,重点检查:

  1. select = 0 时,y 是否始终跟随 b
  2. select = 1 时,y 是否始终跟随 a
  3. 保持 select 不变并修改被选中的输入时,y 是否随之变化
  4. 修改未被选中的输入时,y 是否保持当前值

第四点中的“保持”不是电路记住了结果,而是当前逻辑表达式仍然选择另一输入。

4.8 组合逻辑中的传播关系

考虑两级组合逻辑:

1
2
3
4
wire middle;

assign middle = a & b;
assign y      = middle | c;

它等价于:

1
assign y = (a & b) | c;

真实电路中,输入变化会先经过与门,再经过或门。组合路径越长,传播所需时间通常越长。后续学习时序分析时,会把寄存器之间最长的组合路径作为关键关注对象。

4.9 本章易错点

  • assign 当成只执行一次的顺序语句
  • assign 的输出声明成在过程块中使用的变量类型
  • 对同一个信号进行多处驱动
  • 忽略多位信号的位宽,导致截断或补位
  • 把条件运算符的两个结果写反
  • 认为组合逻辑完全没有物理传播延时

4.10 本章练习

  1. 实现一个 8 位二选一多路选择器。
  2. 输入两个 4 位数,输出较大的数;两个数相等时输出任意一个。
  3. 输入 enable 和 8 位 data,当 enable = 1 时输出 data,否则输出 0。
  4. 使用两个二选一多路选择器组成一个四选一多路选择器。
  5. 编写自检式 Testbench,验证练习 4 的全部选择情况。

4.11 本章检查点

  • 如何判断一个电路是否属于组合逻辑?
  • assign 的目标信号通常使用什么类型?
  • select ? a : bselect = 0 时选择谁?
  • 修改未被选中的多路选择器输入,为什么不会改变当前输出?
  • 两级组合逻辑与一级组合逻辑在物理传播路径上有什么区别?

第 5 章:always 组合逻辑

5.1 为什么需要过程块

简单表达式适合使用 assign。当选择分支较多或判断条件较复杂时,使用 always 过程块通常更清晰。

组合逻辑过程块的基本形式是:

1
2
3
always @(*) begin
    // 组合逻辑赋值
end

@(*) 表示:只要过程块中读取的任意输入信号发生变化,就重新计算该过程块。

旧式写法需要手工列出敏感信号:

1
always @(a or b or select)

如果漏写某个信号,仿真行为可能与综合后的硬件不一致。因此描述组合逻辑时应优先使用 always @(*)

5.2 过程块输出使用 reg

在 Verilog 中,过程块内被赋值的信号需要声明为 reg

1
2
3
4
5
output reg y;

always @(*) begin
    y = a & b;
end

这段代码仍然是组合逻辑,不会因为 y 声明成 reg 就自动生成寄存器。

是否保存状态取决于电路描述是否在所有情况下都给输出赋值,以及过程块是否由时钟边沿触发。

5.3 阻塞赋值 =

组合逻辑过程块通常使用阻塞赋值:

1
2
3
4
always @(*) begin
    middle = a & b;
    y = middle | c;
end

阻塞赋值会让过程块中的下一条语句看到当前语句更新后的值。这适合表达组合计算顺序。

现阶段遵守以下规则:

  • 组合逻辑 always @(*) 使用 =
  • 时钟触发的时序逻辑使用 <=

不要仅凭 =<= 判断电路类型,还要观察过程块的触发条件和赋值完整性。

5.4 使用 if-else

二选一多路选择器可以写成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module mux2_always (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       select,
    output reg  [3:0] y
);

always @(*) begin
    if (select)
        y = a;
    else
        y = b;
end

endmodule

它与下面的连续赋值在功能上等价:

1
assign y = select ? a : b;

简单逻辑使用哪种写法取决于清晰度;复杂分支通常适合使用 always @(*)

5.5 使用 case

当一个选择信号对应多个离散分支时,case 通常比连续的 if-else 更容易阅读:

1
2
3
4
5
6
7
8
9
always @(*) begin
    case (select)
        2'b00: y = d0;
        2'b01: y = d1;
        2'b10: y = d2;
        2'b11: y = d3;
        default: y = 4'b0000;
    endcase
end

虽然 2 位 select 的四种 0/1 组合已经全部列出,仍建议保留 default,使输出在仿真出现 xz 时也有明确赋值,并降低修改代码时漏分支的风险。

5.6 实践:四选一多路选择器

新建 mux4.v

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
module mux4 (
    input  wire [3:0] d0,
    input  wire [3:0] d1,
    input  wire [3:0] d2,
    input  wire [3:0] d3,
    input  wire [1:0] select,
    output reg  [3:0] y
);

always @(*) begin
    case (select)
        2'b00: y = d0;
        2'b01: y = d1;
        2'b10: y = d2;
        2'b11: y = d3;
        default: y = 4'b0000;
    endcase
end

endmodule

5.7 四选一多路选择器 Testbench

新建 mux4_tb.v

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
`timescale 1ns/1ps

module mux4_tb;

reg  [3:0] d0;
reg  [3:0] d1;
reg  [3:0] d2;
reg  [3:0] d3;
reg  [1:0] select;
wire [3:0] y;

integer errors;

mux4 dut (
    .d0(d0),
    .d1(d1),
    .d2(d2),
    .d3(d3),
    .select(select),
    .y(y)
);

initial begin
    errors = 0;
    d0 = 4'h1;
    d1 = 4'h3;
    d2 = 4'h7;
    d3 = 4'hF;

    select = 2'b00; #10;
    if (y !== d0) errors = errors + 1;

    select = 2'b01; #10;
    if (y !== d1) errors = errors + 1;

    select = 2'b10; #10;
    if (y !== d2) errors = errors + 1;

    select = 2'b11; #10;
    if (y !== d3) errors = errors + 1;

    d3 = 4'hA; #10;
    if (y !== 4'hA) errors = errors + 1;

    if (errors == 0)
        $display("PASS: all mux4 test cases passed");
    else
        $display("FAIL: %0d mux4 test case(s) failed", errors);

    $finish;
end

endmodule

最后一个测试保持 select = 2'b11,只修改 d3,用于确认输出会持续跟随当前被选中的输入。

5.8 锁存器是怎样产生的

观察下面的错误代码:

1
2
3
4
always @(*) begin
    if (enable)
        y = data;
end

enable = 1 时,y 得到 data。但当 enable = 0 时,代码没有说明 y 应该是多少。为了让 y 保留上一次的值,综合工具只能推断出一个锁存器。

这就不再是纯组合逻辑,因为输出与过去的值有关。

方法一:补全 else

1
2
3
4
5
6
always @(*) begin
    if (enable)
        y = data;
    else
        y = 8'b0000_0000;
end

方法二:先设置默认值

1
2
3
4
5
6
always @(*) begin
    y = 8'b0000_0000;

    if (enable)
        y = data;
end

第二种写法在输出很多、分支复杂时尤其有用:先为所有输出设置默认值,再在特定条件下覆盖。

5.9 多个输出的完整赋值

下面的写法仍可能产生锁存器:

1
2
3
4
5
6
7
8
always @(*) begin
    if (select) begin
        y     = a;
        valid = 1'b1;
    end else begin
        y = b;
    end
end

select = 0 的分支中,valid 没有赋值。即使 y 的赋值完整,valid 仍会被推断为锁存器。

正确写法:

1
2
3
4
5
6
7
8
9
always @(*) begin
    y     = b;
    valid = 1'b0;

    if (select) begin
        y     = a;
        valid = 1'b1;
    end
end

检查组合过程块时,应逐个检查每一个输出是否在所有路径上都有确定值。

5.10 if-elsecase 的选择

适合使用 if-else 的情况:

  • 条件有明显优先级
  • 条件是范围判断,如 a > b
  • 条件由多个逻辑表达式组成

适合使用 case 的情况:

  • 根据一个操作码选择功能
  • 各选择值地位相同
  • 状态或指令译码

优先级编码器中的多个输入可能同时有效,因此使用按优先级排列的 if-else 会比普通 case 更直接。

5.11 本章易错点

  • 使用 always @(a),却在块内读取了 b,导致敏感列表不完整
  • 在组合逻辑过程块中习惯性使用非阻塞赋值 <=
  • if 没有对应的 else
  • case 没有覆盖全部情况,也没有默认赋值
  • 只检查一个输出是否完整赋值,忽略同一过程块中的其他输出
  • 误以为所有 reg 都会综合成寄存器
  • 同一个 reg 在多个 always 块中被赋值

5.12 本章练习

  1. 使用 always @(*)if-else 重写第 4 章的 8 位二选一多路选择器。
  2. 使用 case 实现一个 8 位四选一多路选择器。
  3. 找出下列代码产生锁存器的原因,并改正:
1
2
3
4
5
6
7
always @(*) begin
    case (op)
        2'b00: y = a + b;
        2'b01: y = a - b;
        2'b10: y = a & b;
    endcase
end
  1. 实现一个组合逻辑 ALU,并在每个 case 分支中同时产生 yzero
  2. 在 Testbench 中令选择信号为 2'bx1,比较带 default 和不带 default 的仿真现象。该测试只用于理解四值逻辑。

5.13 本章检查点

  • 为什么组合逻辑推荐使用 always @(*)
  • 为什么过程块中的输出要声明成 reg
  • reg 是否一定对应物理寄存器?
  • 组合逻辑中不完整赋值为什么会产生锁存器?
  • 设置默认值如何减少漏赋值?
  • if-elsecase 分别适合什么类型的选择逻辑?

第 6 章:常用组合电路

6.1 编码器

编码器把多路输入转换成较少位数的编码。例如,4 线到 2 线编码器假设任意时刻只有一个输入为 1:

in code valid
0001 00 1
0010 01 1
0100 10 1
1000 11 1
其他 00 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module encoder4to2 (
    input  wire [3:0] in,
    output reg  [1:0] code,
    output reg        valid
);

always @(*) begin
    code  = 2'b00;
    valid = 1'b1;

    case (in)
        4'b0001: code = 2'b00;
        4'b0010: code = 2'b01;
        4'b0100: code = 2'b10;
        4'b1000: code = 2'b11;
        default: begin
            code  = 2'b00;
            valid = 1'b0;
        end
    endcase
end

endmodule

普通编码器要求输入满足独热码约束,即只有一位为 1。如果多位同时为 1,应把它判为非法,或者明确规定优先级。

6.2 优先编码器

优先编码器允许多个输入同时为 1,并选择优先级最高的一位。下面规定 in[3] 优先级最高:

 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
module priority_encoder4to2 (
    input  wire [3:0] in,
    output reg  [1:0] code,
    output reg        valid
);

always @(*) begin
    code  = 2'b00;
    valid = 1'b1;

    if (in[3])
        code = 2'b11;
    else if (in[2])
        code = 2'b10;
    else if (in[1])
        code = 2'b01;
    else if (in[0])
        code = 2'b00;
    else begin
        code  = 2'b00;
        valid = 1'b0;
    end
end

endmodule

in = 4'b1010 时,in[3]in[1] 都为 1,但输出为 2'b11,因为 in[3] 优先级更高。

6.3 译码器

译码器执行与编码器相反的映射。2 线到 4 线译码器根据输入编号,只让对应的一位输出为 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
module decoder2to4 (
    input  wire [1:0] code,
    input  wire       enable,
    output reg  [3:0] out
);

always @(*) begin
    out = 4'b0000;

    if (enable) begin
        case (code)
            2'b00: out = 4'b0001;
            2'b01: out = 4'b0010;
            2'b10: out = 4'b0100;
            2'b11: out = 4'b1000;
            default: out = 4'b0000;
        endcase
    end
end

endmodule

enable = 0 时,输出全为 0。先给 out 默认值,可以保证 if 不成立时仍有明确输出。

对于简单译码器,也可以使用移位描述:

1
assign out = enable ? (4'b0001 << code) : 4'b0000;

6.4 多路选择器

多路选择器从多组输入中选择一组送到输出。N 路输入通常需要至少以下位数的选择信号:

1
选择位数 = 向上取整(log2(N))

例如:

输入路数 最少选择位数
2 1
4 2
8 3
16 4

如果输入路数不是 2 的幂,选择信号会存在无效编码,必须明确这些编码对应的输出。

6.5 比较器

一个无符号 8 位比较器可以同时给出大于、等于和小于标志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module comparator8 (
    input  wire [7:0] a,
    input  wire [7:0] b,
    output wire       greater,
    output wire       equal,
    output wire       less
);

assign greater = (a > b);
assign equal   = (a == b);
assign less    = (a < b);

endmodule

对于确定的 0/1 输入,三个标志中应当恰好有一个为 1。

有符号比较需要参与比较的信号都具有正确的 signed 属性:

1
2
input wire signed [7:0] a;
input wire signed [7:0] b;

例如,8'b1111_1111 作为无符号数是 255,作为 8 位有符号补码则是 -1。符号属性不同会直接改变比较结果。

6.6 半加器

半加器把两个 1 位数相加,产生和 sum 与进位 carry

a b sum carry
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module half_adder (
    input  wire a,
    input  wire b,
    output wire sum,
    output wire carry
);

assign sum   = a ^ b;
assign carry = a & b;

endmodule

6.7 全加器

全加器在半加器基础上增加低位传来的进位 carry_in

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
module full_adder (
    input  wire a,
    input  wire b,
    input  wire carry_in,
    output wire sum,
    output wire carry_out
);

assign sum       = a ^ b ^ carry_in;
assign carry_out = (a & b) | (a & carry_in) | (b & carry_in);

endmodule

也可以先形成 2 位完整结果:

1
assign {carry_out, sum} = a + b + carry_in;

拼接左侧是 2 位,因此能够同时保留和与进位。

6.8 多位加法器

4 位无符号加法需要 5 位保存完整结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module adder4 (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       carry_in,
    output wire [3:0] sum,
    output wire       carry_out
);

assign {carry_out, sum} = {1'b0, a} + {1'b0, b} + carry_in;

endmodule

显式把 ab 扩展到 5 位,使表达式的目标位宽清晰。carry_out 表示无符号加法超出了低 4 位。

6.9 二进制减法与补码

固定宽度二进制电路通常使用补码完成减法:

1
a - b = a + (~b) + 1

因此,加法和减法可以共享同一个加法器:

  • 加法时使用 b
  • 减法时使用 ~b
  • 减法时最低位进位输入设为 1

使用复制运算可以根据控制信号选择是否反转 b

1
b ^ {4{subtract}}

subtract = 0 时,b 不变;当 subtract = 1 时,b 的每一位都被反转。

6.10 实践:4 位加减法器

新建 add_sub4.v

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
module add_sub4 (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       subtract,
    output wire [3:0] result,
    output wire       carry_out,
    output wire       zero
);

wire [3:0] b_selected;
wire [4:0] full_result;

assign b_selected = b ^ {4{subtract}};
assign full_result = {1'b0, a} + {1'b0, b_selected} + subtract;

assign result    = full_result[3:0];
assign carry_out = full_result[4];
assign zero      = (result == 4'b0000);

endmodule

subtract 的作用有两个:

  1. 控制 b 是否按位取反
  2. 作为加法器最低位的加一输入

注意:减法时的 carry_out 不能简单地直接称为“借位”。在这种补码实现中,通常 carry_out = 1 表示无借位,carry_out = 0 表示发生借位。实际工程应根据接口需求明确命名和定义。

6.11 加减法器 Testbench

新建 add_sub4_tb.v

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
`timescale 1ns/1ps

module add_sub4_tb;

reg  [3:0] a;
reg  [3:0] b;
reg        subtract;
wire [3:0] result;
wire       carry_out;
wire       zero;

integer errors;

add_sub4 dut (
    .a(a),
    .b(b),
    .subtract(subtract),
    .result(result),
    .carry_out(carry_out),
    .zero(zero)
);

task check;
    input [3:0] test_a;
    input [3:0] test_b;
    input       test_subtract;
    input [4:0] expected_full;
    begin
        a        = test_a;
        b        = test_b;
        subtract = test_subtract;
        #10;

        if ({carry_out, result} !== expected_full) begin
            $display(
                "ERROR: a=%h b=%h sub=%b expected=%b_%h got=%b_%h",
                a, b, subtract,
                expected_full[4], expected_full[3:0],
                carry_out, result
            );
            errors = errors + 1;
        end

        if (zero !== (expected_full[3:0] == 4'b0000)) begin
            $display("ERROR: incorrect zero flag");
            errors = errors + 1;
        end
    end
endtask

initial begin
    errors = 0;

    check(4'd3,  4'd5,  1'b0, 5'b0_1000); // 3 + 5 = 8
    check(4'd15, 4'd1,  1'b0, 5'b1_0000); // 15 + 1 = 16
    check(4'd9,  4'd4,  1'b1, 5'b1_0101); // 9 - 4 = 5, no borrow
    check(4'd4,  4'd9,  1'b1, 5'b0_1011); // 4 - 9 = -5, low 4 bits B
    check(4'd7,  4'd7,  1'b1, 5'b1_0000); // 7 - 7 = 0

    if (errors == 0)
        $display("PASS: all add_sub4 test cases passed");
    else
        $display("FAIL: %0d add_sub4 error(s)", errors);

    $finish;
end

endmodule

task 可以把重复的测试步骤封装起来。这里的任务只用于 Testbench,不会综合到功能电路中。

6.12 加减法器波形分析

重点观察以下情况:

15 + 1

1
2
3
4
完整结果 = 5'b1_0000
result    = 4'b0000
carry_out = 1
zero      = 1

zero 只判断 4 位 result,所以即使完整结果为 16,低 4 位仍然为 0。

9 - 4

1
2
3
9 + (~4) + 1 = 5'b1_0101
result        = 4'b0101
carry_out     = 1

4 - 9

1
result = 4'b1011

如果把 result 解释为 4 位有符号补码,4'b1011 表示 -5;如果解释为无符号数,则表示 11。同一组比特的数值含义由接口的符号约定决定。

6.13 本章易错点

  • 普通编码器输入多位同时有效,却没有定义非法输入处理
  • 优先编码器没有明确最高优先级是哪一位
  • 译码器在禁用时没有给输出赋值,导致锁存器
  • 无符号比较与有符号比较混用
  • 多位加法只保留原位宽,丢失最高位进位
  • 把补码减法的 carry_out 直接当作借位
  • 看到 result = 4'b1011 就断定它一定是 11 或一定是 -5
  • Testbench 只测试普通值,没有覆盖进位、借位和结果为零的边界情况

6.14 本章练习

  1. 编写 8 线到 3 线优先编码器,规定 in[7] 优先级最高,同时输出 valid
  2. 编写 3 线到 8 线译码器,增加 enable 输入。
  3. 编写 8 位比较器,同时支持无符号比较结果 unsigned_less 和有符号比较结果 signed_less
  4. 只使用两个半加器和一个或门组成一个全加器。
  5. add_sub4 扩展成 8 位加减法器。
  6. 为 8 位加减法器编写自检式 Testbench,至少覆盖:普通加法、加法进位、普通减法、减法借位、结果为零。
  7. 给加减法器增加有符号溢出标志 overflow。思考它与 carry_out 的区别。

6.15 本章检查点

  • 普通编码器与优先编码器有什么区别?
  • 为什么译码器通常需要为无效或禁用情况规定输出?
  • 4 位无符号加法为什么需要 5 位保存完整结果?
  • 半加器为什么不能直接处理低位传来的进位?
  • a - b 如何转换成补码加法?
  • carry_out 与有符号溢出是否相同?

第二阶段综合练习

练习 A:带有效信号的 4 路数据选择器

设计一个 8 位四选一多路选择器:

  • 输入为 d0d1d2d3
  • 使用 select[2:0] 选择数据
  • select 为 0~3 时输出对应数据,并令 valid = 1
  • select 为 4~7 时输出 0,并令 valid = 0
  • 使用 always @(*)case 实现

Testbench 必须遍历 select 的全部 8 种取值。

练习 B:十进制数字译码器

输入为 4 位二进制数 digit

  • 当输入为 0~9 时,输出对应的独热码 one_hot[9:0]
  • 输入为 10~15 时,输出全 0
  • 同时输出 valid

要求使用组合逻辑实现,并检查所有 16 种输入。

练习 C:8 位组合 ALU

设计一个 8 位 ALU:

op 功能
3'b000 a + b
3'b001 a - b
3'b010 a & b
3'b011 `a
3'b100 a ^ b
3'b101 a << 1
3'b110 a >> 1
3'b111 a < b,结果为 0 或 1

输出包括:

  • result[7:0]
  • zero
  • carry_out,仅加减法时有意义,其他操作时必须输出 0

要求:

  • 使用 always @(*)case
  • 在过程块开始处设置默认值
  • 不产生锁存器
  • 每种操作至少编写两个测试用例
  • 覆盖 8'hFF + 8'h01、两个相等数相减和比较边界

参考答案

第 4 章练习要点

使能输出:

1
assign y = enable ? data : 8'b0000_0000;

使用二选一多路选择器组成四选一多路选择器:

1
2
3
4
5
6
wire [7:0] lower_selected;
wire [7:0] upper_selected;

assign lower_selected = select[0] ? d1 : d0;
assign upper_selected = select[0] ? d3 : d2;
assign y = select[1] ? upper_selected : lower_selected;

第 5 章锁存器练习

原代码没有覆盖 op = 2'b11,也没有默认赋值。可以补全分支:

1
2
3
4
5
6
7
8
9
always @(*) begin
    case (op)
        2'b00: y = a + b;
        2'b01: y = a - b;
        2'b10: y = a & b;
        2'b11: y = a | b;
        default: y = 8'b0000_0000;
    endcase
end

也可以在 case 前设置默认值,再只覆盖需要的分支。

第 6 章练习要点

使用两个半加器组成全加器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
wire sum_first;
wire carry_first;
wire carry_second;

half_adder u_half_adder_0 (
    .a(a),
    .b(b),
    .sum(sum_first),
    .carry(carry_first)
);

half_adder u_half_adder_1 (
    .a(sum_first),
    .b(carry_in),
    .sum(sum),
    .carry(carry_second)
);

assign carry_out = carry_first | carry_second;

有符号加法溢出的基本判断:两个同号数相加,结果符号却与它们不同。

1
assign overflow = (~(a[7] ^ b[7])) & (result[7] ^ a[7]);

减法溢出的判断条件不同:两个操作数符号不同,并且结果符号与被减数不同。

1
assign overflow = (a[7] ^ b[7]) & (result[7] ^ a[7]);

第二阶段完成标准

满足以下条件后,再进入第三阶段:

  • 能独立使用 assign 实现二选一多路选择器和简单比较器
  • 能使用 always @(*)if-elsecase 描述组合逻辑
  • 能解释组合逻辑为什么通常使用阻塞赋值 =
  • 能检查每个输出在所有分支中是否完整赋值
  • 能定位并修复由不完整赋值产生的锁存器
  • 能独立实现编码器、译码器、比较器和全加器
  • 能解释多位加法的进位以及补码减法的基本原理
  • 能完成 4 位加减法器和 8 位组合 ALU 中至少一个综合练习
  • 能编写自检式 Testbench,并覆盖普通情况与边界情况
comments powered by Disqus