⚡ 第02课:ALU设计——算术逻辑单元

📖 本课目标

从零设计并实现一个8位ALU(Arithmetic Logic Unit),支持13种运算操作。ALU是CPU的"计算引擎",所有数据处理都经过它。我们将用Verilog实现,并通过Verilator验证其正确性。

🧠 ALU是什么?

ALU(算术逻辑单元)是CPU中执行算术和逻辑运算的核心部件。它接收两个操作数和一个操作码,输出运算结果和状态标志。在我们8位复古电脑中,ALU是整个CPU最关键的组合逻辑模块。

ALU输入输出接口 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌──────────────────────┐ A[7:0]─┤ ├─Y[7:0] 运算结果 │ │ B[7:0]─┤ ALU ├─Z 零标志 │ ├─N 负标志 op[3:0]┤ ├─C 进位标志 │ ├─V 溢出标志 cin ───┤ │ └──────────────────────┘ 操作码定义: 0000 = ADD 0001 = SUB 0010 = AND 0011 = OR 0100 = XOR 0101 = NOT 0110 = SHL 0111 = SHR 1000 = INC 1001 = DEC 1010 = CMP 1011 = ROL 1100 = ROR

📐 ALU操作详解

算术运算

操作助记符功能标志位影响
加法ADDY = A + B + CinZ,N,C,V
减法SUBY = A - B - CinZ,N,C,V
递增INCY = A + 1Z,N,V
递减DECY = A - 1Z,N,V
比较CMPA - B(不存结果)Z,N,C,V

逻辑运算

操作助记符功能标志位影响
ANDY = A & BZ,N
ORY = A | BZ,N
异或XORY = A ^ BZ,N
取反NOTY = ~AZ,N

移位与旋转

操作助记符功能标志位影响
逻辑左移SHLY = A << 1, LSB=0Z,N,C
逻辑右移SHRY = A >> 1, MSB=0Z,N,C
循环左移ROLY = {A[6:0],A[7]}Z,N,C
循环右移RORY = {A[0],A[7:1]}Z,N,C

🔧 Verilog实现

ALU模块代码

// ========================================
// 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

🧪 仿真验证

Verilator C++测试台

// 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! ✅

🔍 设计要点深度分析

为什么进位用9位加法器?

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位是进位

溢出标志V的判断逻辑

溢出标志仅对有符号运算有意义。判断规则:

// 加法溢出:正+正=负 或 负+负=正
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的减法运算符已经内部处理了二进制补码,所以我们直接用 - 即可。

📊 ALU操作码快速参考

op[3:0]操作表达式CV
0000ADDA + B + Cin进位有符号溢出
0001SUBA - B - Cin借位有符号溢出
0010ANDA & B00
0011ORA | B00
0100XORA ^ B00
0101NOT~A00
0110SHLA << 1A[7]0
0111SHRA >> 1A[0]0
1000INCA + 1进位有符号溢出
1001DECA - 1借位有符号溢出
1010CMPA - B借位有符号溢出
1011ROL{A[6:0],A[7]}A[7]0
1100ROR{A[0],A[7:1]}A[0]0

📝 练习

练习1:手动计算ALU结果

给定 A = 0xB3, B = 0x4A, cin = 0,手动计算以下操作的结果和标志位:

  1. ADD: Y = ? Z=? N=? C=? V=?
  2. SUB: Y = ? Z=? N=? C=? V=?
  3. AND: Y = ? Z=? N=?
  4. SHL A: Y = ? C=?
  5. ROR A: Y = ? C=?

练习2:扩展ALU功能

在现有ALU基础上增加以下操作:

修改Verilog代码,添加操作码4'd13、4'd14、4'd15,并验证。

练习3:BCD加法修正

BCD(Binary-Coded Decimal)用4位二进制表示1位十进制数。BCD加法后需要修正:如果低4位>9,加6修正。实现一个BCD修正模块,并与ALU的ADD操作配合。

🏆 成就解锁

⚡ 算术逻辑大师

达成条件:

奖励:你已经造出了CPU的心脏!ALU是所有计算的源头,理解它意味着你真正理解了计算机如何"计算"。