Trying to understand some 65c816 assembly code—what is the purpose of this ordering of instructions?

201 views Asked by At

I'm new to assembly and trying to piece together what this piece of code is doing. Here's the Super NES 65c816 reference I'm using.

01: $A4917E AD E5 05    LDA $05E5  [$A4:05E5]
02: $A49181 29 3F 00    AND #$003F
03: $A49184 AA          TAX
04: $A49185 AD E5 05    LDA $05E5  [$A4:05E5]
05: $A49188 89 02 00    BIT #$0002
06: $A4918B D0 05       BNE $05    [$9192]
07: $A4918D 8A          TXA
08: $A4918E 49 FF FF    EOR #$FFFF
09: $A49191 AA          TAX
10: $A49192 8A          TXA
11: $A49193 18          CLC
12: $A49194 6D 7A 0F    ADC $0F7A  [$A4:0F7A]
13: $A49197 85 12       STA $12    [$00:0012]
14: $A49199 AD E5 05    LDA $05E5  [$A4:05E5]
15: $A4919C 29 00 1F    AND #$1F00
16: $A4919F EB          XBA
17: $A491A0 85 14       STA $14    [$00:0014]

What I know already is that $05E5 is the storage location of the game's random number generator (RNG). It's a 16-bit value. What I'm trying to figure out is what kind of math the game's trying to do with this random number.

Here's a more trivial example, for starters:

$86AE1C AD E5 05    LDA $05E5  [$86:05E5]
$86AE1F 29 01 00    AND #$0001
$86AE22 F0 05       BEQ $05    [$AE29]

Here it's clearly just using the random number for a coin flip—the highest entropy bit being 0 or 1—to decide to branch or not.

Getting back to the original example, let me say what (I think) I understand, in pieces:

01: $A4917E AD E5 05    LDA $05E5  [$A4:05E5]
02: $A49181 29 3F 00    AND #$003F
03: $A49184 AA          TAX

This is loading the random number into the accumulator, say 11010101001100010, and AND'ing it to 0000000000111111, yielding 0000000000100010 in the accumulator, which it then transfers to register X.

04: $A49185 AD E5 05    LDA $05E5  [$A4:05E5]

