rustynes_cpu/
addressing.rs

1//! CPU Addressing Modes
2//!
3//! The 6502 has 13 addressing modes that determine how instructions access memory.
4//! Each mode has specific cycle timings and page-crossing behavior.
5
6use crate::bus::Bus;
7
8/// CPU Addressing Modes
9///
10/// Defines how an instruction's operand is accessed.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum AddressingMode {
13    /// No operand (instruction operates on CPU state)
14    ///
15    /// Example: `NOP`, `CLC`, `DEX`
16    Implied,
17
18    /// Operand is the accumulator register
19    ///
20    /// Example: `ASL A`, `ROL A`
21    Accumulator,
22
23    /// Operand is the next byte after the opcode
24    ///
25    /// Example: `LDA #$42` loads the value $42
26    Immediate,
27
28    /// Operand is in zero page ($00-$FF)
29    ///
30    /// Example: `LDA $80` reads from address $0080
31    ZeroPage,
32
33    /// Zero page address + X register (wraps within zero page)
34    ///
35    /// Example: `LDA $80,X` with X=$05 reads from $0085
36    ZeroPageX,
37
38    /// Zero page address + Y register (wraps within zero page)
39    ///
40    /// Example: `LDX $80,Y` with Y=$05 reads from $0085
41    ZeroPageY,
42
43    /// Operand is a 16-bit absolute address
44    ///
45    /// Example: `LDA $1234` reads from address $1234
46    Absolute,
47
48    /// Absolute address + X register
49    ///
50    /// Example: `LDA $1234,X` with X=$10 reads from $1244
51    AbsoluteX,
52
53    /// Absolute address + Y register
54    ///
55    /// Example: `LDA $1234,Y` with Y=$10 reads from $1244
56    AbsoluteY,
57
58    /// Indirect addressing (JMP only)
59    ///
60    /// Example: `JMP ($1234)` reads target address from $1234-$1235
61    Indirect,
62
63    /// Indexed Indirect: (Zero Page + X), then dereference
64    ///
65    /// Example: `LDA ($80,X)` with X=$05:
66    /// 1. Calculate pointer address: $80 + $05 = $85
67    /// 2. Read pointer: [$85] = $20, [$86] = $30
68    /// 3. Read from $3020
69    IndexedIndirectX,
70
71    /// Indirect Indexed: Dereference Zero Page, then + Y
72    ///
73    /// Example: `LDA ($80),Y` with Y=$10:
74    /// 1. Read pointer: [$80] = $20, [$81] = $30
75    /// 2. Add Y: $3020 + $10 = $3030
76    /// 3. Read from $3030
77    IndirectIndexedY,
78
79    /// Relative offset for branch instructions
80    ///
81    /// Example: `BNE $02` branches PC + 2 bytes forward
82    Relative,
83}
84
85impl AddressingMode {
86    /// Get the number of operand bytes for this addressing mode
87    ///
88    /// # Returns
89    ///
90    /// - 0 bytes: Implied, Accumulator
91    /// - 1 byte: Immediate, Zero Page variants, Indexed Indirect, Indirect Indexed, Relative
92    /// - 2 bytes: Absolute variants, Indirect
93    #[inline]
94    #[must_use]
95    pub const fn operand_bytes(self) -> u8 {
96        match self {
97            Self::Implied | Self::Accumulator => 0,
98            Self::Immediate
99            | Self::ZeroPage
100            | Self::ZeroPageX
101            | Self::ZeroPageY
102            | Self::IndexedIndirectX
103            | Self::IndirectIndexedY
104            | Self::Relative => 1,
105            Self::Absolute | Self::AbsoluteX | Self::AbsoluteY | Self::Indirect => 2,
106        }
107    }
108
109    /// Check if this addressing mode can have a page crossing penalty
110    ///
111    /// # Returns
112    ///
113    /// `true` for:
114    /// - Absolute,X
115    /// - Absolute,Y
116    /// - (Indirect),Y
117    /// - Relative (branches)
118    #[inline]
119    #[must_use]
120    pub const fn can_page_cross(self) -> bool {
121        matches!(
122            self,
123            Self::AbsoluteX | Self::AbsoluteY | Self::IndirectIndexedY | Self::Relative
124        )
125    }
126}
127
128/// Addressing mode resolution result
129///
130/// Contains the effective address and whether a page boundary was crossed
131#[derive(Debug, Clone, Copy)]
132pub struct AddressResult {
133    /// Effective memory address (or operand value for Immediate mode)
134    pub addr: u16,
135
136    /// Whether a page boundary was crossed during address calculation
137    ///
138    /// Page crossing adds +1 cycle for read operations in certain modes
139    pub page_crossed: bool,
140
141    /// Base address before index addition (for dummy read calculation)
142    ///
143    /// Used to calculate the incorrect address for dummy reads during
144    /// indexed addressing with page crossing
145    pub base_addr: u16,
146}
147
148impl AddressResult {
149    /// Create a new address result without page crossing
150    #[inline]
151    #[must_use]
152    pub const fn new(addr: u16) -> Self {
153        Self {
154            addr,
155            page_crossed: false,
156            base_addr: 0,
157        }
158    }
159
160    /// Create a new address result with page crossing information
161    #[inline]
162    #[must_use]
163    pub const fn with_page_cross(addr: u16, page_crossed: bool) -> Self {
164        Self {
165            addr,
166            page_crossed,
167            base_addr: 0,
168        }
169    }
170
171    /// Create a new address result with base address for dummy read calculation
172    #[inline]
173    #[must_use]
174    pub const fn with_base_and_cross(addr: u16, base_addr: u16, page_crossed: bool) -> Self {
175        Self {
176            addr,
177            page_crossed,
178            base_addr,
179        }
180    }
181}
182
183/// Check if two addresses are on different pages
184///
185/// A page is 256 bytes, so addresses differ by page if their high bytes differ.
186#[inline]
187#[must_use]
188pub const fn page_crossed(addr1: u16, addr2: u16) -> bool {
189    (addr1 & 0xFF00) != (addr2 & 0xFF00)
190}
191
192/// Addressing mode implementations
193///
194/// These methods are called during instruction execution to resolve operand addresses.
195impl AddressingMode {
196    /// Resolve the effective address for this addressing mode
197    ///
198    /// # Arguments
199    ///
200    /// * `pc` - Current program counter (AFTER opcode fetch)
201    /// * `x` - X register value
202    /// * `y` - Y register value
203    /// * `bus` - Memory bus for reading operands
204    ///
205    /// # Returns
206    ///
207    /// `AddressResult` containing the effective address and page crossing status
208    ///
209    /// # Notes
210    ///
211    /// - For `Immediate`, `addr` is the value itself
212    /// - For `Accumulator` and `Implied`, `addr` is unused
213    /// - PC should point to the first operand byte
214    pub fn resolve(self, pc: u16, x: u8, y: u8, bus: &mut impl Bus) -> AddressResult {
215        match self {
216            Self::Implied | Self::Accumulator => AddressResult::new(0),
217
218            Self::Immediate => AddressResult::new(pc),
219
220            Self::ZeroPage => {
221                let addr = bus.read(pc) as u16;
222                AddressResult::new(addr)
223            }
224
225            Self::ZeroPageX => {
226                let base = bus.read(pc);
227                // Wraps within zero page
228                let addr = base.wrapping_add(x) as u16;
229                AddressResult::new(addr)
230            }
231
232            Self::ZeroPageY => {
233                let base = bus.read(pc);
234                // Wraps within zero page
235                let addr = base.wrapping_add(y) as u16;
236                AddressResult::new(addr)
237            }
238
239            Self::Absolute => {
240                let addr = bus.read_u16(pc);
241                AddressResult::new(addr)
242            }
243
244            Self::AbsoluteX => {
245                let base = bus.read_u16(pc);
246                let addr = base.wrapping_add(x as u16);
247                let crossed = page_crossed(base, addr);
248                AddressResult::with_base_and_cross(addr, base, crossed)
249            }
250
251            Self::AbsoluteY => {
252                let base = bus.read_u16(pc);
253                let addr = base.wrapping_add(y as u16);
254                let crossed = page_crossed(base, addr);
255                AddressResult::with_base_and_cross(addr, base, crossed)
256            }
257
258            Self::Indirect => {
259                let ptr = bus.read_u16(pc);
260                // JMP indirect has a page-wrap bug
261                let addr = bus.read_u16_wrap(ptr);
262                AddressResult::new(addr)
263            }
264
265            Self::IndexedIndirectX => {
266                let base = bus.read(pc);
267                // Zero page pointer + X (wraps)
268                let ptr = base.wrapping_add(x);
269
270                // Read 16-bit address from zero page (wraps)
271                let lo = bus.read(ptr as u16) as u16;
272                let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
273                let addr = (hi << 8) | lo;
274
275                AddressResult::new(addr)
276            }
277
278            Self::IndirectIndexedY => {
279                let ptr = bus.read(pc);
280
281                // Read base address from zero page (wraps)
282                let lo = bus.read(ptr as u16) as u16;
283                let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
284                let base = (hi << 8) | lo;
285
286                // Add Y to base address
287                let addr = base.wrapping_add(y as u16);
288                let crossed = page_crossed(base, addr);
289
290                AddressResult::with_base_and_cross(addr, base, crossed)
291            }
292
293            Self::Relative => {
294                // Read signed offset
295                let offset = bus.read(pc) as i8;
296                // PC has already advanced past the branch instruction
297                let base = pc.wrapping_add(1);
298                let addr = base.wrapping_add(offset as u16);
299
300                let crossed = page_crossed(base, addr);
301                AddressResult::with_page_cross(addr, crossed)
302            }
303        }
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    struct TestBus {
312        ram: [u8; 0x10000],
313    }
314
315    impl Bus for TestBus {
316        fn read(&mut self, addr: u16) -> u8 {
317            self.ram[addr as usize]
318        }
319
320        fn write(&mut self, addr: u16, value: u8) {
321            self.ram[addr as usize] = value;
322        }
323    }
324
325    #[test]
326    fn test_page_crossed_same_page() {
327        assert!(!page_crossed(0x1234, 0x1256));
328    }
329
330    #[test]
331    fn test_page_crossed_different_page() {
332        assert!(page_crossed(0x12FF, 0x1300));
333    }
334
335    #[test]
336    fn test_immediate() {
337        let mut bus = TestBus { ram: [0; 0x10000] };
338        let result = AddressingMode::Immediate.resolve(0x1000, 0, 0, &mut bus);
339        assert_eq!(result.addr, 0x1000);
340        assert!(!result.page_crossed);
341    }
342
343    #[test]
344    fn test_zero_page() {
345        let mut bus = TestBus { ram: [0; 0x10000] };
346        bus.write(0x1000, 0x42);
347
348        let result = AddressingMode::ZeroPage.resolve(0x1000, 0, 0, &mut bus);
349        assert_eq!(result.addr, 0x0042);
350    }
351
352    #[test]
353    fn test_zero_page_x_wrap() {
354        let mut bus = TestBus { ram: [0; 0x10000] };
355        bus.write(0x1000, 0xFF);
356
357        // $FF + $05 = $04 (wraps within zero page)
358        let result = AddressingMode::ZeroPageX.resolve(0x1000, 0x05, 0, &mut bus);
359        assert_eq!(result.addr, 0x0004);
360    }
361
362    #[test]
363    fn test_absolute() {
364        let mut bus = TestBus { ram: [0; 0x10000] };
365        bus.write(0x1000, 0x34);
366        bus.write(0x1001, 0x12);
367
368        let result = AddressingMode::Absolute.resolve(0x1000, 0, 0, &mut bus);
369        assert_eq!(result.addr, 0x1234);
370    }
371
372    #[test]
373    fn test_absolute_x_no_page_cross() {
374        let mut bus = TestBus { ram: [0; 0x10000] };
375        bus.write(0x1000, 0x00);
376        bus.write(0x1001, 0x12);
377
378        let result = AddressingMode::AbsoluteX.resolve(0x1000, 0x10, 0, &mut bus);
379        assert_eq!(result.addr, 0x1210);
380        assert!(!result.page_crossed);
381    }
382
383    #[test]
384    fn test_absolute_x_page_cross() {
385        let mut bus = TestBus { ram: [0; 0x10000] };
386        bus.write(0x1000, 0xFF);
387        bus.write(0x1001, 0x12);
388
389        let result = AddressingMode::AbsoluteX.resolve(0x1000, 0x01, 0, &mut bus);
390        assert_eq!(result.addr, 0x1300);
391        assert!(result.page_crossed);
392    }
393
394    #[test]
395    fn test_indexed_indirect_x() {
396        let mut bus = TestBus { ram: [0; 0x10000] };
397
398        // Instruction operand: $80
399        bus.write(0x1000, 0x80);
400
401        // Pointer at ($80 + X) = $85:  [$85] = $20, [$86] = $30
402        bus.write(0x0085, 0x20);
403        bus.write(0x0086, 0x30);
404
405        let result = AddressingMode::IndexedIndirectX.resolve(0x1000, 0x05, 0, &mut bus);
406        assert_eq!(result.addr, 0x3020);
407    }
408
409    #[test]
410    fn test_indirect_indexed_y_no_page_cross() {
411        let mut bus = TestBus { ram: [0; 0x10000] };
412
413        // Instruction operand: $80
414        bus.write(0x1000, 0x80);
415
416        // Pointer at $80: [$80] = $00, [$81] = $30
417        bus.write(0x0080, 0x00);
418        bus.write(0x0081, 0x30);
419
420        let result = AddressingMode::IndirectIndexedY.resolve(0x1000, 0, 0x10, &mut bus);
421        assert_eq!(result.addr, 0x3010);
422        assert!(!result.page_crossed);
423    }
424
425    #[test]
426    fn test_indirect_indexed_y_page_cross() {
427        let mut bus = TestBus { ram: [0; 0x10000] };
428
429        // Instruction operand: $80
430        bus.write(0x1000, 0x80);
431
432        // Pointer at $80: [$80] = $FF, [$81] = $30
433        bus.write(0x0080, 0xFF);
434        bus.write(0x0081, 0x30);
435
436        let result = AddressingMode::IndirectIndexedY.resolve(0x1000, 0, 0x01, &mut bus);
437        assert_eq!(result.addr, 0x3100);
438        assert!(result.page_crossed);
439    }
440
441    #[test]
442    fn test_relative_forward_same_page() {
443        let mut bus = TestBus { ram: [0; 0x10000] };
444
445        // PC at $1000, offset = +$10
446        bus.write(0x1000, 0x10);
447
448        let result = AddressingMode::Relative.resolve(0x1000, 0, 0, &mut bus);
449        // Base = PC + 1 = $1001, target = $1001 + $10 = $1011
450        assert_eq!(result.addr, 0x1011);
451        assert!(!result.page_crossed);
452    }
453
454    #[test]
455    fn test_relative_forward_page_cross() {
456        let mut bus = TestBus { ram: [0; 0x10000] };
457
458        // PC at $10F0, offset = +$20
459        bus.write(0x10F0, 0x20);
460
461        let result = AddressingMode::Relative.resolve(0x10F0, 0, 0, &mut bus);
462        // Base = $10F1, target = $10F1 + $20 = $1111
463        assert_eq!(result.addr, 0x1111);
464        assert!(result.page_crossed);
465    }
466
467    #[test]
468    fn test_relative_backward() {
469        let mut bus = TestBus { ram: [0; 0x10000] };
470
471        // PC at $1050, offset = -$10 (0xF0 as unsigned)
472        bus.write(0x1050, 0xF0);
473
474        let result = AddressingMode::Relative.resolve(0x1050, 0, 0, &mut bus);
475        // Base = $1051, target = $1051 - $10 = $1041
476        assert_eq!(result.addr, 0x1041);
477        assert!(!result.page_crossed);
478    }
479}