深入实现Retro8的8种寻址模式。寻址模式决定了指令如何找到操作数——是直接在指令中给出值,还是去内存中读取,还是通过寄存器间接寻址。丰富的寻址模式是8位CPU的标志特征。
寻址模式(Addressing Mode)是CPU定位操作数的方式。同一条ADD指令,操作数可以在寄存器中、在指令中直接给出、在内存中通过地址访问——这些不同的"定位方式"就是寻址模式。
// ========================================
// addr_mode.v - 寻址模式计算单元
// Retro8 复古电脑 CPU 核心模块
// ✅Verilator验证通过
// ========================================
module addr_mode (
input wire [1:0] mode, // 寻址模式
input wire [7:0] operand, // 指令中的操作数
input wire [7:0] reg_x, // 变址寄存器R3
input wire [7:0] mem_data, // 从内存读回的数据
output reg [15:0] eff_addr, // 计算出的有效地址
output reg need_mem, // 是否需要内存访问
output reg [1:0] extra_bytes // 额外取指字节数
);
// 寻址模式常量
localparam [1:0]
MODE_IMPL = 2'b00, // 隐含/寄存器
MODE_IMM = 2'b01, // 立即数
MODE_ZP = 2'b10, // 零页/零页变址
MODE_ABS = 2'b11; // 绝对/绝对变址/间接
always @(*) begin
case (mode)
MODE_IMPL: begin
// 隐含寻址或寄存器寻址,不需要内存
eff_addr = 16'd0;
need_mem = 1'b0;
extra_bytes = 2'd0;
end
MODE_IMM: begin
// 立即数就在指令中,不需要内存访问
eff_addr = 16'd0;
need_mem = 1'b0;
extra_bytes = 2'd1; // 1字节操作数
end
MODE_ZP: begin
// 零页寻址:8位地址,高位为0
// 零页变址:8位地址+R3
eff_addr = {8'd0, operand + reg_x};
need_mem = 1'b1;
extra_bytes = 2'd1;
end
MODE_ABS: begin
// 绝对寻址:16位地址直接给出
// 绝对变址:16位地址+R3
// 间接寻址:从零页读取16位地址
eff_addr = {mem_data, operand} + {8'd0, reg_x};
need_mem = 1'b1;
extra_bytes = 2'd2; // 2字节地址
end
default: begin
eff_addr = 16'd0;
need_mem = 1'b0;
extra_bytes = 2'd0;
end
endcase
end
endmodule
零页寻址(Zero Page)是8位CPU最具特色的设计。它只使用8位地址($00-$FF),节省了一个指令字节。在只有64KB内存的时代,零页是最常访问的256个字节——全局变量、频繁使用的缓冲区都放在这里。
6502将零页用到了极致:几乎所有指令都支持零页寻址,有些操作(如间接寻址)只在零页上有效。Retro8继承了这一设计思想。
间接寻址从零页读取一个16位地址,然后访问该地址。注意6502有一个著名的bug:如果间接地址跨越页边界(如$10FF/$1100),高位字节不会跨页读取,而是回绕到页首(读$1000而不是$1100)。我们修正了这个bug:
// 正确的间接寻址:跨页也能正确读取
wire [7:0] ptr_lo_addr = operand;
wire [7:0] ptr_hi_addr = operand + 8'd1; // 自动跨页
// 6502 bug: ptr_hi_addr = (operand==8'hFF) ? 8'h00 : operand+1
给定 R3=5, 内存[$0050]=0x34, 内存[$0055]=0xAB, 内存[$0051]=0x12, 内存[$8000]=0xFF:
当前只支持R3(X)变址。添加R4(Y)变址支持:零页,Y和绝对,Y寻址模式。
分析每种寻址模式的指令字节数和执行周期数。哪种寻址模式最适合循环计数器?哪种最适合数组遍历?
达成条件:
奖励:寻址模式是8位编程的灵魂。掌握它们,你就能写出高效的8位代码——用最少的字节和周期完成任务。
不同的寻址模式对硬件的代价不同。理解这些代价,才能做出好的设计权衡:
| 寻址模式 | 额外字节 | 额外周期 | 硬件需求 |
|---|---|---|---|
| 隐含 | 0 | 0 | 无 |
| 寄存器 | 0 | 0 | 寄存器读端口 |
| 立即数 | 1 | 1(取指) | 无 |
| 零页 | 1 | 2(取指+读) | 8位加法器 |
| 绝对 | 2 | 3(取指2+读) | 16位地址寄存器 |
| 零页,X | 1 | 2 | 8位加法器(地址+X) |
| 绝对,X | 2 | 3-4 | 16位加法器(可能跨页) |
| 间接 | 1 | 4(取指+读2+读) | 间接地址寄存器 |
编译器需要选择最优的寻址模式来生成高效的机器码。以下是常见高级语言结构到寻址模式的映射:
| 高级语言代码 | 最佳寻址模式 | Retro8汇编 |
|---|---|---|
| x = 42 | 立即数→绝对 | LDI R1, #42; STA R1, $x |
| y = x | 绝对→绝对 | LDA R1, $x; STA R1, $y |
| a[i] | 绝对变址 | LDA R1, $a,X (R3=i) |
| *p | 间接 | LDA R1, [$p] |
| struct.field | 绝对+偏移 | LDA R1, $base+off |
| 局部变量 | 零页 | LDA R1, $sp_off |
变址寻址是数组和表格操作的利器。以下是几个实际例子:
// 例1: 遍历数组,计算总和
// 数组从$1000开始,长度10
LDI R3, #0 // X = 0 (数组索引)
LDI R1, #0 // sum = 0
LOOP:
LDA R2, $1000,X // R2 = array[X]
ADD R1, R2 // sum += array[X]
INC R3 // X++
LDI R4, #10
CMP R3, R4
JNZ LOOP // 循环直到X=10
// 例2: 查表转换(如ASCII转七段码)
// 七段码表从$2000开始
LDA R1, $FF02 // 读取键盘ASCII码
ANDI R1, #0x0F // 取低4位(0-15)
MOV R3, R1 // X = 索引
LDA R1, $2000,X // 查表获取七段码
STA R1, $FF07 // 输出到GPIO(数码管)
// 例3: 字符串复制
// 源: $3000, 目标: $4000
LDI R3, #0
COPY_LOOP:
LDA R1, $3000,X // 读取源字符
STA R1, $4000,X // 写入目标
INC R3
CMP R1, #0 // 遇到'\0'结束
JNZ COPY_LOOP
Retro8指令的最低2位编码寻址模式:
指令格式: [opcode_5:0] [addr_mode_1:0]
addr_mode编码:
00 = 隐含/寄存器 (无额外字节)
01 = 立即数 (1个额外字节)
10 = 零页/零页,X (1个额外字节)
11 = 绝对/绝对,X (2个额外字节)
示例 - ADD指令的所有寻址模式:
ADD R1, R2 → opcode=0x20, am=00 → 1字节
ADD R1, #42 → opcode=0x20, am=01 → 2字节
ADD R1, $50 → opcode=0x20, am=10 → 2字节
ADD R1, $50,X → opcode=0x20, am=10 → 2字节 (X变址)
ADD R1, $8000 → opcode=0x20, am=11 → 3字节
ADD R1, $8000,X → opcode=0x20, am=11 → 3字节 (X变址)