🗺️ 第08课:寻址模式

📖 本课目标

深入实现Retro8的8种寻址模式。寻址模式决定了指令如何找到操作数——是直接在指令中给出值,还是去内存中读取,还是通过寄存器间接寻址。丰富的寻址模式是8位CPU的标志特征。

🧠 什么是寻址模式?

寻址模式(Addressing Mode)是CPU定位操作数的方式。同一条ADD指令,操作数可以在寄存器中、在指令中直接给出、在内存中通过地址访问——这些不同的"定位方式"就是寻址模式。

Retro8的8种寻址模式 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. 隐含(Implicit) 操作数由操作码隐含 例: NOP, HLT, SEI 2. 寄存器(Register) 操作数在寄存器中 例: ADD R1, R2 ; R1 += R2 3. 立即数(Immediate) 操作数在指令中 例: LDI R1, #42 ; R1 = 42 4. 零页(Zero Page) 8位地址,访问$00-$FF 例: LDA R1, $50 ; R1 = [$0050] 5. 绝对(Absolute) 16位地址,访问全部64KB 例: LDA R1, $8000 ; R1 = [$8000] 6. 零页变址(Zero Page,X) 地址+R3 例: LDA R1, $50,X ; R1 = [$0050+R3] 7. 绝对变址(Absolute,X) 16位地址+R3 例: LDA R1, $8000,X ; R1 = [$8000+R3] 8. 间接(Indirect) 地址在零页中 例: LDA R1, [$50] ; R1 = [[地址在$50]]16位

🔧 Verilog实现——寻址模式单元

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

📝 练习

练习1:寻址模式计算

给定 R3=5, 内存[$0050]=0x34, 内存[$0055]=0xAB, 内存[$0051]=0x12, 内存[$8000]=0xFF:

  1. LDA R1, $50 → R1=?
  2. LDA R1, $50,X → R1=?
  3. LDA R1, $8000 → R1=?
  4. LDA R1, [$50] → R1=? (间接寻址,地址在$0050/$0051)

练习2:实现变址Y寻址

当前只支持R3(X)变址。添加R4(Y)变址支持:零页,Y和绝对,Y寻址模式。

练习3:寻址模式效率分析

分析每种寻址模式的指令字节数和执行周期数。哪种寻址模式最适合循环计数器?哪种最适合数组遍历?

🏆 成就解锁

🗺️ 寻址模式专家

达成条件:

奖励:寻址模式是8位编程的灵魂。掌握它们,你就能写出高效的8位代码——用最少的字节和周期完成任务。

🔬 寻址模式的硬件代价

不同的寻址模式对硬件的代价不同。理解这些代价,才能做出好的设计权衡:

寻址模式硬件复杂度对比

寻址模式额外字节额外周期硬件需求
隐含00
寄存器00寄存器读端口
立即数11(取指)
零页12(取指+读)8位加法器
绝对23(取指2+读)16位地址寄存器
零页,X128位加法器(地址+X)
绝对,X23-416位加法器(可能跨页)
间接14(取指+读2+读)间接地址寄存器
💡 零页寻址为什么快?因为零页地址只需要8位,不需要取第二个地址字节,也不需要16位地址计算。在早期CPU中,零页寻址可以节省1个字节和1-2个周期——对于只有64KB内存的8位系统来说,这是显著的优化。

📐 寻址模式与编译器

编译器需要选择最优的寻址模式来生成高效的机器码。以下是常见高级语言结构到寻址模式的映射:

高级语言 → 寻址模式映射

高级语言代码最佳寻址模式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变址)