Understanding 6502 assembly on the Commodore 64 - (25) Address modes

We've steered of course long enough.  Its time to get back to grass roots.  One of these days I will get all of these chapters in order because addressing modes should definitely be in the first 10.

     The addressing modes are a fundamental part of 6502 programming,  even though we have been using several different addressing modes, we haven't stopped to discuss them. This chapter will be dedicated to them.


    This would be the easiest to understand.  Not every instruction references and address.  We have used many in this fashion. Note that in our programs, nothing comes after these instructions. They sit alone, the processor will not expect any value after receiving these opcodes.

     The syntax constitutes a single BYTE instruction only.

00 - BRK
60 - RTS
E8 - INX
EA - NOP (of course)

     I'm not going to list all of them, but you get the point. We can group these in a type of instruction that doesn't address a memory location of any type.  This is not entirely true, but on the surface for our purpose, this is the case.  No memory address is referenced after the instruction.

     Consider TAX transfers the value of A to X.  The movement of the BYTE to the associated location is implied in the opcode itself.  INX, ROL, and other opcodes will most definitely have an effect of the stored value in their registers.

E8 - INX - would effect the value in X
A8 - TAY - would effect the value in Y
2A - ROL - would effect the value in A

Note an exception here, ROL in this mode is 2A.  ROL can exist in other modes but the hex value wont be 2A.  ROL and other opcodes have multiple Hex values based on the mode they use.


     This is another one we've ofter used.  Consider if we want to load a value into A.  That value would not exceed one byte, as the maximum value that came be stored in A,X or Y is 8 bits.  This means anything from $00 to $FF. So were looking at a one byte opcode, and a one byte value.

A9 55 - LDA #$55

Here we have LDA, which obviously takes a argument...........  note the # before the value denotes an actual value and not a memory address.

LOAD A..... Load A with what? LDA can not exist alone, an argument is expected.  In this instance we load A with $55.  Sure LDA can have more than one addressing mode, in this instance, with one byte it is immediately addressed.
Again A9 is LDA when using this mode, as such, A9 will only expect 1 byte after it.  From the programmer perspective, LDA #$55 will compile and produce A9 55 becuause the compiler does that work for us. But its good to know.


    Just like Jim did, were going to go out of order here (size wise) the reason will become apparent.   In absolute mode we reference a NON zero page address. Not a value!!!!! an address. Consider the following......

AD 00 0C - LDA $0C00

     In this mode we reference a 16 BIT memory address, and in this case we load the contents of 0C00 into the A register.  Just the same, STA 0C00 would store the value in A to $0C00.  We see in this case that LDA is not A9 but AD.

8D 00 0C - STA $0C00

     Remember that the values in hex are little Endian, hence 00 0C and not 0C00.  In both cases, AD and 8D will expect 2 bytes after it.


    Just like absolute mode, zero page mode references a 16 bit memory address, however it does so with only 8 bits.  It does so only because the processor was designed to forgo the first two zeros, as discussed in Chapter 15 - the zero page. This naturally save program size, and processor cycles. 

In the case of......

It would be wasteful and unnecessary, and by many compilers, not allowed to:

AD CE 00 - LDA $00CE   (this was personally painful to write)

when you could simply do:


     Now it's time to go over the next group, the indexed group.  It is permissible to allow the value after the opcode to be affected by a value in one of the other existing registers. We will go over these as well.  You will be surprised (or not surprised) to note that we have emplyed these too in previous code.

      There are rules associated with the following modes, so they are not entirely straight forward, however the concept is easily understood.  we will start with absolute indexed.  Consider the following scenartio where X=05:

BD 00 C0 - LDA 0C00,X

     The first thing you might notice is the fact that with the added information our line is still 3 bytes long.  If you notice the change in the hex for the opcode is now BD.  It is BD that dictates that the value will be indexed by X.  Consider if we were doing the same thing but indexing with Y.

B9 00 C0 - LDA 0C00,Y

Getting back though, in our initial example with X having the value of 5:

BD 00 C0 - LDA 0C00,X

     The effective value would load the value of $0C05 into A.  There's no trickery here, its quite easy to understand, there are however some limitations.  The first should be painfully obvious. X can only hold a value between 00 and FF, so from our initial value, the furthest you could index would be 0CFF. Also, there is no indexing backwards, that is to say there is no negative X, only forwards. 

    Another thing to consider is wraparound.  If the index is such that the final value would exceed FFFF, the count will wrap around to 0000 and keep going.

Consider where X = 5


     The address you will arrive at would be 0003.  Another thing worth noting that should the value produced by indexing leave the current 256 byte memory page boundary, it will come at the cost of an extra processor cycle.  Lastly, different opcodes support different registers as an index, not all will support X, or Y or A or a combination of the registers.



     Very much the same as absolute indexed mode.  Where X =5:


This would result in an address of D3.  One important note for zero page is that any attempts to exceed memory address FF will cause a wraparound to 0000.  For instance, where X = 5:


Will result in a memory location of 03.  The jury is still out on weather this is a feature or a bug.  The wraparound at FFFF is obvious but the one at 00FF, is somewhat less obvious.  it is however possible to use absolute indexed mode on the zero page area to avoid this wraparound as shown below, where X = 5:

BD FE 00 - LDA 00FE,X

The result this time is address 0103.


      This mode only exists with JMP, so you will not find it associated with any other opcode.  Its a bit weird, but easy to understand, and can be quite useful.


4C 00 0C - JMP 0C00

The program would jump to 0C00 and continue execution, however:

6C 00 0C - JMP (0C00)

Now we have something entirely different!!!!!

     This is entirely dependant on the values located in memory addresses 0C00 and 0C01. So were going to have to assume some values here. Keeping in mind that the value is little endian.  So if:

0C00 has $22
0C01 has $EA

6C 00 0C - JMP (0C00) will take us to address EA22.

     There is, as you might expect a pitfall here to watch out for.  Never use the last byte of a memory page for this instruction, for example:

6C FF 0C - JMP (0CFF)

     This will not use 0CFF and 0D00!!! it will use 0CFF and 0C00, another wraparound to watch out for. Just avoid it.  Just a note, this jump was used by the kernel for our irq vector at $0314


     This mode represents a combination of the technique found in indirect absolute mode and combines it with indexed mode. Where Y = 5, EA = 11 and EB = 22:

B1 EA - LDA ($EA),Y

     We will get 11 and 22 from EA for a value of $2211 and then add 5 to it for a final memory location of $2216.


     This one is similar to indirect indexed mode, but the order of operation is different.  Consider the following where X = 5, EF = 11 and F0 = 22:

A1 EA - LDA ($EA,X)

     We will  add X to EA to arrive at a value of $EF.  If we look at the values in EF and F0 we get 11 and 22 for a final address of $2211

     We've used this one quite often, this address mode is associated with branching.  It is special because it is a signed number which allows if to go to a memory address in either direction.

   Consider that we have a label called OURROUTINE.  and A is #$05.
We could say:

C9 05 - CMP #$05

What we are actually doing is going forwards or backwards to where OURROUTINE may be.  6502 doesnt understand OURROUTINE, but the compiler does.

If for example,

CMP #$05 was at C005 and C006 (remember 2 bytes) and...
BEQ OURROUTINE was at C007 and C008 (also 2 bytes) and....
OURROUTINE was located at C00E.....

The jump would be 6 bytes, and calculated by our compiler as

C9 05 - CMP #$05

Remember however that this value is a signed number, and instead of a range of 0 to 256 we have a range of -128 to +127.  While gaining the ability to go forwads and backwards, we are only able to branch a maximum of 128 bytes in each direction.

To give an idea how this would work:

01 is +1
30 is +48
50 is +80
7F is +127

80 is -128
81 is - 127
82 is -126
F7 is -9
FC is -4
FF is -1

Understanding 6502 assembly on the Commodore 64 - (26) Intro to unsigned mathematics

Table of contents

No comments:

Post a Comment