Confusion about indirect X

Let's talk about anything related to the 6502 microprocessor.
Post Reply
User avatar
cbmeeks
Posts: 1254
Joined: 17 Aug 2005
Location: Soddy-Daisy, TN USA
Contact:

Confusion about indirect X

Post by cbmeeks »

I'm trying to iterate through two different sections of memory (for C64).

Let's say for example that I have:

Code: Select all


lda #<BUFFER
sta ZP10
lda #>BUFFER
sta ZP11

lda #<SCREEN
sta ZP12
lda #>SCREEN
sta ZP13

ldx #00
ldy #40

lda (ZP10), y    // get the 40th character from buffer
sta (ZP12), x    // store at position 0 on the screen

It seems you cannot use X as an indirect offset like you can Y.
However, my assembler (KickAssembler) compiles this just fine. It even runs. But it obviously doesn't work.

The only way I can get indirect addressing to work that way is to always use Y. Which can be tedious sometimes.
Does anyone know why this is?

BTW, if anyone is curious, this is my *nearly* full screen scrolling (not smooth) with color. As you can see, I don't use "indirect X".

Code: Select all

    .align $100
    * = * "Map.DrawMap"
    DrawMap: {
            .label COL = ZP.TEMP1
            lda #$00
            sta COL

            ldx #<MAP_BUFFER        // X contains the LOW byte.  Character column.
            ldy #>MAP_BUFFER        // Y contains the HIGH byte of the MAP_BUFFER (character row)
            stx ZP.MapTemp          // ZP.MapTemp contains MAP_BUFFER address
            sty ZP.MapTemp + 1

            ldx #$00
            ldy ZP.MapX
        !loop:
            lda #>MAP_BUFFER
            sta ZP.MapTemp + 1

            .for(var i = 0; i < 18; i++) {
                lda (ZP.MapTemp), y
                sta SCR_BUFFER + (i * 40), x
                sty ZP.TEMP1
                tay
                lda ATTR_DATA, y
                sta VIC.COLOR_RAM + (i * 40), x
                ldy ZP.TEMP1
                inc ZP.MapTemp + 1
            }
            inx
            iny
            cpx #40
            beq !exit+
            jmp !loop-
        !exit:
            rts
    }
Cat; the other white meat.
John West
Posts: 383
Joined: 03 Sep 2002

Re: Confusion about indirect X

Post by John West »

cbmeeks wrote:

Code: Select all

lda (ZP10), y    // get the 40th character from buffer
sta (ZP12), x    // store at position 0 on the screen
It seems you cannot use X as an indirect offset like you can Y.
However, my assembler (KickAssembler) compiles this just fine. It even runs. But it obviously doesn't work.

