返回 FPGA 与 Verilog 学习目录
第二阶段:组合逻辑讲义
对应课程目录第 4~6 章。建议学习时间:4~6 天。
本阶段学习目标
完成本阶段后,你应该能够:
- 判断一段逻辑是否属于组合逻辑
- 使用
assign 和条件运算符描述组合电路
- 使用
always @(*)、if-else 和 case 描述组合电路
- 正确选择阻塞赋值
=,并说明它与非阻塞赋值 <= 的使用场景
- 解释锁存器产生的原因,并通过完整赋值避免锁存器
- 独立实现多路选择器、编码器、译码器和比较器
- 理解半加器、全加器以及多位加减法器的连接关系
- 编写自检式 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 驱动。a 或 b 变化后,表达式会重新求值。
assign 不是从上到下执行一次的软件语句。下面三个逻辑关系会同时存在:
1
2
3
|
assign and_y = a & b;
assign or_y = a | b;
assign xor_y = a ^ b;
|
连续赋值的目标通常声明为 wire:
同一根普通 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;
|
含义如下:
综合后,这段代码通常对应一个二选一多路选择器。条件运算符两侧的数据可以是多位信号,但位宽最好保持一致。
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 波形分析
观察波形时,重点检查:
select = 0 时,y 是否始终跟随 b
select = 1 时,y 是否始终跟随 a
- 保持
select 不变并修改被选中的输入时,y 是否随之变化
- 修改未被选中的输入时,
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 本章练习
- 实现一个 8 位二选一多路选择器。
- 输入两个 4 位数,输出较大的数;两个数相等时输出任意一个。
- 输入
enable 和 8 位 data,当 enable = 1 时输出 data,否则输出 0。
- 使用两个二选一多路选择器组成一个四选一多路选择器。
- 编写自检式 Testbench,验证练习 4 的全部选择情况。
4.11 本章检查点
- 如何判断一个电路是否属于组合逻辑?
assign 的目标信号通常使用什么类型?
select ? a : b 在 select = 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,使输出在仿真出现 x 或 z 时也有明确赋值,并降低修改代码时漏分支的风险。
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-else 与 case 的选择
适合使用 if-else 的情况:
- 条件有明显优先级
- 条件是范围判断,如
a > b
- 条件由多个逻辑表达式组成
适合使用 case 的情况:
- 根据一个操作码选择功能
- 各选择值地位相同
- 状态或指令译码
优先级编码器中的多个输入可能同时有效,因此使用按优先级排列的 if-else 会比普通 case 更直接。
5.11 本章易错点
- 使用
always @(a),却在块内读取了 b,导致敏感列表不完整
- 在组合逻辑过程块中习惯性使用非阻塞赋值
<=
if 没有对应的 else
case 没有覆盖全部情况,也没有默认赋值
- 只检查一个输出是否完整赋值,忽略同一过程块中的其他输出
- 误以为所有
reg 都会综合成寄存器
- 同一个
reg 在多个 always 块中被赋值
5.12 本章练习
- 使用
always @(*) 和 if-else 重写第 4 章的 8 位二选一多路选择器。
- 使用
case 实现一个 8 位四选一多路选择器。
- 找出下列代码产生锁存器的原因,并改正:
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
|
- 实现一个组合逻辑 ALU,并在每个
case 分支中同时产生 y 和 zero。
- 在 Testbench 中令选择信号为
2'bx1,比较带 default 和不带 default 的仿真现象。该测试只用于理解四值逻辑。
5.13 本章检查点
- 为什么组合逻辑推荐使用
always @(*)?
- 为什么过程块中的输出要声明成
reg?
reg 是否一定对应物理寄存器?
- 组合逻辑中不完整赋值为什么会产生锁存器?
- 设置默认值如何减少漏赋值?
if-else 与 case 分别适合什么类型的选择逻辑?
第 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 路输入通常需要至少以下位数的选择信号:
例如:
| 输入路数 |
最少选择位数 |
| 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
|
显式把 a 和 b 扩展到 5 位,使表达式的目标位宽清晰。carry_out 表示无符号加法超出了低 4 位。
6.9 二进制减法与补码
固定宽度二进制电路通常使用补码完成减法:
因此,加法和减法可以共享同一个加法器:
- 加法时使用
b
- 减法时使用
~b
- 减法时最低位进位输入设为 1
使用复制运算可以根据控制信号选择是否反转 b:
当 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 的作用有两个:
- 控制
b 是否按位取反
- 作为加法器最低位的加一输入
注意:减法时的 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
如果把 result 解释为 4 位有符号补码,4'b1011 表示 -5;如果解释为无符号数,则表示 11。同一组比特的数值含义由接口的符号约定决定。
6.13 本章易错点
- 普通编码器输入多位同时有效,却没有定义非法输入处理
- 优先编码器没有明确最高优先级是哪一位
- 译码器在禁用时没有给输出赋值,导致锁存器
- 无符号比较与有符号比较混用
- 多位加法只保留原位宽,丢失最高位进位
- 把补码减法的
carry_out 直接当作借位
- 看到
result = 4'b1011 就断定它一定是 11 或一定是 -5
- Testbench 只测试普通值,没有覆盖进位、借位和结果为零的边界情况
6.14 本章练习
- 编写 8 线到 3 线优先编码器,规定
in[7] 优先级最高,同时输出 valid。
- 编写 3 线到 8 线译码器,增加
enable 输入。
- 编写 8 位比较器,同时支持无符号比较结果
unsigned_less 和有符号比较结果 signed_less。
- 只使用两个半加器和一个或门组成一个全加器。
- 把
add_sub4 扩展成 8 位加减法器。
- 为 8 位加减法器编写自检式 Testbench,至少覆盖:普通加法、加法进位、普通减法、减法借位、结果为零。
- 给加减法器增加有符号溢出标志
overflow。思考它与 carry_out 的区别。
6.15 本章检查点
- 普通编码器与优先编码器有什么区别?
- 为什么译码器通常需要为无效或禁用情况规定输出?
- 4 位无符号加法为什么需要 5 位保存完整结果?
- 半加器为什么不能直接处理低位传来的进位?
a - b 如何转换成补码加法?
carry_out 与有符号溢出是否相同?
第二阶段综合练习
练习 A:带有效信号的 4 路数据选择器
设计一个 8 位四选一多路选择器:
- 输入为
d0、d1、d2、d3
- 使用
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-else 和 case 描述组合逻辑
- 能解释组合逻辑为什么通常使用阻塞赋值
=
- 能检查每个输出在所有分支中是否完整赋值
- 能定位并修复由不完整赋值产生的锁存器
- 能独立实现编码器、译码器、比较器和全加器
- 能解释多位加法的进位以及补码减法的基本原理
- 能完成 4 位加减法器和 8 位组合 ALU 中至少一个综合练习
- 能编写自检式 Testbench,并覆盖普通情况与边界情况