Anybody here with 6502 experience?

Which means Commodore 64, Apple ][, Atari 2600, Acorn Atom, NES…inspired, of course, by this topic.

I’m working on the 6502 Å-machine interpreter, and my inexperience with 6502 assembly is really showing. I’m currently working on this block of code, which is run when entering a div:

	ldx	divsp        ; Div stack pointer
	lda	opermsb+0    ; Most significant byte of operand 0
	sta	divstk,x     ; Div stack
	lda	operlsb+0    ; Least significant byte of operand 0
	sta	divstk+1,x
	inx
	inx
	stx	divsp

	asl
	rol	opermsb+0
	asl
	rol	opermsb+0
	asl
	rol	opermsb+0
	;clc
	adc	stybase
	sta	phydata
	lda	opermsb+0
	adc	stybase+1
	sta	phydata+1

I can’t figure out what this ASL - ROL - ASL - ROL - ASL - ROL sequence is doing. I know it’s messing with the bits of the operand, but what happens when you interleave them like this?

This treats accumulator as the lowbyte and opermsb+0 as the highbyte of a 16-bit number, and shifts this number three steps to the left, i.e. multiplies it by 8.

ASL shifts a 0-bit in at the bottom of a value and shifts the top bit of the value out into the carry. ROL shifts the carry in at the bottom of a value and shifts the top bit out into the carry.

At the end, if we’re sure the original number was small enough that we can’t get an overflow (i.e. the result can’t be bigger than what fits in 16 bits), we know that the carry is clear, thus we can skip clc before the addition.

Since multiplication isn’t supported in hardware on 6502, one will typically use shift operations instead, when speed is important and one of the factors is known in advance. E.g. an entry in the object table is 14 bytes long in z5. Instead of calling a generic multiplication routine with the value and 14 as the parameters, you can:

  • shift the value three times (Now we have the original value x8)
  • subtract the original value from this (Now we have the original value x7)
  • shift the value one more time (Now we have the original value x14)

Ahh, that makes sense! This interpreter stores style class information in structs seven bytes long; I’m guessing those structs are padded out to eight bytes for simplicity, so this is taking the index of a struct, multiplying it by eight (23), then adding it to stybase (start of the struct array?) and putting the 16-bit result in phydata (start of the current struct?).

Yes, it performs:
phydata = stybase + 8 * oper0
(where oper0 consists of an LSB (least significant byte) and MSB (most significant byte), which are stored in two different arrays, because it’s faster to access them that way)

Not much to add to what Fredrik wrote, but regarding the structure padding, a non uncommon way to optimize things on 6502 is to unpack “arrays of structs” into “structs of arrays”.

Say you have an array with 50 elements that each contain a 16 bit pointer, and two 8 bit variables, instead of storing that as 50 times 4 bytes, you could have 4 arrays of 50 elements:

  • Low byte of the pointer
  • High byte of the pointer
  • first 8 bit variable
  • second 8 bit variable

This way you can access any element just with X or Y indexed access without multiplying.
Obviously that’s only useful if you are going to have no more than 256 elements, and if all the sizes are known at compile time.

It’s a common technique for modern platforms too!

I think once I finally wrap my head around all the opcode mnemonics, I’ll get a lot faster at all of this. I have the advantage of experience with assembly on other systems (though generally ones with a lot of registers, like MIPS or the Z-machine); having only A, X, and Y is a bit of a change! Thankfully Linus commented his 6502 assembly a lot more than his C.

I felt very smart when I finally figured out the purpose of BIT yesterday. The C64 Å-machine interpreter has a “register” (is there a better short name for “byte in the zero page”?) called stflag that records whether it’s currently in a status area: $00 for nothing, $FF for the top status bar (supported), $80 for the inline status bar (not supported). It took me ages to figure out why these values specifically—it’s because they can both be created without any immediates (decrement $00 to get $FF, rotate $00 with the carry flag set to get $80), and can then be tested with BIT. N will be set if any status bar is active, and V will be set if it’s a supported one.

Just wait until you start seeing !byte $2C declarations in the code.

It’s a common 6502 idiom that allows you to skip the next two bytes, which is enough to skip an immediate load or ZP operation.

z_rfalse
   ldx #0
   !byte $2C
z_true
   ldx #1
   ; common code here...

(2C is the opcode for BIT absolute-address; as you’ve seen it’s an instruction that doesn’t modify any registers, although it does affect flags)

That is diabolical! I guess I should start paying attention to how many bytes different instructions take…

Size optimization on the 6502 is a compounding thing: Since relative branches are encoded on a single byte you can only go that far forward or backward, so as soon as a routine grows you start having to use JMP instead of Bcc… which in turn makes the code larger so you end up doing inverted tests to branch over a JMP that can actually go where you want to go.

So every time you can reduce by one byte here, one byte there, you increase the change you can turn a JMP into a Bcc (by ensure state flags are stable of course), which in turn can be a large enough change that you can convert some more, remove the inverted branches, etc…

You also often see things like the RTS statement is actually in the middle of the routine so the exit can be reached relatively from any point in the routine.

Global variables? :upside_down_face:

I tend to say “zp something” like “zp entry”, “zp var” or “zp pointer”

Anyone want to guess how long it took me to realize that PHY and PLY didn’t exist on the 6502 today (and thus my code including PHY and PLY would hang the emulator)? It was too long.

What I need to do is:

lda (hdbase), 1 ; Look at the first byte of the header
bne v1 ; If it's not zero, then this is a version 1.x story file

…but without affecting Y or C.

So I ended up with this abomination:

	.(
	php ; Save the flags, since C is used to choose instruction variant
	tya ; PHY/PLY don't exist on the 6502, so we have to use A
	pha
	ldy	#1
	lda	(hdbase),y ; Header byte 0: major version
	bne v1
	pla
	tay
	plp ; Restore the flags
	jmp op_en_lv_st ; Version 0.x = enter/leave status
v1
	pla
	tay
	plp ; Don't leave the flags lying around on the stack
	jmp op_bstyle ; Version 1.x = body style
	.)

This is definitely a lot of wasted cycles, but this isn’t a very frequent operation, so hopefully it’s fine for now. If someone who actually knows 6502 assembly wants to optimize it, pull requests are always welcome!

I know very little about the 6502, but as a Swiftie, I’m delighted to see TAY in action.

It’s one of those microprocessors with very few registers (A, X, Y, and that’s about it), so there are separate instructions for TAY (Transfer A to Y), TYA, TAX, and TXA. As far as I know there’s no way to copy X into Y or vice versa without clobbering A.

As someone used to machines with a lot more registers (at least 16 of them), trying to keep the values of my three registers intact while doing any sort of calculation is a nightmare! I thought I could PHY (PusH Y onto the stack) and PLY (PulL Y from the stack) for the brief moment I need to store something else in there, but no…

It’s tricky to optimize because pretty much everything would end up changing the flags so the plp is pretty much required to be before the jmp.

Technically the plp/jmp could be emulated with a RTI, but pushing the address to jump to on the stack would defeat the size gain.

Replacing the tya/pha and pla/tay by a zero page sty/ldy only gains like one byte in size and requires a zero page location).

Storing on ZP is faster, but the same size.

oops yes, I was doing that quickly and fumbled with the count.

  php
  lda hdbase
  clc
  adc #1
  sta check_addr+1
  lda hdbase+1
  adc #0 
  sta check_addr+2
  plp
check_addr
  lda $1234
  bne v1

Uses only the A register and some self-modifying code to avoid having to touch Y.

On my 6502 Z machine, the header is always loaded at a fixed address in memory, so the equivalent for me would be just lda $1001 or lda HEADER+1

Edited to add php/plp since your constraints included “don’t modify the carry flag” and at this point I can’t say this is any better than your solution.

The constraints of “don’t touch carry flag but the answer has to be in the zero flag” are fairly limiting since both the load and transfer instructions affect the Z flag.

  sty yrestore+1
  ldy #1
  lda (hdbase),y ; Z contains result we care about
  php
yrestore
  ldy #$12 ; this modifies Z
  plp          ; restore original Z result from above

is another option that avoids ZP use but is still self-modifying code. If you didn’t need to preserve the carry flag, you could leave out the php and replace the plp with cmp #$00 or whatever header value you cared about.

-Dave