Products > Programming

6502 addressing modes: VERY confused.

(1/4) > >>

eti:
Hi all.

I've finally got my mind around the first principles of how the 6502 works, but am stuck on one part; the "addressing modes". As usual with YouTube, there's no consistency in videos... we see all the "retro" channels showing off restorations, we see poorly explained "tutorials" which are either in barely understandable broken English, or... well, you know how frustrating it all is.

When you don't know something and are desperate for someone to explain what it does, one's thoughts are fragile in form, and wading through blog after blog, video after video is an exercise in frustration and futility. I merely want to know how AND WHY these various 6502 addressing modes exist, please.

The closest I've got is this SUPERB video from "Chaos Computer Club" on the 6502; the trouble is, the addressing modes section didn't seem long or thorough enough to me:




Help! Please?  Thanks.

ledtester:
Do you have a specific question about the addressing modes?

This page is pretty good at explaining things:

http://www.emulator101.com/6502-addressing-modes.html

The syntax can also be helpful if you understand the patterns behind it.

A bare address just means access that memory location, e.g.:

LDA $1234
STX $56

means load accumulator from address 0x1234 and store X into zero-page location $56.

A comma means to add the X or Y register to the address, e.g.:

LDA $1234,X
LDX $56,Y

means to load A from the address 0x1234+X and to load X from the address 0x0056+Y. Note that in this last case the addition 0x0056+Y is done mod 256 so the result is always in the "zero page" (first 256 bytes of memory)

Parenthesis around an address means indirection, e.g.:

JMP ($4000)

means to set the program counter to the contents of addresses 0x4000 and 0x4001. By contrast the instruction "JMP $4000" means to set the program counter to 0x4000.

Now there are two ways to combine the comma and paren operators:

LDA ($20,X)
STA ($20),Y

In the first case you add $20+X (mod 256) to get a zero page address. Then the contents of that zero page location and the next one are fetched to create a 16-address from which data is fetched to be stored in the accumulator. In other words, you do the comma first and then the indirection.

In the second case you look at the contents of 0x0020 and 0x0021 to create a 16-bit memory address. Then the Y register is added to this address to give the address to which the accumulator is stored. Here you do the indirection first and then the comma.

Another way to look at things...

in "LDA ($20,X)" you have set up a bunch of addresses in zero page, e.g. the locations 0x0020 and 0x0021 hold a 16-bit address, the locations 0x0022, 0x0023 hold another 16-bit address, etc. With X set to 0 you will be indirecting through the first address; with X set to 2 you will be indirecting through the second address.

in "STA ($20),Y", the locations 0x0020 and 0x0021 hold a 16-bit address and Y is an offset to that address. This is a much more conventional way of using an index value.



eti:

--- Quote from: ledtester on September 28, 2022, 03:32:43 am ---Do you have a specific question about the addressing modes?

This page is pretty good at explaining things:

http://www.emulator101.com/6502-addressing-modes.html

The syntax can also be helpful if you understand the patterns behind it.

A bare address just means access that memory location, e.g.:

LDA $1234
STX $56

means load accumulator from address 0x1234 and store X into zero-page location $56.

A comma means to add the X or Y register to the address, e.g.:

LDA $1234,X
LDX $56,Y

means to load A from the address 0x1234+X and to load X from the address 0x0056+Y. Note that in this last case the addition 0x0056+Y is done mod 256 so the result is always in the "zero page" (first 256 bytes of memory)

Parenthesis around an address means indirection, e.g.:

JMP ($4000)

means to set the program counter to the contents of addresses 0x4000 and 0x4001. By contrast the instruction "JMP $4000" means to set the program counter to 0x4000.

Now there are two ways to combine the comma and paren operators:

LDA ($20,X)
STA ($20),Y

In the first case you add $20+X (mod 256) to get a zero page address. Then the contents of that zero page location and the next one are fetched to create a 16-address from which data is fetched to be stored in the accumulator. In other words, you do the comma first and then the indirection.

In the second case you look at the contents of 0x0020 and 0x0021 to create a 16-bit memory address. Then the Y register is added to this address to give the address to which the accumulator is stored. Here you do the indirection first and then the comma.

Another way to look at things...

in "LDA ($20,X)" you have set up a bunch of addresses in zero page, e.g. the locations 0x0020 and 0x0021 hold a 16-bit address, the locations 0x0022, 0x0023 hold another 16-bit address, etc. With X set to 0 you will be indirecting through the first address; with X set to 2 you will be indirecting through the second address.

in "STA ($20),Y", the locations 0x0020 and 0x0021 hold a 16-bit address and Y is an offset to that address. This is a much more conventional way of using an index value.

--- End quote ---

Thank you SO much for such a detailed and thorough explanation, and for taking the time to help me. In the interim I also found this wonderful video, equally as well explained:

brucehoult:

--- Quote from: eti on September 28, 2022, 02:42:24 am ---I've finally got my mind around the first principles of how the 6502 works, but am stuck on one part; the "addressing modes".

--- End quote ---

It's not surprising.

Today the 6502 is often used as an example of a very simple processor -- and it certainly uses very few transistors, and especially gets very good performance from a very small number of transistors -- but it does it by being quite complex to understand.