Next it loads the same random number into the accumulator—I guess because we lost it during the AND operation? (The random number is generated elsewhere, executing once approximately every 1/60th of a second. But it's guaranteed to be the same here.)

05: $A49188 89 02 00    BIT #$0002
06: $A4918B D0 05       BNE $05    [$9192]

Here's where it gets fuzzy for me. I read multiple resources online (though not specific to the 65816) saying BIT is like an AND but without mutating either operand. But it seems like there are some nuances having to do with flags.

  1. Continuing with my example, am I correct to believe that BIT #$0002 on the accumulator value of 11010101001100010 would be 1, because the second lowest bit is 1?

  2. Am I correct to believe that as a result of the bit test, the z flag's value will have been set to 0, precisely the opposite of the previous result?

  3. Am I correct to believe that it will then branch (to instructions at address $05), since Z=0?

Okay, if I've gotten those fundamentals down, then these are my real questions:

  • Why wouldn't the developers (who in those days optimized everything) have swapped lines 01-03 with 04-06? That is, if there's a chance you'll end up jumping to some totally different piece of code, why not test and do the jumping first, and only if not branching, do the AND operation? (Wait, is it possible register X might be used wherever the code jumps to?)

  • Could someone help me understand the next few instructions? In particular, what is the point of executing TAX then TXA? Isn't that just swapping then unswapping values?

Sorry this is many questions in one, but hopefully it's okay because I'm referring to the same set of instructions. Thanks in advance for any assistance.

P.S. Here's the page about BIT in a textbook I have about the 65816, in case it helps.

enter image description here

I could not understand the paragraph starting, "BIT is usually used immediately preceding a conditional branch instruction:", though it seems relevant...

3

There are 3 answers

1
Weather Vane On BEST ANSWER

Answering one of the questions about these lines

06: $A4918B D0 05       BNE $05    [$9192]
07: $A4918D 8A          TXA
08: $A4918E 49 FF FF    EOR #$FFFF
09: $A49191 AA          TAX
10: $A49192 8A          TXA

Why is TAX followed by TXA?

It is because of the branch at line 6, which if taken will execute from line 10.

The TAX shown is matching the previous TXA (not the following one) as in

07: $A4918D 8A          TXA
08: $A4918E 49 FF FF    EOR #$FFFF
09: $A49191 AA          TAX

Some whitespace or labels in the code would make this "phrasing" clearer, although the branch does give the destination address.


Questions 1-3 about the Z flag: you are correct in saying that the Z flag is set if the result being tested is 0.

  1. Am I correct to believe that it will then branch (to instructions at address $05), since Z=0?

Yes, but the $05 is not an address, it is a signed displacement (from the instruction that would have followed, because the PC program counter (aka IP instruction pointer) would already have been advanced by the time the decision is taken).

1
Sep Roland On

Could someone help me understand the next few instructions? In particular, what is the point of executing TAX then TXA? Isn't that just swapping then unswapping values?

  • It's not a swap at all! These Txx instructions copy a value from one register to another leaving both registers equal to each other.
  • The BNE instruction at line 6 caused two different execution paths that eventually had to come together again. The programmer could have used the BRA unconditional jump to this effect, but ultimately didn't because they preferred codesize over execution speed:

The BNE instruction either falls through in the first path or jumps to the second path. At the end of the first path, BRA jumps to the come-together-point. This solution requires 1 byte more (3 instead of 2), but the first path executes in 1 cycle less (3 instead of 4, unless there's page boundary crossing then it would be 4 cycles also):

06: $A4918B D0 06       BNE $06    [$9193]
    *** first path
07: $A4918D 8A          TXA
08: $A4918E 49 FF FF    EOR #$FFFF
09: $A49191 .. 01       BRA $01    [$9194]
    *** second path
10: $A49193 8A          TXA
    *** come together point
11: $A49194 18          CLC

The TAX instruction is used to avoid having to use the costlier branch instruction. It sets up the X register so the code can fall through in the TXA instruction in the second path. This solution requires 1 byte less (2 instead of 3), but the first path executes in 1 cycle more (4 instead of 3):

06: $A4918B D0 05       BNE $05    [$9192]
    *** first path
07: $A4918D 8A          TXA
08: $A4918E 49 FF FF    EOR #$FFFF
09: $A49191 AA          TAX
    *** second path
10: $A49192 8A          TXA
    *** come together point
11: $A49193 18          CLC

the same random number into the accumulator ... But it's guaranteed to be the same here.

Why is this code so complicated?

On the premise that the random number from 05E5 remains the same, and knowing that BIT does not change any of its operands, and that the bit that we test is amongst the bits that remain from the AND, we can write this code without even touching X:

01: $A4917E AD E5 05    LDA $05E5  [$A4:05E5]
02: $A49181 29 3F 00    AND #$003F
05: $A49184 89 02 00    BIT #$0002
06: $A49187 D0 03       BNE $03    [$918C]
08: $A49189 49 FF FF    EOR #$FFFF
11: $A4918C 18          CLC
3
Grégory W On

About this question and Weather Vane answer, I think depending on the RNG value (0x05E5), you can guess why it is done this way :

  • If the value is 0x3F (Taking the important bits here for demonstration purposes)
01: $A4917E AD E5 05    LDA $05E5  [$A4:05E5] ;  A=0x3F, X=?
02: $A49181 29 3F 00    AND #$003F ; A=0x3F, X=?, Y=?, flags: Z=0
03: $A49184 AA          TAX ; A=0x3F X=0x3F
04: $A49185 AD E5 05    LDA $05E5  [$A4:05E5] ; A=0x3F, X=0x3F
05: $A49188 89 02 00    BIT #$0002; A=0x3F, X=0x3F, flags: Z=0
06: $A4918B D0 05       BNE $05    [$9192] ; Branch !
10: $A49192 8A          TXA A=0x3F, X=0x3F
11: $A49193 18          CLC ; Continue code
  • If the value V RNG & 0x3F == 0 && RNG & 0x2 == 0, like 0x40:
01: $A4917E AD E5 05    LDA $05E5  [$A4:05E5] ; A=0x40
02: $A49181 29 3F 00    AND #$003F ; Z=0
03: $A49184 AA          TAX ; A=0x40, X=0x40
04: $A49185 AD E5 05    LDA $05E5  [$A4:05E5] ; A=0x40
05: $A49188 89 02 00    BIT #$0002 ; A=0x40 Z=1
06: $A4918B D0 05       BNE $05    [$9192] ; No branching here
07: $A4918D 8A          TXA ; A=0x40, X=0x40
08: $A4918E 49 FF FF    EOR #$FFFF ; A=0xBF, X=0x40 , assuming 8 bits mode but I'll talk about that later
09: $A49191 AA          TAX ; A=0xBF, X=0xBF
10: $A49192 8A          TXA ; A=0xBF, X=0xBF
11: $A49193 18          CLC ; c=0

When looking at this, yes, the question is : Why did the dev did not swap and do TAX/TXA ?

My best guess for the first question is the same as Weather Vane, it's because the branch. I think it does have another purpose : Clear the flags.

Until here, we did not talk about 8/16-bits mode (m flag) or other flags. I think that this is the point of the CLC after, also clear flags.

When using transfer instructions, depending on the m flag (which determines the memory width mode) you copy flags, you clear some.

In my source, http://6502.org/tutorials/65c816opcodes.html#6.10.1, they give this exemple :

Example: If the accumulator is $1234, the X register is $ABCD, and the m flag is 1, then after a TXA :

  • the accumulator will be $12CD
  • the n flag will be 1 (since only $CD was actually transferred)
  • the z flag will be 0

I assume this only reset flags to a known state.


About your last question :

I could not understand the paragraph starting, "BIT is usually used immediately preceding a conditional branch instruction:", though it seems relevant...

Having the possibility to perform a Bitwise check without mutating your values let's you perform additional instructions. Otherwise, you would need to LDA again like done in line 4. That's how I would use it.