The only way I can get indirect addressing to work that way is to always use Y. Which can be tedious sometimes.
Does anyone know why this is?
The 6502 has two addressing modes that involve zero-page indirection: (zp, X) and (zp), Y. They differ in when the indexing takes place: before or after reading the address from zero page. (65C02 adds (zp) with no indexing, but that's not relevant to the C64).

Why is that? I don't think any of us know. We can speculate, but only the original designers would know (and they may have forgotten). You can use pre-indexing with X or post-indexing with Y, and that's all. I'm not sure I've ever used (zp, X).

If your assembler is accepting STA (zp), X and producing code for it, that's a bug in the assembler. What opcode does it produce for that line?
User avatar
cbmeeks
Posts: 1254
Joined: 17 Aug 2005
Location: Soddy-Daisy, TN USA
Contact:

Re: Confusion about indirect X

Post by cbmeeks »

I didn't think to check the opcode.

It's producing $95 which is Zero Page, X.

Code: Select all

STA (STore Accumulator)

Affects Flags: none

MODE           SYNTAX       HEX LEN TIM
Zero Page     STA $44       $85  2   3
Zero Page,X   STA $44,X     $95  2   4
Absolute      STA $4400     $8D  3   4
Absolute,X    STA $4400,X   $9D  3   5
Absolute,Y    STA $4400,Y   $99  3   5
Indirect,X    STA ($44,X)   $81  2   6
Indirect,Y    STA ($44),Y   $91  2   6
Honestly, this isn't the first time I forgot that you cannot use X like I am wanting to. lol
Cat; the other white meat.
John West
Posts: 383
Joined: 03 Sep 2002

Re: Confusion about indirect X

Post by John West »

Oh. Yes. That's not a bug then, but an extremely confusing ambiguity. I ran into it myself with my own assembler.

It's good for an assembler to accept general expressions wherever it needs a value. That way we can write things like

Code: Select all

    LDA buffer + (something+5)*3, Y
Expressions are made of subexpressions: if A and B are expressions, then A + B is too. We want to be able to control precedence with parentheses, so an expression inside ( ) is also an expression.

Code: Select all

    LDA (buffer + (something+5)*3), Y
Unfortunately that's identical to the syntax for indirect addressing, and there's no way to tell which was intended. I don't remember the solution I went with (in practice it has been working well enough, whatever it was). If I was writing it again now I'd probably say parentheses can only be used in subexpressions. You can't have the whole expression wrapped in ( ), because that's reserved for indirection.

In that case

Code: Select all

    STA (ZP12), x
would be an error, because there is no (zp), X addressing mode, and putting the whole expression inside ( ) cannot be anything but indirection.
barnacle
Posts: 1831
Joined: 19 Jan 2004
Location: Potsdam, DE
Contact:

Re: Confusion about indirect X

Post by barnacle »

That suggests that an assembler should be looking for a ( immediately (ignoring spaces) after the instruction and treating whatever follows that as an expression; you shouldn't need enclosing braces so the final ,X) or ),Y can be unambiguous.

Of course, since there's never any in-software evaluation, you can define the whole thing externally to your actual instruction:

Code: Select all

ptr equ (buffer + (something+5)*3)
;or ptr equ buffer + (something+5) * 3
    LDA (ptr),Y
which ought to be acceptable to the assembler?

Neil
BillG
Posts: 710
Joined: 12 Mar 2020
Location: North Tejas

Re: Confusion about indirect X

Post by BillG »

If X always contains zero,

sta (ZP12,X)

does what

sta (ZP12), x

appears to do.

If you have to vary the index, you will have to

- ping pong between the indices using Y

or

- modify ZP12 to incorporate the index

Edit: <RANT>

This is why I prefer that the assembler always require a delimiter like ';' before the comment field. The other extreme, what your assembler does, is that any text after the addressing mode is parsed is considered to be a comment. If ';' is required, ", X" will be flagged as a comment without a leading ';'

</RANT>
jgharston
Posts: 181
Joined: 22 Feb 2004

Re: Confusion about indirect X

Post by jgharston »

John West wrote:
You can use pre-indexing with X or post-indexing with Y, and that's all. I'm not sure I've ever used (zp, X).
I've used it to optimise code, to save the use of another register, save some opcode bytes, and get a bit more speed:

Code: Select all

.bytelp
LDX #8                       :\ Prepare to rotate CRC 8 bits
LDA (addr-8 AND &FF,X)       :\ Fetch byte from memory
:
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Re: Confusion about indirect X

Post by GARTHWILSON »

John West wrote:
The 6502 has two addressing modes that involve zero-page indirection: (zp, X) and (zp), Y.  They differ in when the indexing takes place: before or after reading the address from zero page.  (65C02 adds (zp) with no indexing, but that's not relevant to the C64).

Why is that?  I don't think any of us know.  We can speculate, but only the original designers would know (and they may have forgotten).  You can use pre-indexing with X or post-indexing with Y, and that's all.  I'm not sure I've ever used (zp, X).
I suppose it'd be nice to be able to do either operation with either index register; but that would take up more places in the op code table, and maybe the reason they didn't do that is that they were keeping those places open for other future enhancements, and/or that it would require more silicon space in a die they wanted to keep the price down on.  At least we do have those functions though, only restricting which index register can be used for each.  So many people have wondered why the (ZP,X) addressing mode even existed, thinking it was useless.  The secret is to think of its use not for a table, but rather for a data stack in ZP.  The Forth programming language uses it constantly, and I have heard that other languages that use a ZP stack (although they might hide it from the user) also use it.
BillG wrote:
This is why I prefer that the assembler always require a delimiter like ';' before the comment field.  The other extreme, what your assembler does, is that any text after the addressing mode is parsed is considered to be a comment.  If ';' is required, ", X" will be flagged as a comment without a leading ';'
That's a good consideration that I'm not sure I had ever thought of.  It's similar to the use of a colon (":") after a label, which I always recommend even if the assembler does not require it.  It makes searches easier, because otherwise if you're looking for the particular routine, a search without the colon may turn up many, many references to the label before you find the label itself.  I've also had assembly errors, even in the last month, resulting from violating this good practice.
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?
User avatar
BigDumbDinosaur
Posts: 9425
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Confusion about indirect X

Post by BigDumbDinosaur »

John West wrote:
I'm not sure I've ever used (zp, X).

The driver code I wrote for the virtual QUART in my POC unit makes extensive use of (<dp>,X) addressing to select the chip registers and serial I/O queues being manipulated.  The alternative would have been to set up and take down pointers each time a DUART or queue had to be accessed, which, in particular, would have inflated the size of the IRQ handler that processes DUART interrupts.  It came down to counting cycles, and use of (<dp>,X) was significantly faster, even though it eats up a fair amount of direct page space (less a problem with the 65C816 than the 65C02, of course).

GARTHWILSON wrote:
I suppose it'd be nice to be able to do either operation with either index register; but that would take up more places in the op code table, and maybe the reason they didn't do that is that they were keeping those places open for other future enhancements, and/or that it would require more silicon space in a die they wanted to keep the price down on.

Also, the originally-intended application for the 6502, as was the case for the other early microprocessors, was in machine controllers and the like, not general-purpose computing.  As a goal of Chuck Peddle and his posse was to undercut Intel’s and Motorola’s position in that market, a lot of the 6502’s refinement was focused on the cost angle, which entailed, among other things, excising instructions that weren’t critical to the application.  Something such as (<zp>,X),Y would have been useful, but not essential.  Their (MOS’) success in that area arguably is what accelerated the homebrew computer hobby, since a 6502 was a *lot* cheaper than an 8080 or 6800...and as it turned out, just as capable.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
teamtempest
Posts: 443
Joined: 08 Nov 2009
Location: Minnesota
Contact:

Re: Confusion about indirect X

Post by teamtempest »

Quote:
Unfortunately that's identical to the syntax for indirect addressing, and there's no way to tell which was intended. I don't remember the solution I went with (in practice it has been working well enough, whatever it was). If I was writing it again now I'd probably say parentheses can only be used in subexpressions. You can't have the whole expression wrapped in ( ), because that's reserved for indirection.
I'm not sure about that. The value of an expression wrapped in parentheses is no different than the value of the same expression not wrapped in parentheses. So that in itself is not a problem.

What matters for indicating indirection, particularly post-indexed Y, is that the parentheses are properly balanced at the beginning and end of of the expression. If you add one to counter when encountering an open parenthesis and subtract one for a close parenthesis, indirection is flagged if the counter value reaches zero at the final close parenthesis at the end of the expression. If you reach zero before then, then it's not indirection, it's just parentheses indicating order of evaluation.

So this:

Code: Select all

lda ((expr1) + (expr2)),y
is indirection, but this:

Code: Select all

lda (expr1) + (expr2),y
is not, even though both lines have an opening parenthesis at the start and a close parenthesis at the end.
John West
Posts: 383
Joined: 03 Sep 2002

Re: Confusion about indirect X

Post by John West »

teamtempest wrote:
So this:

Code: Select all

lda ((expr1) + (expr2)),y
is indirection, but this:

Code: Select all

lda (expr1) + (expr2),y
is not, even though both lines have an opening parenthesis at the start and a close parenthesis at the end.
That's what I'm saying. The second is an expression containing two sub-expressions (each of which are in parentheses) added together. It is not in parentheses itself.

Although it starts and ends with ( ), they aren't paired. The first ( goes with the first ) making a pair around expr1. The second ( goes with the second ) making a pair around expr2. There's nothing around the whole expression. My assembler has a proper recursive expression parser with precedence, so this falls out naturally.
User avatar
BigDumbDinosaur
Posts: 9425
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Confusion about indirect X

Post by BigDumbDinosaur »

teamtempest wrote:

Code: Select all

lda ((expr1) + (expr2)),y
is indirection, but this:

Code: Select all

lda (expr1) + (expr2),y
is not, even though both lines have an opening parenthesis at the start and a close parenthesis at the end.

That last bit of code, in most assemblers that can evaluate parenthesized expressions, would effectively be...

Code: Select all

lda expr,y

...where EXPR is the sum of EXPR1 and EXPR2.

On the other hand, in older assemblers that could not evaluate an operand in that fashion, the result would be a syntax error, due to the parentheses being seen solely as indications of indirection.  That was the behavior of Commodore’s MADS assembler for the C-64.

In the Kowalski assembler, expression grouping is done with curly braces ({ }), which is a little confusing until one gets used to it.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
fachat
Posts: 1123
Joined: 05 Jul 2005
Location: near Heidelberg, Germany
Contact:

Re: Confusion about indirect X

Post by fachat »

When I need to use a calculation with brackets in a normal addressing mode, I just use something like

Code: Select all

    LDA 0+(....),Y
That tricks the assembler into absolute addressing mode

André
Author of the GeckOS multitasking operating system, the usb65 stack, designer of the Micro-PET and many more 6502 content: http://6502.org/users/andre/
User avatar
BigDumbDinosaur
Posts: 9425
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: Confusion about indirect X

Post by BigDumbDinosaur »

fachat wrote:
When I need to use a calculation with brackets in a normal addressing mode, I just use something like

Code: Select all

LDA 0+(....),Y
That tricks the assembler into absolute addressing mode

Due to the variations in how assemblers handle expressions (and some can’t do algebraic evaluation at all), I’ve generally avoided doing such evaluation in operands and instead evaluate earlier in the source code, assign the result to a symbol and use the symbol as the operand.

In the Kowalski assembler, there is no confusion over use of parentheses, since curly brackets are used to set precedence, e.g....

Code: Select all

XYZ = {2+3}*{8-1}

I could, however, use the above expression as an operand without trouble:

Code: Select all

LDA ({2+3}*{8-1},X)

...but I prefer to keep such clutter out of instructions.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
User avatar
cbmeeks
Posts: 1254
Joined: 17 Aug 2005
Location: Soddy-Daisy, TN USA
Contact:

Re: Confusion about indirect X

Post by cbmeeks »

I do enjoy Kick Assembler. If I'm not mistaken, you can use square brackets in place of parenthesis so that evaluations don't mess up the assembled output.

Also, I usually keep complex evaluations down to a minimum. The original issue I was having on this thread was solved using the following code.

Code: Select all

    * = * "Map.DrawMap_DBL_BUFFER_Top"
    DrawMap_DBL_BUFFER_Top: {
            ldx #$00
            stx ZP.MapTemp
            ldy ZP.MapX
        !loop:
            lda #>MAP_BUFFER
            sta ZP.MapTemp + 1

            .for(var i = 0; i < 9; i++) {
                lda (ZP.MapTemp), y                 // A has CHAR from MAP_BUFFER
                sta DBL_BUFFER + (i * 40), x        // Blit the CHAR
                sty ZP.TEMP1                        // Copy Y position (X position in MAP_BUFFER)
                tay                                 // Move the CHAR to Y
                lda ATTR_DATA, y                    // Lookup color/attr for the CHAR
                sta VIC.COLOR_RAM + (i * 40), x     // Blit the color value
                ldy ZP.TEMP1                        // Restore Y
                inc ZP.MapTemp + 1                  // Increment HIGH byte of MAP_BUFFER ptr
            }
            inx                                     // Advance to next CHAR column in MAP_BUFFER
            iny                                     // Move Y 
            cpx #40
            beq !exit+
            jmp !loop-
        !exit:
            rts
    }
Cat; the other white meat.
Post Reply