6502 寻址方式详解
寻址方式 = CPU 如何找到指令的操作数(数据在哪里)
同一条指令(如 LDA)配合不同寻址方式,含义完全不同。掌握寻址方式是读懂 6502 汇编的关键。
目录
- 总览对比表
- 立即寻址 Immediate
- 零页寻址 Zero Page
- 零页变址 Zero Page,X / Zero Page,Y
- 绝对寻址 Absolute
- 绝对变址 Absolute,X / Absolute,Y
- 间接寻址 Indirect(仅 JMP)
- 间接索引 Indexed Indirect (zp,X)
- 索引间接 Indirect Indexed (zp),Y
- 相对寻址 Relative(分支跳转)
- 隐含寻址 Implied
- 累加器寻址 Accumulator
- 场景选择指南
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
语法特征:# 号
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 sta $20 inc $30 lda $00
|
原理
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 sta $20,x ldx #$03 lda $10,x
|
原理
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
| lda #$00 ldx #$00 clear: sta $10,x inx cpx #$10 ; 清满16个 bne clear
|
5. 绝对寻址 Absolute
语法特征:2 字节地址(可访问全部 64K)
1 2 3 4
| lda $8000 sta $2007 lda $0600 jmp $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 sta $2007,y lda $0600,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
| lda #$00 ldx #$00 clear: sta $0200,x inx bne clear
|
X vs Y 的分工惯例:X 常用于”基地址+偏移”访问数组;Y 常配合间接索引 (zp),y 使用。
7. 间接寻址 Indirect(仅 JMP)
语法:(地址)
原理
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 asl tax lda jump_table,x sta $10 lda jump_table+1,x sta $11 jmp ($0010)
jump_table: .word title_mode .word game_mode .word over_mode
|
注意:只有 JMP 支持这种寻址。LDA/STA 不能直接用 ($1000),需要用下面的 (zp),Y 方式。
8. 间接索引 Indexed Indirect (zp,X)
语法:(零页地址,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
| level_ptrs: .word level1_data .word level2_data .word level3_data
lda level_num asl tax lda ($10,x)
|
使用频率:较低。大多数情况用 (zp),Y 更方便。
9. 索引间接 Indirect Indexed (zp),Y
⭐ 最灵活的寻址方式,NES 开发必备
语法:(零页地址),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
| lda #<msg ; 指针低字节 (#< 取地址低8位) sta $10 lda #>msg ; 指针高字节 (#> 取地址高8位) sta $11 ldy #$00 print: lda ($10),y beq done sta $2007 iny bne print done:
msg: .byte "HELLO NES", 0
ldy #$00 lda tmp1 sta (ptr1),y iny lda tmp2 sta (ptr1),y iny lda tmp3 sta (ptr1),y
|
为什么最灵活:指针可以指向任何地址(包括运行时计算的地址),Y 提供 0-255 偏移。配合”指针+1 字节”还能访问超过 256 字节的大数组。
10. 相对寻址 Relative(分支跳转)
语法:条件分支指令 + 标号
1 2 3
| cpx #$10 bne loop beq 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
| 指令: 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 lsr a rol a ror 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
| asl a asl a asl a
lsr a bcs bit_was_set
|
对比: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+)
|
优化原则:
- 最热的变量放零页
- 小数组放零页用 零页,X
- 大数组/动态地址用 (zp),Y(慢一点但灵活)
- 循环内指令能短则短
NES 实战中的典型分布
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| lda #$00 ; 立即: 装常量 sta $2001 ldx #$00 ; 立即: 初始化X loop: lda $0010,x sta $0200,x inx bne loop lda ($10),y beq done sta $2007 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 指令集参考