020. Lda indirect x

Add indexed-indirect LDA ($A1, written `(d,X)`).

Lesson 20 of 356 · tests/chapter_01_cpu/test_020_LDA_indirect_x.py

Files to update

emulator/cpu/addressing_modes.py
emulator/cpu/opcodes.py

Locations

addressing_modes.indirect_x
opcodes.lda_indirect_x
opcodes.OPCODE_TABLE[$A1]

Why this step exists

Indexed-indirect addressing uses the operand plus X to select a two-byte pointer in zero page. The pointer then supplies the final 16-bit address of the LDA value.

Complete example implementation

# emulator/cpu/addressing_modes.py
def indirect_x(cpu) -> int:
    operand = cpu.fetch_byte()
    pointer = (operand + cpu.x) & 0xFF

    low = cpu.bus.read(pointer)
    high = cpu.bus.read((pointer + 1) & 0xFF)

    return low | (high << 8)


# emulator/cpu/opcodes.py
from emulator.cpu.addressing_modes import indirect_x


def lda_indirect_x(cpu) -> None:
    address = indirect_x(cpu)
    lda(cpu, cpu.bus.read(address))


OPCODE_TABLE = {
    # Preserve existing entries.
    0xA1: lda_indirect_x,
}

Address timeline for A1 20 with X=$04:

fetch operand $20
    -> pointer location $24
    -> read low byte from $0024
    -> read high byte from $0025
    -> assemble final address
    -> read value for LDA

Important invariants

  • X is added before reading the pointer
  • pointer selection wraps within zero page
  • the high-byte read from pointer $FF wraps to $00

Common misconception

Do not add X to the final 16-bit address. That is a different addressing mechanism; (d,X) indexes the zero-page pointer location.

Out of scope

  • indirect,Y, introduced in Test 021
  • page-cross cycle penalties
  • JMP's distinct indirect behavior

Run this lesson

uv run pytest tests/chapter_01_cpu/test_020_LDA_indirect_x.py -v