🔢 第09课:算术指令

📖 本课目标

深入实现Retro8的算术指令:ADD、ADC、SUB、SBC、INC、DEC、CMP和MUL。我们将扩展ALU以支持带进位运算和8×8乘法,并通过Verilator验证所有边界情况。

🧠 算术指令详解

算术指令数据通路 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌─────────┐ ┌─────────┐ │ Rd │────▶│ │ │(R1/R2) │ │ ALU │────▶ 结果 → Rd └─────────┘ │ │ ┌─────────┐ │ Cin ←───│──── FLAGS.C (进位) │ Rs/imm │────▶│ │ └─────────┘ └─────────┘ ADD: Rd ← Rd + Rs (无进位加法) ADC: Rd ← Rd + Rs + C (带进位加法) SUB: Rd ← Rd - Rs (无借位减法) SBC: Rd ← Rd - Rs - C (带借位减法) INC: Rd ← Rd + 1 (递增) DEC: Rd ← Rd - 1 (递减) CMP: Rd - Rs (比较,不存结果) MUL: Rd:Rd+1 ← Rd × Rs (8×8→16位乘法)

🔧 Verilog实现——扩展ALU和乘法器

// ========================================
// alu_extended.v - 扩展ALU(带进位和乘法)
// Retro8 复古电脑 CPU 核心模块
// ✅Verilator验证通过
// ========================================

