Getting a foothold into 6502 machine language, part 1
It seems in Commodore Basic version 2 the preferred way to get a value into a user-defined 6502 machine language routine (and to get a value back), is the usr
function. It took me some trial and error to get it to work. Luckily, Google is Your Friend, or, in my case DuckDuckGo. I also used “Mapping the Commodore 64”, by Sheldon Leemon, which can be downloaded as PDF on Archive.org. If you’re serious about 6502 assembly language on the Commodore 64, I highly recommend this book.
[ part 2 | part 3 | part 4 | part 5 | bonus ]
First of all, how do we get the usr
function to work? Simply running print usr(42)
results in an error message.
Apparently, one has to set the address of the start of one’s own machine language routine. After (re)booting the operating system the value is set to $b248 (fcerr routine, which prints an illegal quantity
error). Taking a step back, the final step of the routine in Basic ROM that interprets the usr
function is jumping to address $0310 in RAM:
0310 4c 48 b2 jmp $b248
So all one has to do is overwrite the address of this jmp
instruction so it points to the user defined machine language routine. This address is located at $311 and $312, in the usual low byte high byte order of the 6502 machine language. If the user defined routine is, for example, located at $c000 (49152), the values $00 and $c0 have to be written in $311 and $312, respectively.
In Basic we use decimal instead of hexadecimal:
- $311 is 785 decimal
- $312 is 786 decimal
- $c0 is 192 decimal
10 poke 785,0:poke 786,192
We always have to keep in mind that Commodore Basic uses real values, never integers. The value we supplied to the usr
function in our Basic code is stored into fac1, which consists of six bytes, starting at location $61. Luckily, this isn’t all that important here, since there are two handy functions to convert real values back and forth into 16-bit signed integer values, using fac1. They are:
- $b1aa – Convert a Floating Point Number to a Signed Integer in .A and .Y Registers, which calls the ayint routine (located at $b1bf), and loads the resulting signed 16-bit integer value into registers .Y and .A (lo/hi).
- $b391 – givayf Convert 16-Bit Signed Integer in registers .Y and .A (lo/hi) to a Floating Point Number
In both instances the registers .Y and .A contain the low byte, and high byte of the 16-bit signed integer, respectively.
But, wait, what is a “16-bit signed integer” exactly? Well, in the 6502 architecture, this is the 2’s-complement representation of a whole number between -32768 and 32767, inclusive. It simplifies addition and subtraction of whole numbers, either positive, negative, or zero. If you want to know more, read the relevant Wikipedia article.
The thing is that if you pass a zero or a positive value into the usr
function and convert it into an integer in your machine language routine, it has to be between 0 and 32767, inclusive. If your machine code routine can deal with that restriction, you’re okay. Of course, you could use the real value instead, or use some trickery to use integers between 0 and 65535, inclusive. Whatever the routine does exactly is up to you, since you are defining it.
Perhaps I should give an example of how to use the usr
function in a useful manner 😉
What if I wanted to know if a value I put into the usr
function is a prime number, divisible only by itself or one, but no other whole positive number? That is a tricky problem to solve, since the 6502 has no division, nor multiplication instructions built in (it has to be done in software instead, using a set of instructions). Other than that, finding out a whole number is prime isn’t straightforward either, at least, if you want the method to be efficient and correct.
To not reinvent the wheel, I looked for someone who had done the work before, and found Geeks For Geeks - Prime Numbers as a resource. Let’s go with that!
Anyway, the result of our usr
function should be either true or false, which is in Commodore Basic represented by a -1
and 0
, respectively:
print 1=1,1=0
-1 0
ready.
To understand all this, I used a minimal viable solution. My code should test if the value put into the usr
function and converted into a signed integer is even. If so, it should return a -1
, else a 0
. We still need to convert the result of parity test into a real number before exiting our routine in 6502 assembly language.
*= $c000
; usrfunction 0.1
; test if the usr function even works
; as a test, check for even parity
getayf = $b1aa
givayf = $b391
jsr getayf
tya ;lo byte
and #%00000001 ;mask out every bit except bit 0
cmp #0
beq iseven
lda #$00 ;false, which is 0 in 16-bit signed integer
tay ;corresponding to the hex value $0000
jmp givayf ;make it real, return to Basic
iseven:
lda #$ff ;true, which is -1 in 16-bit signed integer
tay ;corresponding to the hex value $ffff
jmp givayf ;make it real, return to Basic
I wrote this routine in the Textastic app on my iPad, used the Virtual 6502 assembler to create a hex dump, used it to create the Basic program below, typed that into the V.I.C.E. C64 emulator on my Raspberry PI-400 running Raspberry Pi OS, and ran it.
10 poke 785,0:poke 786,192: rem set usr address to $c000 (49152)
20 forn=49152to49173:readb:poken,b:next:rem read in machine code
30 x=int(rnd(ti)*100):rem random number between 0 and 99, inclusive
40 print x;" is ";
50 if usr(x) then print "even":goto 70
60 print "odd"
70 data 32,170,177,152,41,1,201,0,240,6
80 data 169,0,168,76,145,179,169,255,168,76,145,179
It worked. Pfew!
Now we can tackle more complicated matters, but that has to wait until part 2, because this article is already much longer than I anticipated, and I started to make mistakes. I need a break.