返回首页

6502 寻址方式详解

6502 寻址方式详解

寻址方式 = CPU 如何找到指令的操作数(数据在哪里)

同一条指令(如 LDA)配合不同寻址方式,含义完全不同。掌握寻址方式是读懂 6502 汇编的关键。


目录

  1. 总览对比表
  2. 立即寻址 Immediate
  3. 零页寻址 Zero Page
  4. 零页变址 Zero Page,X / Zero Page,Y
  5. 绝对寻址 Absolute
  6. 绝对变址 Absolute,X / Absolute,Y
  7. 间接寻址 Indirect(仅 JMP)
  8. 间接索引 Indexed Indirect (zp,X)
  9. 索引间接 Indirect Indexed (zp),Y
  10. 相对寻址 Relative(分支跳转)
  11. 隐含寻址 Implied
  12. 累加器寻址 Accumulator
  13. 场景选择指南

1. 总览对比表

# 寻址方式 语法 指令长度 周期* 一句话说明
1 立即寻址 lda #$0A 2字节 2 操作数就是数值本身
2 零页寻址 lda $10 2字节 3 访问 $00-$FF(1字节地址)
3 零页,X lda $10,x 2字节 4 零页地址 + X 偏移
4 绝对寻址 lda $8000 3字节 4 访问任意地址(2字节地址)
5 绝对,X lda $8000,x 3字节 4+ 任意地址 + X 偏移
6 绝对,Y lda $8000,y 3字节 4+ 任意地址 + Y 偏移
7 间接寻址 jmp ($1000) 3字节 5 跳到”指针指向的地址”
8 (zp,X) lda ($10,x) 2字节 6 查指针表后取值
9 (zp),Y lda ($10),y 2字节 5+ 指针 + Y 偏移(最灵活)
10 相对寻址 bne loop 2字节 2/3 向前/后跳 ±127 字节
11 隐含寻址 txs rts 1字节 2 操作数隐含在指令中
12 累加器寻址 asl a 1字节 2 操作对象就是 A 寄存器

* 周期为典型值,跨页访问会 +1


2. 立即寻址 Immediate

语法特征:# 号

1
2
3
4
lda #$0A        ; A = 10        (#$0A 是十六进制数值10)
lda #10 ; A = 10 (十进制写法, 同上)
ldx #$FF ; X = 255
cmp #$80 ; A 与 128 比较

原理

1
2
3
4
5
指令:      A9 0A        (机器码)
│ └── 操作数: $0A 就是数据本身
└───── 操作码: LDA #imm

执行: A ← $0A (数据从"指令流"直接进入寄存器, 不访问内存)

使用场景

场景 示例
给变量赋常量 lda #$0A + sta VAR (BASIC: A=10)
初始化计数器 ldx #$00 (循环前清零)
设置栈指针 ldx #$FF + txs
与固定值比较 cmp #60 (帧计数到60?)
写硬件寄存器 lda #$0A + sta $2001 (开显示)

