深入实现Retro8的算术指令:ADD、ADC、SUB、SBC、INC、DEC、CMP和MUL。我们将扩展ALU以支持带进位运算和8×8乘法,并通过Verilator验证所有边界情况。
// ========================================
// 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
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位运算的基础。
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;
实现32位加法(4个8位寄存器串联)。写出需要的指令序列,并计算总共需要多少字节和多少周期。
8位CPU没有除法指令。用移位-减法算法实现8÷8除法,写出汇编代码。
实现BCD加法修正:两个BCD数相加后,如果半字节结果>9或产生了半进位,需要加6修正。实现BCD_ADD指令。
达成条件:
奖励:你的CPU不仅能做简单运算,还能处理任意精度的数学。从8位到无穷——这就是计算的魔力。
8位CPU中处理有符号数需要理解二进制补码表示:
| 二进制 | 十六进制 | 无符号值 | 有符号值 |
|---|---|---|---|
| 00000000 | $00 | 0 | 0 |
| 00000001 | $01 | 1 | 1 |
| 01111111 | $7F | 127 | 127 |
| 10000000 | $80 | 128 | -128 |
| 10000001 | $81 | 129 | -127 |
| 11111110 | $FE | 254 | -2 |
| 11111111 | $FF | 255 | -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没有浮点单元,但可以用定点小数实现近似的小数运算:
用16位表示一个小数:高8位是整数部分,低8位是小数部分。例如:
定点加减法与整数完全相同。乘法需要右移8位。除法需要左移8位。