Assembly 101: Explaining addressing modes using LDA as an example

LDA is the assembly instruction to load a value into the accumulator. How does it work exactly, and which options are available with this instruction?

The accumulator (from now on referred to as A) can hold one byte (a.k.a. eight bits, a.k.a a hexadecimal value from 0 to FF) worth of data. The LDA operation is what puts data into A. There are various ways this instruction can be applied. I will explain which ways there are, with an example per option.

Immediate

The simplest variation of loading data into the A is immediate addressing mode. Here you just put an actual value into A.

LDA #$44  ;; Load the hexadecimal number 44 (or 68 in decimal) into the accumulator

Now, the value of A will be #$44. The pound sign (#) is what tells the LDA instruction to interpret the hex value $44 as the actual number to be loaded. Omitting the pound sign is a common pitfall for NESMaker developers and will result in unexpected behavior of your game, so whenever your compiled ROM seems to be acting up, a missing (or superfluous) # might be the cause of your problems.

Zero Page

You don’t always need a static value to be loaded into A. For example, if you want to load the player’s x-coordinate on screen into A, which isn’t always #$44 obviously. Fortunately, you can also load a variable value into A. The fastest variables to load into A are those that are stored in the zero page. That’s why we call this method zero page addressing.

LDA $44   ;; Load the number that is stored in zero page address $44 (or $0044 in full)

This instruction will look into memory which value is stored at address $44, and loads that value into the accumulator. In this screenshot, which shows the CPU memory – specifically, the zero page memory – you’ll find that the value of memory address $44 is #$20:

Zero page, indexed

Another method to load a value into A, is the indexed zero page addressing method. This is pretty much like regular zero page addressing, except it uses the value of the X- or Y-register as an offset.

LDX #$08   ;; Load a value of 8 into the X-register
LDA $44,X  ;; Load the value that's X positions after memory address $44 into the accumulator

What it basically does, is this: Go to address $44 in memory, go X (or 8 in this example) steps further and return that value. Looking at the previous memory map screenshot, this means it returns the value from memory address $4C ($44 + $08), which is #$13.

Additionally, you can also use LDA $44,Y which uses the Y-register as the offset value, instead of the X-register. Also note that this method does not use the carry; when adding the X or Y value results in an address over $FF (e.g. it overflows), it will wrap around in the zero page.

Absolute addressing and indexed absolute addressing

Indexed addressing as described above does not only apply to the zero page. Absolute addressing works with basically any address that’s present in CPU memory. You can load variables from other spots in memory the same way as well, by using the full address instead of a single byte value.

LDX #$08     ;; Load a value of 8 into the X-register
LDA $4400,X  ;; Load the value that's X positions after memory address $4400 (i.e. $4408) into the accumulator

Above is an example of indexed absolute addressing, which works the same as the zero page example mentioned before. This method takes up an extra byte in assembled code and may also take one extra CPU cycle if the offset crosses a page boundary.

Indirect addressing

Most NESMaker developers won’t probably need this method very often, but if you have been snooping around NESMaker system script files, you may have encountered something that looks like this:

LDY #$08     ;; load a value of 8 into the Y-register
LDA ($1B),Y  ;; indirect addressing, explained below

This is probably the most complex type of addressing called indirect addressing and works as follows: take the values from address $1B and $1C, which together form a two byte address. In the example screenshot, these values are #$4A and #$8D, resulting in an address of $8D4A*. Go to this address, then go Y steps forward (which is 8; this results in address $8D52), and return the value present at that address.

* updated (29 december 2022) – this uses Little Endian addressing, which means: low byte first, high byte after.

Another example of indirect addressing is this:

LDX #$08    ;; load a value of 8 into the X-register
LDA ($44,X) ;; indirect addressing, explained below

This method is a bit different from the former example and works as follows: go to address $44, go forward X steps (which is 8 in this example) and get the value at that address ($4C) and the next one ($4D). These two values form the memory address from which to load the value into A. Looking at the screenshot, address $4C has a value of #$13 and $4D has a value of #$98, so the value at memory address $9813 will be loaded into A.

Implied/implicit and absolute indirect addressing

There are two more addressing modes: implied/implicit addressing (where the operand is implicitly defined by the instruction, for example INX, which increments the X-registers value by one) and absolute indirect addressing (only used for the jump instruction: JMP ($44) takes the value of memory address $44 and $45, jumps to the address that’s formed by those two values, and then jumps there). These modes are not used with LDA though.

Conclusion

So there you have it! These are all the addressing modes available to you when you’re writing assembly code.