设计: 4 位算术逻辑单元(ALU)
本节目标:完成一个支持 4 种运算的 4 位 ALU
接口规格
信号 位宽 作用 a4 位 第一个操作数 b4 位 第二个操作数 op2 位 选择运算 y4 位 运算结果 运算编码
op运算 示例 00加法 3 + 2 = 5 01减法 5 – 3 = 2 10按位与 AND1010 & 1100 = 1000 11按位或 OR1010 | 1100 = 1110
认识 4 位 ALU
4 位 ALU(Arithmetic Logic Unit,算术逻辑单元)是 CPU 中负责执行算术运算和逻辑运算的核心部件之一。
之前实现过 4 位 2:1 MUX,它根据 sel 从 a、b 中选一个输出。ALU 可以把这个思想扩展到多个运算结果:每种运算都产生一个候选结果,操作码决定最终输出哪一个。
目前只完成一个 4 位 ALU 的设计,支持加法、减法、按位与、按位或四种运算。但是需要知道,完整 ALU 的设计还会涉及更多的运算、进位、溢出等问题,这些在后续会继续完善。
使用 SystemVerilog 设计简易 4 位 ALU
新建一个 src/alu.sv 文件。
先自己写出模块的接口声明,参考上一节的 mux2,但需要注意:
a、b是 4 位输入。op是 2 位输入。y是 4 位输出。- 这是组合逻辑,不需要
clk。
暂时不要写运算逻辑。
module alu(
input logic [3:0] a,
input logic [3:0] b,
input logic [1:0] op,
output logic [3:0] y
);
endmodule
定义好了接口后,使用 always_comb 和 case。
之前的 MUX 用 assign 描述连续赋值。现在我们使用 always_comb,它表示这段逻辑是组合逻辑:输入变化时,仿真器会重新计算输出,不需要时钟。
在 alu.sv 的模块内部、endmodule 之前,先添加这个框架:
always_comb begin
case (op)
2'b00: ; // 加法
2'b01: ; // 减法
2'b10: ; // 按位与
2'b11: ; // 按位或
default: ;
endcase
end
这里的 ; 是临时占位,下一步需要替换成赋值语句。
现在先写加法逻辑。不过赋值有 y = a + b; 和 y <= a + b; 两种写法,前者是阻塞赋值,后者是非阻塞赋值。这里在 always_comb 中推荐使用阻塞赋值。
简单区分:
=:阻塞赋值,语句执行时立即更新变量,适合组合逻辑。<=:非阻塞赋值,通常用于always_ff描述的时序逻辑。
然后补齐减法逻辑、按位与逻辑、按位或逻辑。得到完整代码如下:
module alu (
input logic [3:0] a,
input logic [3:0] b,
input logic [1:0] op,
output logic [3:0] y
);
always_comb begin
case (op)
2'b00: y = a + b;
2'b01: y = a - b;
2'b10: y = a & b;
2'b11: y = a | b;
endcase
end
endmodule
然后开始创建测试文件 test/tb_alu.sv,编写测试用例。现在给出模板如下,补充 TODO 的部分。
module tb_alu;
// TODO: 声明 a、b、op、y
// TODO: 实例化 alu,连接 a、b、op、y
initial begin
// Test 1: 3 + 2 = 5
a = 4'd3;
b = 4'd2;
op = 2'b00;
#1;
if (y !== 4'd5)
$fatal(1, "ADD failed");
$display("ADD passed");
$finish;
end
endmodule
第一个 TODO 声明四个信号,按照上节方法声明即可:
logic [3:0] a;
logic [3:0] b;
logic [1:0] op;
logic [3:0] y;
第二个 TODO 实例化 alu,连接四个信号:
alu dut (
.a (a),
.b (b),
.op(op),
.y (y)
);
注意,4'd3 是 4 位的十进制数 3;!== 是严格不等比较;如果结果中出现 X 或 Z,也会判定为不相等。
现在开始运行测试,检查输出是否正确。
just test-sv alu tb_alu
加法测试成功了:
ADD passed
- tests/tb_alu.sv:25: Verilog $finish
- S i m u l a t i o n R e p o r t: Verilator 5.032 2025-01-01
- Verilator: $finish at 1ps; walltime 0.001 s; speed 861.778 ps/s
- Verilator: cpu 0.001 s on 1 threads; alloced 57 MB
加法通过后,我们再依次添加:
| 运算 | 输入 a | 输入 b | op | 预期 y |
|---|---|---|---|---|
| 减法 | 4'd5 |
4'd3 |
01 |
4'd2 |
| 按位与 | 1010 |
1100 |
10 |
1000 |
| 按位或 | 1010 |
1100 |
11 |
1110 |
最后大致得到这样的代码:
module tb_alu;
logic [3:0] a;
logic [3:0] b;
logic [1:0] op;
logic [3:0] y;
alu dut (
.a (a),
.b (b),
.op(op),
.y (y)
);
initial begin
// Test 1: 3 + 2 = 5
a = 4'd3;
b = 4'd2;
op = 2'b00;
#1;
if (y !== 4'd5) $fatal(1, "ADD failed");
// Test 2: 5 - 3 = 2
a = 4'd5;
b = 4'd3;
op = 2'b01;
#1;
if (y !== 4'd2) $fatal(1, "SUB failed");
// Test 3: 0b1010 & 0b1100 = 0b1000
a = 4'b1010;
b = 4'b1100;
op = 2'b10;
#1;
if (y !== 4'b1000) $fatal(1, "AND failed");
// Test 4: 0b1010 | 0b1100 = 0b1110
op = 2'b11;
#1;
if (y !== 4'b1110) $fatal(1, "OR failed");
$display("Passed");
$finish;
end
endmodule
测试通过。
下一步,测试 4 位减法的溢出表现
现在的输出 y 只有 4 位,因此:
2-5=-3
负数无法直接用 4 位无符号数表示。在 4 位二进制补码中,结果会按模 16 回绕:
2-5\equiv 13\pmod{16}
因此预期输出是 4'b1101。
现在,在 tb_alu.sv 中新增一个减法测试,设置 a = 4'd2、b = 4'd5,并检查 y 是否等于 4'b1101。
// Test 5: 2 - 5 = -3 === 13 (mod 16)
a = 4'd2;
b = 4'd5;
op = 2'b01;
#1;
if (y !== 4'b1101) $fatal(1, "SUB MOD failed");
标志位
这个 ALU 目前没有标志位输出。标志位通常用于表示运算结果的状态,例如:
- 零标志(Zero Flag):当结果为零时置位。
- 负标志(Negative Flag):当结果为负数时置位(结果最高位为 1)。
先实现以上两个标志位,规定:
运算结果 y |
zero |
negative |
|---|---|---|
0000 |
1 |
0 |
0011 |
0 |
0 |
1101 |
0 |
1 |
1111 |
0 |
1 |
zero 和 negative
这里先采用简单定义:negative 直接取 y[3]。它表示结果的最高位,不保证所有情况下都代表数学意义上的负数,尤其是无符号运算或发生溢出时。
现在开始完成以下任务:
- 在
alu.sv中添加zero和negative两个output logic。 - 想一想,应该把它们写在
always_comb的case内部,还是在case结束后统一计算? - 在测试台中增加检查:当
2 - 5得到1101时,zero应为0,negative应为1。
这样一来 alu.sv 应该变成以下代码:
module alu (
input logic [3:0] a,
input logic [3:0] b,
input logic [1:0] op,
output logic [3:0] y,
output logic zero,
output logic negative
);
always_comb begin
case (op)
2'b00: y = a + b;
2'b01: y = a - b;
2'b10: y = a & b;
2'b11: y = a | b;
endcase
if (y === 4'd0) zero = 1'b1;
else zero = 1'b0;
negative = y[3];
end
endmodule
测试里面也要增加 2 - 5 对 zero 和 negative 的检查:
if (y !== 4'b1101 || zero !== 1'b0 || negative !== 1'b1) $fatal(1, "SUB MOD failed");
测试应当通过。
然而,现在的 ALU 的 negative 标志位只是简单地取了 y[3],并没有考虑溢出情况。对于减法运算,溢出会导致结果的符号位不正确。先添加两个测试,一个测试 zero 标志位是否生效,另一个测试 negative 标志位是否过滤正常判断溢出(现在应当未正常判断)。
| 输入 | 运算 | 预期 y | zero | negative |
|---|---|---|---|---|
| 3、3 | 减法 | 0000 | 1 | 0 |
| 7、1 | 加法 | 1000 | 0 | 1 |
添加测试应当如下:
// Test 6: 3 - 3 = 0
a = 4'd3;
b = 4'd3;
op = 2'b01;
#1;
if (zero !== 1'b1) $fatal(1, "zero test failed");
// Test 7: 7 + 1 = 8 = 0b1000, negative !== 0
a = 4'd7;
b = 4'd1;
op = 2'b00;
#1;
if (negative === 1) $fatal(1, "negative test failed");
应当有以下测试错误:
[7] %Fatal: tb_alu.sv:73: Assertion failed in tb_alu: negative test failed
%Error: tests/tb_alu.sv:73: Verilog $stop
Aborting...
Aborted
error: Recipe `test-sv` failed on line 10 with exit code 134
现在的 negative 标志位只是简单地取了 y[3],并没有考虑溢出情况。但在常见 CPU 的 ALU 中,通常会把 negative 定义为结果最高位,即 N = y[3],再用独立的 overflow 标志判断有符号溢出。这样职责更清晰。
所以,这里的测试错误是因为我们没有实现 overflow 标志位。现在开始实现 overflow 和 carry 标志位。
把第七个测试目标结果改为 0,继续接下来的操作。
当前 ALU 的 y 只有 4 位。比如:
1111+0001=1\,0000
完整结果需要 5 位,但如果只赋给 4 位的 y,最高位的进位就会被丢掉。
因此,需要先想一想:怎样把两个 4 位数相加时产生的第 5 位保留下来?
提示:可以用一个 5 位的中间变量保存加法结果,再分别取低 4 位和最高位。先不改
overflow,把carry的加法行为单独验证好再继续。
carry
先在 alu.sv 中做这三项修改:
- 新增
output logic carry。 - 在模块内部声明一个 5 位中间变量,例如
logic [4:0] ext;。 - 在
always_comb开始处给carry一个默认值,避免推导出锁存器;然后在加法分支中,用中间变量保存完整结果。
注意,参与加法的两个操作数也要扩展到 5 位,否则需要仔细确认表达式的位宽规则。
完成后,先增加这个测试:
a |
b |
预期 y |
预期 carry |
|---|---|---|---|
| 0011 | 0010 | 0101 | 0 |
| 1111 | 0001 | 0000 | 1 |
| 1111 | 1111 | 1110 | 1 |
先只实现加法的 carry,暂时不要实现减法的 carry 和 overflow。
加法 carry 的实现
module alu (
input logic [3:0] a,
input logic [3:0] b,
input logic [1:0] op,
output logic [3:0] y,
output logic zero,
output logic negative,
output logic carry
);
logic [4:0] ext;
always_comb begin
ext = 5'b00000;
carry = 1'b0;
case (op)
2'b00: begin
ext = {1'b0, a} + {1'b0, b};
y = ext[3:0];
carry = ext[4];
end
2'b01: y = a - b;
2'b10: y = a & b;
2'b11: y = a | b;
endcase
if (y === 4'd0) zero = 1'b1;
else zero = 1'b0;
negative = y[3];
end
endmodule
这里使用 {1'b0, a} 将 a 扩展为 5 位,最高位补 0。然后把加法结果赋给 ext,再分别取低 4 位给 y,最高位给 carry。这样能保证最高位确实被保留。
...
// Test 8: Carry
a = 4'b0011;
b = 4'b0010;
op = 2'b00;
#1;
if (carry !== 1'b0) $fatal(1, "CARRY TEST 1 should be carry = 0");
a = 4'b1111;
b = 4'b0001;
op = 2'b00;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 2 should be carry = 1");
a = 4'b1111;
b = 4'b1111;
op = 2'b00;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 3 should be carry = 1");
...
运行 just test-sv alu tb_alu,确认全部通过。
减法 carry 的实现
接下来实现减法的 carry。
先思考一个问题:在 4 位无符号减法中,a = 3、b = 2 时没有借位;而 a = 2、b = 3 时发生借位。按照我们约定的 carry = 1 表示没有借位,这两种情况下 carry 应该分别是多少?
按照我们采用的约定:
a |
b |
运算 | 是否借位 | carry |
|---|---|---|---|---|
| 3 | 2 | $3-2=1$ | 否 | 1 |
| 2 | 3 | $2-3=-1$ | 是 | 0 |
| 5 | 5 | $5-5=0$ | 否 | 1 |
| 0 | 1 | $0-1=-1$ | 是 | 0 |
下一步,实现减法的 carry
现在已经有了 5 位中间变量 ext。可以利用二进制补码,把减法转换为加法:
a-b=a+\overline{b}+1
这里的 \overline{b} 是对 b 的每一位取反。
你的任务:
在 op == 2'b01 的分支中:
- 用 5 位扩展后的
a和b计算a + ~b + 1。 - 将
ext[3:0]赋给y。 - 将
ext[4]赋给carry。
注意:要对扩展到 5 位的 b 取反,再加 1,确保运算位宽正确。
完成后,至少测试以下三种情况:
– 3 - 2:y = 0001,carry = 1
– 2 - 3:y = 1111,carry = 0
– 5 - 5:y = 0000,carry = 1
此时 op == 2'b01 的分支应当如下:
2'b01: begin
ext = {1'b0, a} + {1'b0, ~b} + 5'd1;
y = ext[3:0];
carry = ext[4];
end
注意:这里的
~b是对b的每一位取反,不要写成~{1'b0, b}!
添加测试:
a = 4'd3;
b = 4'd2;
op = 2'b01;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 4 should be carry = 1");
a = 4'd2;
b = 4'd3;
#1;
if (carry !== 1'b0) $fatal(1, "CARRY TEST 5 should be carry = 0");
a = 4'd5;
b = 4'd5;
#1;
if (carry !== 1'b1) $fatal(1, "CARRY TEST 6 should be carry = 1");
测试应当通过。
overflow
现在进入最后一个标志位:overflow(有符号溢出)。
溢出条件:
4 位二进制补码的表示范围为 -8 到 +7。
| 运算 | 4 位结果 | overflow |
|---|---|---|
| $3+2=5$ | 0101 |
0 |
| $7+1=8$ | 1000 |
1 |
| $(-3)+(-2)=-5$ | 1011 |
0 |
| $3-2=1$ | 0001 |
0 |
| $(-8)-1=-9$ | 0111 |
1 |
注意:carry 和 overflow 是不同的概念。比如 7+1 没有无符号进位,但发生了有符号溢出。
实现加法 overflow
对于有符号加法,溢出只会发生在以下情况:
- 两个操作数符号相同;
- 结果符号与操作数符号不同。
可以把它转化为逻辑表达式。
提示:操作数的符号位分别是
a[3]、b[3],结果符号位是y[3]。
先写出这个条件,并在 op == 2'b00 的分支中赋值给 overflow。
至少增加以下两个测试:
a = 4'b0111,b = 4'b0001,预期y = 4'b1000、overflow = 1。a = 4'b0011,b = 4'b0010,预期y = 4'b0101、overflow = 0。
先只实现加法的 overflow。通过后,我们再用类似的思路处理减法。
分支 op == 2'b00 的代码应当如下:
2'b00: begin
// Carry
ext = {1'b0, a} + {1'b0, b};
y = ext[3:0];
carry = ext[4];
// Overflow
if (a[3] === b[3] && y[3] !== a[3]) overflow = 1'b1;
end
添加新的测试:
// Test 9: Overflow
a = 4'b0111;
b = 4'b0001;
op = 2'b00;
#1;
if (overflow !== 1) $fatal(1, "OVERFLOW TEST 1 should be overflow = 1");
a = 4'b0011;
b = 4'b0010;
#1;
if (overflow !== 0) $fatal(1, "OVERFLOW TEST 2 should be overflow = 0");
测试应当通过。
实现减法 overflow
有符号减法发生溢出的条件与加法不同:
- 被减数
a和减数b的符号不同; - 结果
y的符号与被减数a的符号不同。
也就是说,判断逻辑需要关注 a[3]、b[3] 和 y[3]。
在 2'b01 的减法分支中实现这个条件,并增加以下测试:
a |
b |
运算 | 预期 y |
overflow |
|---|---|---|---|---|
0111 |
0001 |
$7-1$ | 0110 |
0 |
1000 |
0001 |
$-8-1$ | 0111 |
1 |
0111 |
1111 |
$7-(-1)$ | 1000 |
1 |
注意:最后一项中,1111 按 4 位补码解释为 -1。
分支 op == 2'b01 的代码应当如下:
2'b01: begin
// Carry
ext = {1'b0, a} + {1'b0, ~b} + 5'd1;
y = ext[3:0];
carry = ext[4];
// Overflow
if (a[3] !== b[3] && y[3] !== a[3]) overflow = 1'b1;
end
添加测试:
a = 4'b0111;
b = 4'b0001;
op = 2'b01;
#1;
if (overflow !== 0) $fatal(1, "OVERFLOW TEST 3 should be overflow = 0");
a = 4'b1000;
b = 4'b0001;
#1;
if (overflow !== 1) $fatal(1, "OVERFLOW TEST 4 should be overflow = 1");
a = 4'b0111;
b = 4'b1111;
#1;
if (overflow !== 1) $fatal(1, "OVERFLOW TEST 5 should be overflow = 1");
目前已经完全实现了功能,不过,overflow 需要改进一下判断方法。例如,现在加法中的溢出判断是:
if (a[3] === b[3] && y[3] !== a[3]) overflow = 1'b1;
这里使用了 ===,表示严格比较,避免出现 X 或 Z 的情况。但是对于 ALU 的正常运算逻辑,可以使用普通的 ==、!=,或者直接写成布尔表达式。
现在,把加法条件判断改成更易读的形式:
overflow = (a[3] == b[3]) && (y[3] != a[3]);
减法同理。
故现在的 alu.sv 完整模块应当如下:
module alu (
input logic [3:0] a,
input logic [3:0] b,
input logic [1:0] op,
output logic [3:0] y,
output logic zero,
output logic negative,
output logic carry,
output logic overflow
);
logic [4:0] ext;
always_comb begin
ext = 5'b00000;
carry = 1'b0;
overflow = 1'b0;
case (op)
2'b00: begin
// Carry
ext = {1'b0, a} + {1'b0, b};
y = ext[3:0];
carry = ext[4];
// Overflow
overflow = (a[3] == b[3]) && (y[3] != a[3]);
end
2'b01: begin
// Carry
ext = {1'b0, a} + {1'b0, ~b} + 5'd1;
y = ext[3:0];
carry = ext[4];
// Overflow
overflow = (a[3] != b[3]) && (y[3] != a[3]);
end
2'b10: y = a & b;
2'b11: y = a | b;
endcase
if (y === 4'd0) zero = 1'b1;
else zero = 1'b0;
negative = y[3];
end
endmodule
至此,4 位 ALU 的设计已经完成,支持加法、减法、按位与、按位或四种运算,并且实现了 zero、negative、carry 和 overflow 四个标志位。