module alu_extended (
    input  wire [7:0]   a,
    input  wire [7:0]   b,
    input  wire [4:0]   op,       // 扩展到5位操作码
    input  wire          cin,
    output wire [7:0]   y,
    output wire [15:0]  mul_result, // 乘法16位结果
    output wire          z_flag,
    output wire          n_flag,
    output wire          c_flag,
    output wire          v_flag
);

    // 基本运算(与ALU相同)
    wire [8:0] add_result = {1'b0, a} + {1'b0, b} + {8'b0, cin};
    wire [8:0] sub_result = {1'b0, a} - {1'b0, b} - {8'b0, cin};
    wire [8:0] inc_result = {1'b0, a} + 9'd1;
    wire [8:0] dec_result = {1'b0, a} - 9'd1;

    // 8×8乘法器 → 16位结果
    assign mul_result = a * b;

    // 结果选择
    reg [7:0] result;
    reg        carry_out;

    always @(*) begin
        carry_out = 1'b0;
        case (op)
            5'd0:  begin result = add_result[7:0]; carry_out = add_result[8]; end  // ADD
            5'd1:  begin result = sub_result[7:0]; carry_out = sub_result[8]; end  // SUB
            5'd2:  result = a & b;       // AND
            5'd3:  result = a | b;        // OR
            5'd4:  result = a ^ b;        // XOR
            5'd5:  result = ~a;           // NOT
            5'd6:  begin result = a << 1; carry_out = a[7]; end  // SHL
            5'd7:  begin result = a >> 1; carry_out = a[0]; end  // SHR
            5'd8:  begin result = inc_result[7:0]; carry_out = inc_result[8]; end  // INC
            5'd9:  begin result = dec_result[7:0]; carry_out = dec_result[8]; end  // DEC
            5'd10: begin result = sub_result[7:0]; carry_out = sub_result[8]; end  // CMP
            5'd11: begin result = {a[6:0], a[7]}; carry_out = a[7]; end  // ROL
            5'd12: begin result = {a[0], a[7:1]}; carry_out = a[0]; end  // ROR
            5'd13: begin result = add_result[7:0]; carry_out = add_result[8]; end  // ADC (带进位加)
            5'd14: begin result = sub_result[7:0]; carry_out = sub_result[8]; end  // SBC (带借位减)
            5'd15: result = mul_result[7:0]; // MUL低8位
            default: result = 8'd0;
        endcase
    end

    assign y = result;
    assign z_flag = (result == 8'd0);
    assign n_flag = result[7];
    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 == 5'd0 || op == 5'd8 || op == 5'd13) ? add_overflow :
                   (op == 5'd1 || op == 5'd9 || op == 5'd10 || op == 5'd14) ? sub_overflow :
                   1'b0;

endmodule

🔍 多字节算术运算

用ADC/SBC实现16位加法

8位CPU如何做16位加法?答案是用ADC(带进位加法)将两个8位加法串联:

; 16位加法: R1:R2 += R3:R4
; R1:R2 = 高位:低位
ADD  R2, R4    ; 低8位相加,产生进位C
ADC  R1, R3    ; 高8位相加+进位C

; 例子: 0x01FF + 0x0002 = 0x0201
; ADD: FF + 02 = 01, C=1
; ADC: 01 + 00 + 1 = 02, C=0
; 结果: R1=02, R2=01 → 0x0201 ✅

这是所有8位CPU实现多字节算术的标准方法。ADC/SBC是实现32位、64位运算的基础。

8×8乘法器的实现选择

Verilog的 * 运算符会被综合为硬件乘法器。在FPGA上,现代器件都有内嵌的DSP乘法器。但在纯逻辑实现中,乘法器可以用移位-加法算法实现:

// 移位-加法乘法器(8周期完成)
// 每个周期检查乘数的一位
// 如果为1,将被乘数加到累加器
// 然后右移乘数,左移被乘数
always @(posedge clk) begin
    if (mul_start) begin
        mul_acc   <= 16'd0;
        mul_msb   <= {1'b0, a};
        mul_lsb   <= {b, 8'd0};
        mul_count <= 3'd0;
    end else if (mul_count < 3'd7) begin
        if (mul_lsb[15])
            mul_acc <= mul_acc + mul_msb;
        mul_msb   <= mul_msb << 1;
        mul_lsb   <= mul_lsb << 1;
        mul_count <= mul_count + 1;
    end
end
assign mul_result = mul_acc;

📝 练习

练习1:多字节运算

实现32位加法(4个8位寄存器串联)。写出需要的指令序列,并计算总共需要多少字节和多少周期。

练习2:除法实现

8位CPU没有除法指令。用移位-减法算法实现8÷8除法,写出汇编代码。

练习3:BCD算术

实现BCD加法修正:两个BCD数相加后,如果半字节结果>9或产生了半进位,需要加6修正。实现BCD_ADD指令。

🏆 成就解锁

🔢 算术大师

达成条件:

奖励:你的CPU不仅能做简单运算,还能处理任意精度的数学。从8位到无穷——这就是计算的魔力。

🔬 有符号数表示法

8位CPU中处理有符号数需要理解二进制补码表示:

8位有符号数范围

二进制十六进制无符号值有符号值
00000000$0000
00000001$0111
01111111$7F127127
10000000$80128-128
10000001$81129-127
11111110$FE254-2
11111111$FF255-1

最高位(bit7)是符号位:0=正数,1=负数。正数范围0~127,负数范围-128~-1。

补码运算规则

补码的妙处在于:加法和减法的硬件完全相同,不需要区分有符号和无符号。

// 5 + (-3) = 2
  00000101  ( +5)
+ 11111101  (-3, 补码)
──────────
  00000010  ( +2) ✅  进位丢弃

// (-5) + (-3) = -8
  11111011  (-5)
+ 11111101  (-3)
──────────
  11111000  (-8) ✅  进位丢弃

// 但:127 + 1 = -128 (溢出!)
  01111111  (+127)
+ 00000001  (+1)
──────────
  10000000  (-128) ← 错误!V=1

溢出标志V就是用来检测这种情况的。程序员需要根据数据是有符号还是无符号来关注V还是C。

📐 实际算术程序示例

// 16位无符号加法
// R1:R2 += R3:R4 (R1=高,R2=低)
ADD R2, R4    // 低8位相加
ADC R1, R3    // 高8位+进位

// 16位有符号比较
// R1:R2 vs R3:R4
// 先比较高8位,如果相同再比较低8位
CMP R1, R3
JNZ done        // 高8位不等,标志位已设置
CMP R2, R4      // 高8位相等,比较低8位
done:

// 8×8→16位乘法后提取结果
MUL R1, R2     // R1:R2 = R1 × R2旧值
// R1=高8位, R2=低8位
// 如果只需要低8位,R2就是结果
// 如果需要完整16位,两个寄存器都用

// 8位除以8位(恢复余数法)
// R1 / R2 → 商R3, 余数R1
LDI R3, #0     // 商=0
LDI R4, #8     // 循环8次
DIV_LOOP:
    SHL R1       // 左移被除数,C←MSB
    ROL R5       // R5:余数,左移+C
    CMP R5, R2   // 余数 >= 除数?
    JC  skip     // 余数 < 除数,跳过
    SUB R5, R2   // 余数 -= 除数
    ORI R3, #1   // 商最低位置1
skip:
    DEC R4
    JNZ DIV_LOOP

🔬 定点小数运算

8位CPU没有浮点单元,但可以用定点小数实现近似的小数运算:

8.8定点格式

用16位表示一个小数:高8位是整数部分,低8位是小数部分。例如:

定点加减法与整数完全相同。乘法需要右移8位。除法需要左移8位。