The 6502's spiritual ancestor (and actual ancestor if you look at the people, not the companies) the 6800 is far simpler to understand, and easier to write programs for. But it's a lot less efficient, and that's important when you have a 1 MHz clock.

ledtester did a pretty good job of explaining the 6502 addressing modes in terms of assembly language syntax -- syntax that is very similar to that still used in modern assembly language today.

I will give a couple of examples, specifically of how to use the "indirect indexed X" and "indexed indirect Y" addressing modes.

First, let's say we want to have a subroutine that copies a block of bytes of memory from one place to another. We'll call it "memcpy". It has a "from" argument and a "to" argument and a size of the memory block which, for today, we'll limit to between 1 and 128 bytes (don't bother to call it at all if you want to copy 0 bytes!)

Suppose you have two blocks of memory:


--- Code: ---foo:    .byte 0,0,0,0,0,0,0
bar:    .byte 3,1,4,1,5,9,3

--- End code ---

To copy the contents of bar to foo we want to be able to do:


--- Code: ---        lda >foo
        pha
        lda <foo
        pha
        lda >bar
        pha
        lda <bar
        pha
        lda #7
        pha
        jsr memcpy

--- End code ---

Here, a ">" means the hi 9 bits of a 16 bit address and "<" means the lo 8 bits.

There are many other ways we might do this. Instead of pushing everything on to the stack we might store the various values into Zero Page locations that we know the memcpy() function expects to find them in. That would be faster, but it would add five extra bytes of code to every caller of memcpy() and you might have quite a lot of them in the program. Or the memcpy() function might expect to find the length in the X or Y register instead of on the stack. That would both be faster and save a byte of code in the caller (or two bytes compared to storing it in Zero Page)

Another way of doing this that makes the caller a lot smaller -- 8 bytes instead of 18 bytes (!) -- but less flexible is:


--- Code: ---        jsr memcpy
        .byte >foo, <foo, >bar, <bar, 7

--- End code ---

This makes memcpy work a lot harder to get the parameters, but it could be worth it if getting the most amount of program features into fixed size memory is important.  A real program might have both this version and another that was faster but more code at the caller.

On modern computers where you are often interfacing to code from a C compiler or using a standard library, there is a standard convention for how subroutines and their callers interact, but on the 6502 and z80 it was pretty much wild west and someone who wrote a function did whatever they wanted, and the users of the function needed to consult the documentation for every function before calling it.

Let's stick with the first version for now.

Here's what the memcpy() subroutine might look like:


--- Code: ---src     equ 13 ; completely arbitrary location in first 256 bytes of RAM. We need 2 bytes free here
dst     equ 42 ; again, arbitrary

memcpy:
        pla
        tay
        pla
        sta src
        pla
        sta src+1
        pla
        sta dst
        pla
        sta dst+1
        dey ; convert 1..256 into 0..255, one less than how many bytes we will copy
loop:
        lda (src),y
        sta (dst),y
        dey
        bpl loop
        rts

--- End code ---

We'd really like to test the carry flag here, so that we could copy up to 256 bytes at a time, but unfortunately the DEY instruction only sets N and Z. We could add a CPY #0 and then use BCS loop, or CPY #255 and BNE loop, but these would slow down the loop quite a bit. Ok, 12.5% -- 18 cycles per loop instead of 16. Maybe it's worth it.

Programming weird things such as the 6502 is full of such trade-offs.


OK, so when would we use ZP,x and absolute,Y addressing modes?

One example would be if we have a program doing a lot of arithmetic on 16 bit or 32 bit integers. Maybe a subroutine has a number of variables like this, and stores them in Zero Page while working on them.

Let's say we have some 32 bit variables in Zero Page with their least significant 8 bits at addresses ... 13, 42, 69, and 100. And for some reason we want to add them all up and put the result at address 42 (the answer).

You could do it like this:


--- Code: ---add32:
        clc
        lda $0000,y ;sadly, there is no ZP,y
        adc $00,x
        sta $00,x
        lda $0001,y
        adc $01,x
        sta $01,x
        lda $0002,y
        adc $02,x
        sta $02,x
        lda $0003,y
        adc $03,x
        sta $03,x
        rts

--- End code ---

... and then using it ...


--- Code: ---        ldx #42
        ldy #13
        jsr add32
        ldy #69
        jsr add32
        ldy #100
        jsr add32

--- End code ---


Finally, when would you use {ZP,x) addressing mode?

To be honest, I think I've almost never used it, except when X was 0 and Y was being used for something else. Note that (ZP,x) and (ZP),y are exactly the same thing if X and Y are 0. But (ZP),y is 1 clock cycle faster, except for STA.

I do have an example where you might use it, but it's kind of obscure so I'll leave it for now.

Peabody:
Not related to addressing modes, but one thing to note on the 6502 is the behavior of the carry flag on a subtract or compare.  If the two numbers are the same, or if you are subracting a smaller number from a larger one, so there is no borrow, the resulting CF will be set.  In fact, you generally need to set the CF before doing a subtract to get the right answer.  Some will consider this to be backward.

Navigation

[0] Message Index

[#] Next page

There was an error while thanking
Thanking...
Go to full version
Powered by SMFPacks WYSIWYG Editor
Powered by SMFPacks Advanced Attachments Uploader Mod