易错:lda $10(无#)是读内存 $10 的值;lda #$10(有#)是装载数值 16。差一个 # 含义完全不同!


3. 零页寻址 Zero Page

语法特征:1 字节地址($00-$FF)

1
2
3
4
lda $10         ; A = 内存$0010处的值
sta $20 ; 内存$0020 = A
inc $30 ; 内存$0030 的值 +1
lda $00 ; A = 内存$0000 的值

原理

1
2
3
4
5
指令:      A5 10         (机器码)
│ └── 零页地址: $10 → 实际访问 $0010
└───── 操作码: LDA zp

执行: A ← [$0010] (读内存, 地址高字节默认为 $00)

为什么零页快?

  • 零页地址只需 1 字节编码(绝对寻址要 2 字节)
  • CPU 不用拼 16 位地址,访问速度最快
  • 指令短 → 同样 ROM 空间能放更多代码

使用场景

场景 示例
最常用变量 游戏状态、帧计数器、临时变量
FBASIC 变量表索引 lda $10 读当前变量
标志位/开关 lda $00 + and #$01 测试开关
循环计数器 inc $10 + bne loop

NES 开发惯例:零页 256 字节是”黄金地段”,放最高频访问的变量。cc65 编译器也把 sp、ptr1-4 等运行时变量放这里。


4. 零页变址 Zero Page,X / Zero Page,Y

语法:地址,x

1
2
3
4
lda $10,x       ; A = 内存[$0010 + X] 的值
sta $20,x ; 内存[$0020 + X] = A
ldx #$03
lda $10,x ; X=3 → A = 内存$0013 的值

原理

1
2
3
4
5
6
7
8
指令:      B5 10         (机器码: LDA zp,X)
│ └── 基地址: $10
└───── 操作码

执行: 有效地址 = ($0010 + X) & $FF ← 注意: 结果回绕在零页内!
A ← [有效地址]

例: X=$FF, 基地址$10 → 有效地址 = $0F (回绕, 不会进到$0110)

使用场景

场景 示例
访问小型数组 16 字节的状态表 lda states,x
批量清零 lda #0 + 循环 sta $10,x
多路复用 X=通道号, lda ch_volume,x 取各通道音量
1
2
3
4
5
6
7
8
; 实例: 清零 $10-$1F 的 16 个变量
lda #$00
ldx #$00
clear:
sta $10,x ; 内存[$10+X] = 0
inx
cpx #$10 ; 清满16个
bne clear

5. 绝对寻址 Absolute

语法特征:2 字节地址(可访问全部 64K)

1
2
3
4
lda $8000       ; A = 内存$8000 的值 (ROM区)
sta $2007 ; PPU数据口 = A (硬件寄存器)
lda $0600 ; A = 内存$0600 (RAM区, FBASIC变量表)
jmp $C000 ; 跳转到 $C000

原理

1
2
3
4
5
6
指令:      AD 00 80      (机器码: LDA abs)
│ │ └── 地址高字节: $80
│ └───── 地址低字节: $00
└──────── 操作码

执行: A ← [$8000] (完整16位地址, 可达任何位置)

使用场景

场景 示例
访问硬件寄存器 sta $2006 (PPU地址)、sta $4014 (OAM DMA)
访问 RAM 高区 lda $0600 (FBASIC 变量表)
读 ROM 数据表 lda $E000 (字符映射表)
全局变量 不在零页的全局变量
函数调用 jsr $C150 (调用固定地址例程)

对比零页:sta $10(2字节指令,3周期)vs sta $0010(3字节指令,4周期)——效果相同,但零页版更快更省。所以高频变量尽量放零页。


6. 绝对变址 Absolute,X / Absolute,Y

语法:地址,x / 地址,y(地址 ≥ $0100)

1
2
3
4
5
lda $8000,x     ; A = 内存[$8000 + X]
sta $2007,y ; PPU数据 = 内存... 不对, 是 A 写入...
; 正确: 这条是把 A 存到 [$2007+Y]? 不!
; sta $2007,y = 内存[$2007+Y] = A (硬件寄存器会镜像)
lda $0600,x ; A = FBASIC变量表[$600+X]

原理

1
2
3
4
5
6
7
8
9
10
指令:      BD 00 80      (机器码: LDA abs,X)
│ │ └── 基地址高: $80
│ └───── 基地址低: $00
└──────── 操作码

执行: 有效地址 = $8000 + X (16位加法, 可跨页)
A ← [有效地址]

例: X=$FF → 有效地址 = $80FF
X=$100 → 有效地址 = $8100 (不回绕, 与零页变址不同!)

使用场景

场景 示例
读大数组/数据表 lda music_data,x (X=音符序号)
写 OAM 缓冲区 sta $0200,x (精灵数据, X=0-255)
清大块内存 循环 sta $0400,x + sta $0500,x
访问变量表 lda $0600,x (X=变量槽×3)
1
2
3
4
5
6
7
; 实例: 清零 $0200-$02FF (OAM缓冲区)
lda #$00
ldx #$00
clear:
sta $0200,x ; 内存[$0200+X] = 0
inx
bne clear ; X回绕到0前恰好256次

X vs Y 的分工惯例:X 常用于”基地址+偏移”访问数组;Y 常配合间接索引 (zp),y 使用。


7. 间接寻址 Indirect(仅 JMP)

语法:(地址)

1
jmp ($1000)     ; 读内存$1000:$1001 → 得到目标地址 → 跳过去

原理

1
2
3
4
5
6
7
8
9
指令:      6C 00 10      (机器码: JMP (ind))
│ │ └── 指针地址高: $10
│ └───── 指针地址低: $00
└──────── 操作码

执行: 目标 = [$1000] (低字节) + [$1001]×256 (高字节)
PC ← 目标

例: 内存$1000=$50, $1001=$C2 → 跳转到 $C250

使用场景

场景 示例
跳转表/函数指针 主循环按状态跳不同处理函数
可重定位代码 入口地址存在变量里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; 实例: 按游戏状态跳转 (状态机)
lda game_state ; 0=标题 1=游戏 2=结束
asl ; ×2 (每个地址2字节)
tax
lda jump_table,x ; 取目标地址低字节
sta $10
lda jump_table+1,x ; 取高字节
sta $11
jmp ($0010) ; 跳到目标函数

jump_table:
.word title_mode ; 状态0
.word game_mode ; 状态1
.word over_mode ; 状态2

注意:只有 JMP 支持这种寻址。LDA/STA 不能直接用 ($1000),需要用下面的 (zp),Y 方式。


8. 间接索引 Indexed Indirect (zp,X)

语法:(零页地址,x)

1
lda ($10,x)     ; 查"指针表"取值

原理(两步走)

1
2
3
4
5
6
7
8
指令:      A1 10         (机器码: LDA (zp,X))

第1步: 指针地址 = $0010 + X (在零页找指针)
第2步: 目标地址 = [指针地址] + [指针地址+1]×256 (读出16位指针)
第3步: A ← [目标地址]

例: X=4, 内存$0014=$00, $0015=$80
→ 目标地址 = $8000 → A = [$8000]

使用场景

场景 示例
指针数组 一组指针, 按编号取一个并访问
多 bank 数据 各 bank 数据地址存指针表
1
2
3
4
5
6
7
8
9
10
; 实例: 3 个关卡数据, 按编号加载
level_ptrs:
.word level1_data ; $0010:$0011
.word level2_data ; $0012:$0013
.word level3_data ; $0014:$0015

lda level_num ; 0/1/2
asl ; ×2 (指针占2字节)
tax
lda ($10,x) ; A = 对应关卡数据的第1字节

使用频率:较低。大多数情况用 (zp),Y 更方便。


9. 索引间接 Indirect Indexed (zp),Y

⭐ 最灵活的寻址方式,NES 开发必备

语法:(零页地址),y

1
lda ($10),y     ; A = [指针$10指向的地址 + Y]

原理(两步走)

1
2
3
4
5
6
7
8
指令:      B1 10         (机器码: LDA (zp),Y)

第1步: 基地址 = [$0010] + [$0011]×256 (零页$10存着16位指针)
第2步: 有效地址 = 基地址 + Y
第3步: A ← [有效地址]

例: 内存$0010=$00, $0011=$80 (指针=$8000), Y=5
→ 有效地址 = $8005 → A = [$8005]

使用场景

场景 示例
遍历任意数组 指针指向数组头, Y=下标
读字符串 Y 逐字符递增直到遇到 0
VRAM 批量写入 指针指向数据, 循环写 $2007
FBASIC 程序行读取 指针指向当前行, Y=行内偏移
大数组访问 数组超过 256 字节时 (X 变址不够用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
; 实例1: 遍历字符串 (以0结尾)
lda #<msg ; 指针低字节 (#< 取地址低8位)
sta $10
lda #>msg ; 指针高字节 (#> 取地址高8位)
sta $11
ldy #$00
print:
lda ($10),y ; A = msg[Y]
beq done ; 遇到0结束
sta $2007 ; 写PPU显示
iny
bne print ; (假设字符串<256字节)
done:

msg: .byte "HELLO NES", 0

; 实例2: FBASIC 变量表访问 (let.s 的 var_set 用法)
; 指针 ptr1 指向变量槽, 写入3字节
ldy #$00
lda tmp1
sta (ptr1),y ; 槽+0 = 变量名
iny
lda tmp2
sta (ptr1),y ; 槽+1 = 值高
iny
lda tmp3
sta (ptr1),y ; 槽+2 = 值低

为什么最灵活:指针可以指向任何地址(包括运行时计算的地址),Y 提供 0-255 偏移。配合”指针+1 字节”还能访问超过 256 字节的大数组。


10. 相对寻址 Relative(分支跳转)

语法:条件分支指令 + 标号

1
2
3
cpx #$10
bne loop ; 不等则跳回 loop (向前跳)
beq next ; 相等则跳到 next (向后跳)

原理

1
2
3
4
5
6
指令:      D0 FB         (机器码: BNE -5)
│ └── 相对偏移: $FB = -5 (有符号)
└───── 操作码

执行: 若条件成立 → PC = PC + 2 + (-5) (跳回5字节前)
若不成立 → PC = PC + 2 (继续下一条)

范围限制:±127 字节

1
2
3
4
5
6
7
偏移量是1字节有符号数: -128 ~ +127
超出范围 → 汇编器报错 "Range error"

解决方案 (长跳转):
bne @skip
jmp far_target ; 用JMP接力
@skip:

使用场景

场景 示例
循环 inx + bne loop
条件执行 cmp + beq/bne
等待硬件 bit $2002 + bpl wait
错误检查 beq error_handler

分支 vs 跳转:Bxx 是相对寻址(2字节,快,±127限制);JMP 是绝对寻址(3字节,可达任意地址)。短距离用分支,长距离用 JMP。


11. 隐含寻址 Implied

语法:无操作数

1
2
3
4
5
txs             ; S ← X (操作数就是X和S, 不用写)
tax ; X ← A
clc ; C标志 ← 0
rts ; 子程序返回
nop ; 什么都不做 (空转2周期)

原理

1
2
3
4
指令:      9A           (机器码: TXS, 只有1字节!)
└── 操作码 (操作对象隐含: X→S)

执行: S ← X (无需任何地址计算)

使用场景

场景 示例
寄存器间传送 TAX TXA TAY TYA TSX TXS
标志位操作 CLC SEC SEI CLI CLD
栈操作 PHA PLA PHP PLP
流程控制 RTS RTI
空操作/对齐 NOP

特点:指令最短(1字节)、执行最快(2周期)、不需要操作数。


12. 累加器寻址 Accumulator

语法:a(或省略)

1
2
3
4
asl a           ; A 左移1位 (A = A × 2)
lsr a ; A 右移1位 (A = A ÷ 2)
rol a ; A 循环左移
ror a ; A 循环右移

原理

1
2
3
4
指令:      0A           (机器码: ASL A)
└── 操作码 (操作对象就是A寄存器)

执行: C ← [A的bit7], A = A << 1, A的bit0 ← 0

使用场景

场景 示例
乘除 2 的幂 asl a ×2, lsr a ÷2
逐位检测 移位后看 C 标志
位打包/解包 把多个标志压进1字节
1
2
3
4
5
6
7
8
; 实例: A × 8 (左移3次)
asl a
asl a
asl a

; 实例: 检测 A 的 bit0
lsr a ; bit0 → C标志
bcs bit_was_set ; C=1 说明原bit0是1

对比:asl a(移位A寄存器)vs asl $10(移位内存$10的值)——同一操作码家族,不同寻址方式。


13. 场景选择指南

按需求选寻址方式

需求 推荐寻址 示例
装载常量 立即 lda #$0A
访问高频变量 零页 lda $10
访问硬件寄存器 绝对 sta $2006
访问 RAM 高区变量 绝对 lda $0600
遍历 ≤256 字节数组 绝对,X lda $8000,x
遍历零页小数组 零页,X lda $10,x
遍历任意/大数组 (zp),Y lda ($10),y
读字符串 (zp),Y lda ($10),y + iny
函数指针跳转 间接 jmp ($1000)
循环/条件跳转 相对 bne loop
寄存器操作 隐含 tax clc
移位运算 累加器 asl a

性能优先级(同是读内存)

1
零页 (3周期) > 零页,X (4) > 绝对 (4) > 绝对,X (4+) > (zp),Y (5+)

优化原则:

  1. 最热的变量放零页
  2. 小数组放零页用 零页,X
  3. 大数组/动态地址用 (zp),Y(慢一点但灵活)
  4. 循环内指令能短则短

NES 实战中的典型分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; 一段典型 NES 代码用到的寻址方式
lda #$00 ; 立即: 装常量
sta $2001 ; 绝对: 写PPU寄存器
ldx #$00 ; 立即: 初始化X
loop:
lda $0010,x ; 绝对,X: 读OAM缓冲区
sta $0200,x ; 绝对,X: 写另一缓冲区
inx ; 隐含: X+1
bne loop ; 相对: 循环
lda ($10),y ; (zp),Y: 读字符串数据
beq done ; 相对: 条件跳出
sta $2007 ; 绝对: 写VRAM
iny ; 隐含
jmp loop ; 绝对: 长跳转
done:

附:寻址方式 × 指令支持矩阵

并非所有指令都支持所有寻址方式:

指令 立即 零页 零页,X 绝对 绝对,X 绝对,Y (zp,X) (zp),Y
LDA ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
STA ❌ ✅ ✅ ✅ ✅ ✅ ✅ ✅
LDX ✅ ✅ ❌ ✅ ❌ ✅* ❌ ❌
STX ❌ ✅ ❌ ✅ ❌ ❌ ❌ ❌
ADC ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
CMP ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
INC ❌ ✅ ✅ ✅ ✅ ❌ ❌ ❌
ASL ❌ ✅ ✅ ✅ ✅ ❌ ❌ ❌
JMP ❌ ❌ ❌ ✅ ❌ ❌ ❌ ❌
JSR ❌ ❌ ❌ ✅ ❌ ❌ ❌ ❌

* LDX abs,Y 存在但少用

完整矩阵见 6502 指令集参考