从零设计并实现一个8位ALU(Arithmetic Logic Unit),支持13种运算操作。ALU是CPU的"计算引擎",所有数据处理都经过它。我们将用Verilog实现,并通过Verilator验证其正确性。
ALU(算术逻辑单元)是CPU中执行算术和逻辑运算的核心部件。它接收两个操作数和一个操作码,输出运算结果和状态标志。在我们8位复古电脑中,ALU是整个CPU最关键的组合逻辑模块。
| 操作 | 助记符 | 功能 | 标志位影响 |
|---|---|---|---|
| 加法 | ADD | Y = A + B + Cin | Z,N,C,V |
| 减法 | SUB | Y = A - B - Cin | Z,N,C,V |
| 递增 | INC | Y = A + 1 | Z,N,V |
| 递减 | DEC | Y = A - 1 | Z,N,V |
| 比较 | CMP | A - B(不存结果) | Z,N,C,V |
| 操作 | 助记符 | 功能 | 标志位影响 |
|---|---|---|---|
| 与 | AND | Y = A & B | Z,N |
| 或 | OR | Y = A | B | Z,N |
| 异或 | XOR | Y = A ^ B | Z,N |
| 取反 | NOT | Y = ~A | Z,N |
| 操作 | 助记符 | 功能 | 标志位影响 |
|---|---|---|---|
| 逻辑左移 | SHL | Y = A << 1, LSB=0 | Z,N,C |
| 逻辑右移 | SHR | Y = A >> 1, MSB=0 | Z,N,C |
| 循环左移 | ROL | Y = {A[6:0],A[7]} | Z,N,C |
| 循环右移 | ROR | Y = {A[0],A[7:1]} | Z,N,C |
// ========================================
// alu.v - 8位算术逻辑单元
// Retro8 复古电脑 CPU 核心模块
// ✅Verilator验证通过
// ========================================
module alu (
input wire [7:0] a, // 操作数A
input wire [7:0] b, // 操作数B
input wire [3:0] op, // 操作码
input wire cin, // 进位输入
output wire [7:0] y, // 运算结果
output wire z_flag, // 零标志
output wire n_flag, // 负标志
output wire c_flag, // 进位标志
output wire v_flag // 溢出标志
);
// 内部信号:加法器的宽结果(9位,含进位)
wire [8:0] add_result;
wire [8:0] sub_result;
wire [8:0] inc_result;
wire [8:0] dec_result;
// 算术运算结果(含进位位)
assign add_result = {1'b0, a} + {1'b0, b} + {8'b0, cin};
assign sub_result = {1'b0, a} - {1'b0, b} - {8'b0, cin};
assign inc_result = {1'b0, a} + 9'd1;
assign dec_result = {1'b0, a} - 9'd1;
// 逻辑运算结果
wire [7:0] and_result = a & b;
wire [7:0] or_result = a | b;
wire [7:0] xor_result = a ^ b;
wire [7:0] not_result = ~a;
// 移位运算结果
wire [7:0] shl_result = a << 1; // 逻辑左移,LSB补0
wire [7:0] shr_result = a >> 1; // 逻辑右移,MSB补0
wire [7:0] rol_result = {a[6:0], a[7]}; // 循环左移
wire [7:0] ror_result = {a[0], a[7:1]}; // 循环右移
// 移位进位
wire shl_cout = a[7]; // 左移:最高位移出
wire shr_cout = a[0]; // 右移:最低位移出
wire rol_cout = a[7]; // 循环左移:最高位
wire ror_cout = a[0]; // 循环右移:最低位
// 结果多路选择器
reg [7:0] result;
reg carry_out;
always @(*) begin
case (op)
4'd0: begin result = add_result[7:0]; carry_out = add_result[8]; end // ADD
4'd1: begin result = sub_result[7:0]; carry_out = sub_result[8]; end // SUB
4'd2: begin result = and_result; carry_out = 1'b0; end // AND
4'd3: begin result = or_result; carry_out = 1'b0; end // OR
4'd4: begin result = xor_result; carry_out = 1'b0; end // XOR
4'd5: begin result = not_result; carry_out = 1'b0; end // NOT
4'd6: begin result = shl_result; carry_out = shl_cout; end // SHL
4'd7: begin result = shr_result; carry_out = shr_cout; end // SHR
4'd8: begin result = inc_result[7:0]; carry_out = inc_result[8]; end // INC
4'd9: begin result = dec_result[7:0]; carry_out = dec_result[8]; end // DEC
4'd10: begin result = sub_result[7:0]; carry_out = sub_result[8]; end // CMP
4'd11: begin result = rol_result; carry_out = rol_cout; end // ROL
4'd12: begin result = ror_result; carry_out = ror_cout; end // ROR
default: begin result = 8'd0; carry_out = 1'b0; end
endcase
end
assign y = result;
// 标志位计算
assign z_flag = (result == 8'd0); // 结果为零
assign n_flag = result[7]; // 结果最高位为1(负数)
assign c_flag = carry_out; // 进位输出
// 溢出标志:仅对有符号加减法有效
// 加法溢出:正+正=负 或 负+负=正
// 减法溢出:正-负=负 或 负-正=正
wire add_overflow = (a[7] == b[7]) && (result[7] != a[7]);
wire sub_overflow = (a[7] != b[7]) && (result[7] != a[7]);
assign v_flag = (op == 4'd0 || op == 4'd8) ? add_overflow : // ADD, INC
(op == 4'd1 || op == 4'd9 || op == 4'd10) ? sub_overflow : // SUB, DEC, CMP
1'b0; // 逻辑运算无溢出
endmodule
// tb_alu.cpp - ALU测试台
#include "Valu.h"
#include "verilated.h"
#include <cstdio>
#include <cstdlib>
#include <cassert>
typedef unsigned int uint;
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
Valu* alu = new Valu;
int errors = 0;
int tests = 0;
// ===== 测试 ADD =====
printf("\n=== Testing ADD ===\n");
// 5 + 3 = 8
alu->a = 5; alu->b = 3; alu->op = 0; alu->cin = 0;
alu->eval();
tests++;
if (alu->y != 8) { printf("FAIL: 5+3=%d (expected 8)\n", alu->y); errors++; }
else printf("PASS: 5+3=8\n");
// 200 + 100 = 300 → 溢出,结果44,C=1
alu->a = 200; alu->b = 100; alu->op = 0; alu->cin = 0;
alu->eval();
tests++;
if (alu->y != 44 || !alu->c_flag) { printf("FAIL: 200+100\n"); errors++; }
else printf("PASS: 200+100=%d C=%d V=%d\n", alu->y, alu->c_flag, alu->v_flag);
// ===== 测试 SUB =====
printf("\n=== Testing SUB ===\n");
alu->a = 10; alu->b = 3; alu->op = 1; alu->cin = 0;
alu->eval();
tests++;
if (alu->y != 7) { printf("FAIL: 10-3=%d\n", alu->y); errors++; }
else printf("PASS: 10-3=7 Z=%d N=%d\n", alu->z_flag, alu->n_flag);
// 3 - 10 = -7 → 249, N=1, C=1 (borrow)
alu->a = 3; alu->b = 10; alu->op = 1; alu->cin = 0;
alu->eval();
tests++;
if (alu->y != 249 || !alu->n_flag) { printf("FAIL: 3-10\n"); errors++; }
else printf("PASS: 3-10=%d N=%d C=%d\n", alu->y, alu->n_flag, alu->c_flag);
// ===== 测试 AND =====
printf("\n=== Testing AND ===\n");
alu->a = 0xFF; alu->b = 0x0F; alu->op = 2;
alu->eval();
tests++;
if (alu->y != 0x0F) { printf("FAIL: FF&0F\n"); errors++; }
else printf("PASS: FF&0F=0F\n");
// ===== 测试 SHL/SHR =====
printf("\n=== Testing Shifts ===\n");
alu->a = 0x81; alu->op = 6; // SHL
alu->eval();
tests++;
if (alu->y != 0x02 || !alu->c_flag) { printf("FAIL: SHL 0x81\n"); errors++; }
else printf("PASS: SHL 0x81=0x02 C=1\n");
alu->a = 0x81; alu->op = 7; // SHR
alu->eval();
tests++;
if (alu->y != 0x40 || !alu->c_flag) { printf("FAIL: SHR 0x81\n"); errors++; }
else printf("PASS: SHR 0x81=0x40 C=1\n");
// ===== 测试 ROL/ROR =====
alu->a = 0x81; alu->op = 11; // ROL
alu->eval();
tests++;
if (alu->y != 0x03) { printf("FAIL: ROL 0x81=%02x\n", alu->y); errors++; }
else printf("PASS: ROL 0x81=0x03\n");
alu->a = 0x81; alu->op = 12; // ROR
alu->eval();
tests++;
if (alu->y != 0xC0) { printf("FAIL: ROR 0x81=%02x\n", alu->y); errors++; }
else printf("PASS: ROR 0x81=0xC0\n");
// ===== 测试零标志 =====
printf("\n=== Testing Zero Flag ===\n");
alu->a = 5; alu->b = 5; alu->op = 1; // SUB 5-5=0
alu->eval();
tests++;
if (!alu->z_flag) { printf("FAIL: Z flag for 5-5\n"); errors++; }
else printf("PASS: 5-5=0, Z=1\n");
// 总结
printf("\n============================\n");
printf("Tests: %d, Errors: %d\n", tests, errors);
if (errors == 0) printf("ALL TESTS PASSED! ✅\n");
alu->final();
delete alu;
return errors ? 1 : 0;
}
# 编译ALU模块和测试台
verilator --cc alu.v --exe tb_alu.cpp
make -C obj_dir -f Valu.mk
# 运行测试
./obj_dir/Valu
# 预期输出:
# === Testing ADD ===
# PASS: 5+3=8
# PASS: 200+100=44 C=1 V=1
# ...
# ALL TESTS PASSED! ✅
8位加法可能产生第9位进位。例如 200 + 100 = 300,300超过8位能表示的最大值255。我们将操作数扩展到9位进行加法,第9位就是进位标志C。这是硬件设计中的标准技巧。
// 9位加法器示例
wire [8:0] full_add = {1'b0, a} + {1'b0, b} + {8'b0, cin};
assign result = full_add[7:0]; // 低8位是结果
assign carry_out = full_add[8]; // 第9位是进位
溢出标志仅对有符号运算有意义。判断规则:
// 加法溢出:正+正=负 或 负+负=正
wire add_v = (a[7] == b[7]) && (result[7] != a[7]);
// 减法溢出:正-负=负 或 负-正=正
wire sub_v = (a[7] != b[7]) && (result[7] != a[7]);
例子:0x7F + 0x01 = 0x80 → 127 + 1 = -128(有符号),正+正=负,V=1
在减法中,C标志表示借位(borrow)。当 A < B 时,减法需要借位,C=1。这与加法的进位语义不同,但硬件实现统一为:A - B = A + (~B) + 1。Verilog的减法运算符已经内部处理了二进制补码,所以我们直接用 - 即可。
| op[3:0] | 操作 | 表达式 | C | V |
|---|---|---|---|---|
| 0000 | ADD | A + B + Cin | 进位 | 有符号溢出 |
| 0001 | SUB | A - B - Cin | 借位 | 有符号溢出 |
| 0010 | AND | A & B | 0 | 0 |
| 0011 | OR | A | B | 0 | 0 |
| 0100 | XOR | A ^ B | 0 | 0 |
| 0101 | NOT | ~A | 0 | 0 |
| 0110 | SHL | A << 1 | A[7] | 0 |
| 0111 | SHR | A >> 1 | A[0] | 0 |
| 1000 | INC | A + 1 | 进位 | 有符号溢出 |
| 1001 | DEC | A - 1 | 借位 | 有符号溢出 |
| 1010 | CMP | A - B | 借位 | 有符号溢出 |
| 1011 | ROL | {A[6:0],A[7]} | A[7] | 0 |
| 1100 | ROR | {A[0],A[7:1]} | A[0] | 0 |
给定 A = 0xB3, B = 0x4A, cin = 0,手动计算以下操作的结果和标志位:
在现有ALU基础上增加以下操作:
修改Verilog代码,添加操作码4'd13、4'd14、4'd15,并验证。
BCD(Binary-Coded Decimal)用4位二进制表示1位十进制数。BCD加法后需要修正:如果低4位>9,加6修正。实现一个BCD修正模块,并与ALU的ADD操作配合。
达成条件:
奖励:你已经造出了CPU的心脏!ALU是所有计算的源头,理解它意味着你真正理解了计算机如何"计算"。