Friday, March 4, 2022

6502 Machine Code Hacking on a C64 Emulator

It's cool to see the Commodore 64 and the Apple ][s seeing their glory again in the retro-computing revolution. My first computer was a TRS-80 Model I computer (Z-80). I remember tearing that thing apart, upgrading the ROMS with an EEPROM burner to level 2 BASIC. Then I needed a 16K expansion interface, so I soldered one together using a hobbyist kit board from LNW. My second computer was an Apple ][c where I hammered my way through 6502 machine code. My love for these machines has been rekindled as I purchased Apple I replica board that uses an Arduino serial interface. That is currently a work in progress. My case of the I-wannas surged inside me again when I saw a Commodore 64 hobbyist motherboard available. Which I thought would be a cool project. However, it's not a very cost-effective project that I may tackle anyway after completing the Apple I replica project. The Commodore is not cost-effective because of the amount of retro equipment you need to purchase to run an old used one again. You'll need an old-style "retro" C64 monitor and no telling what the condition of the motherboard will be in. Will it need capacitor and component retrofitting? There were numerous reasons I temporarily aborted building a new C64 or acquiring an old original CPU breadbox. The simple cost and time-saving approach were to run a virtual machine C64.
I selected the VICE, VIrtual Commodore Emulator, which runs on literally every platform. I am currently running this emulator on Mac OS X on my Mac Mini M1 CPU platform.

Installation and Software

I am not going to cover installation in detail here. The installation is straightforward, and the software has a lot of good sites and videos available on this. However, I am going to include the URLs of the installation software.

VICE Software

 Choose the installations based on the platform you are running on.

VICE - the Versatile Commodore Emulator

Supermon+64

Download and install the Supermon+64 Machine Language Monitor.

Supermon+64 Machine Language Monitor

 Note: I stored the supermon+64 image, sprmon64.64 in the Documents directory of my Mac Mini. This folder will be my virtual floppy drive mount for my C64 emulator.

Documents->Commodore C64 Disk Drive->sprmon64.d64


Running the Supermon+64 Machine Language Monitor

 On my Mac Mini I am executing the Applications->vice-arm64-gtk3-3.6.1->x64sc emulator software. 

If you made it this far, and see the C64 purple startup screen shown above. Let's fire up the machine code monitor!

Under the VICE File menu, there is an option to Smart-attach a file. I setup Smart-attach a file to look into my Documents->Commodore 64 Disk Drive Folder.  There I Smart-attach the file, sprmon64.d64.


 Select the sprmon64.d64 file to execute.Use the Autostart button on the Smart-attach a file dialog box to load and auto-execute the Supermon+64 software. Now you should see the Commodore 64 running the machine language monitor.

Playing in the Sand Box

Let's enter some code and do a simple Hello World program. I plagiarized this code from a Youtube video, Hello World in C64 / 6502 Assembly Tutorial Part-1, from someone who claims not to be a Wizard. I love their coding example because it covers many basics in programming, accessing an array (memory), basic code logic, and branching. 
 

Setting up the Hello World Array in Memory

 We need to create an array in memory of our characters to spell out "HELLO WORLD!". 
 Here is the pseudo code to illustrate what we're doing. It's pseudo code because we're not really using an array, we're actually using pointers to places in memory. An array is good illustrative tool to conceptualize what we're going to be doing in the machine code to write HELLO WORLD!"
 
 
Array       Char  Hex     Address
----------------------------------------
Array[0]   = "H"  0x08    0x1030
Array[1]   = "E"  0x05    0x1031
Array[2]   = "L"  0x0C    0x1032
Array[3]   = "L"  0x0C    0x1033
Array[4]   = "O"  0x0F    0x1034
Array[5]   = " "  0x20    0x1035
Array[6]   = "W"  0x17    0x1036
Array[7]   = "O"  0x0F    0x1037
Array[8]   = "R"  0x12    0x1038
Array[9]   = "L"  0x0C    0x1039
Array[10]  = "D"  0x04    0x103A
Array[11]  = "!"  0x21    0x103B
Array[12]  =  0   0x00    0x103C   null byte end of string

We need to enter these values into the machine language monitor (from the array above).

MEMORY
 
M 1030 1040
>1030 08 05 0C 0C 0F 20 17 0F
>1038 12 0C 04 2E 00 FF 00
    

Let's use the monitor to examine the existing data in these memory locations.

Type the following command from the machine language monitor to see the contents of memory at this location.

M $1030 $1040      (in Supermon+64 both M $1030 $1040  and M 1030 1040 do the same thing).

To implement the array use the cursor keys to move the cursor over the data values you want to change and implement. The following snapshot shows the BEFORE (before modifying the bytes) and AFTER editing the memory locations 0x1030 - 0x1040.

If you'll notice there is a bug in my version of supermon+64 where the updated byte fields don't display the correct bytes in memory. We should see H E L L O  W O R L D ! in the byte fields. Most likely a bug in this version.
 

Clearing the Screen

If you're screen becomes to convoluted with coding or memory dumps, the keyboard combination, "SHIFT+HOME KEY" clears your screen.

Implementing the Code

Let's implement the following machine code to read our array of bytes spelling "HELLO WORLD!".
 
 1000     A2 00          LDX          #$00  
 1002     BD 30 10       LDA          $1030,X  
 1005     F0 07          BEQ          $100E
 1007     9D 00 04       STA          $0400, x
 100A     E8             INX
 100B     4C 02 10       JMP          $1002
 100E     60             RTS
    
Break it down. What is the code doing? This is a really nice example code for learning. Simple but elegant in learning some basic 6502 machine code functionality. It demonstrates a simple loop operation using a memory pointer. We set a pointer to our array in memory located at $1030. We are reading a string of unknown size so we will terminate the string with a NULL byte 00. 
 
H E L L O WORLD ! 0
 
This code starts with the first BYTE in the array, ArrayPtr[index = 0]. In 6502 machine code this looks like,
 
 1000     A2 00          LDX          #$00       REM Set the Array pointer to index the first value in the array "H"
 1002     BD 30 10       LDA          $1030,X    REM Using 6502 indexed addressing mode, LDA from $1030 + X (our index value offest)
    

Keep in mind, if you don't know already, the A2 00, and BD 30 10 values are the machine language opcodes. A2 means LDX to the 6502. BD is interpreted as LDA using indexed addressing. The above code retrieves the first byte of data from our arrayPtr = $1030 + 0 (index). We are using the 6502 processor's indexed addressing mode. We load the index offset of the memory we wish to access into the X register of the 6502 using the LDX (LoaDX) command. We load the 6502 accumulator (LDA) register using the indexed addressing mode.

LDA      $1030 +X

X register = 0

LDA     $1030 + 0          Accumulator register contains the BYTE "H" or we see 0x08

Now we have an indeterminate number of bytes to continue reading from memory. We know to stop reading BYTES when we read a NULL value of 00. If the BYTE value is not 00, then we want to write the BYTE to the screen. 

 
 1005     F0 07          BEQ          $100E      REM if the accumulaor contains 0, we have read a NULL byte and we're at the end of the array
 1007     9D 00 04       STA          $0400, x   REM write the BYTE to screen memory (starting at $0400) + index offset.
    

We are testing for the NULL BYTE value using the 6502, BEQ, Branch on EQual to zero instruction. If the accumulator holds the value 0x00, then the code flow needs to branch to the instruction at 0x100E, which terminates our code with the RTS, Return from Subroutine, instruction.

If the BYTE value is not 00, then write the BYTE to the screen memory location, 0x0400+index, to display it. The BYTE 0x08, "H", is written to 0x0400. The BYTE 0x05, "E", is written to location 0x0401, and so on for each byte.

The final portion of code increments the index by 1, INX, with each iteration until all the HELLO WORLD! bytes are read. When we call INX, the value in the X register is incremented by 1. This is how we increment our arrayPtr index to the next value in the BYTE  array. If we haven't read the NULL BYTE 00 we jump back to the instruction at 0x1002 to read the next BYTE. A very simple and eloquent example of 6502 machine code.

 
 100A     E8             INX
 100B     4C 02 10       JMP          $1002
 100E     60             RTS
    

Here is the listening in entirety if this brings clarity. Again, this is not my code. It is from Not a Wizard channel on YouTube. I just really liked the simplicity and illustration value of this code.

 
 1000     A2 00          LDX          #$00       REM Set the Array pointer to index the first value in the array "H"
 1002     BD 30 10       LDA          $1030,X    REM Using 6502 indexed addressing mode, LDA from $1030 + X (our index value offest)
 1005     F0 07          BEQ          $100E      REM if the accumulaor contains 0, we have read a NULL byte and we're at the end of the array
 1007     9D 00 04       STA          $0400, x   REM write the BYTE to screen memory (starting at $0400) + index offset.
 100A     E8             INX                     REM we haven't read a NULL byte, so increment the array index (index = index +1)
 100B     4C 02 10       JMP          $1002      REM JMP to instrunction at $1002 and read another BYTE from the array
 100E     60             RTS                     REM we're done, we read a NULL BYTE. RTS - return from subroutine
    

Typing the Program into the Monitor

 Now that we understand what the code is doing. Let's enter our program into the machine language monitor. To enter the code, merely type in the following from the machine language monitor.

 
A 1000 LDX #$00
    

This will put the monitor in 6502 assembly language input mode. Hit return after your first line of code and it will immediately move to the next line of memory as shown.

Here is the entire code listing to enter.

 
 A 1000     LDX          #$00       
 A 1002     LDA          $1030,X    
 A 1005     BEQ          $100E      
 A 1007     STA          $0400, x   
 A 100A     INX                     
 A 100B     JMP          $1002      
 A 100E     RTS                     
    

Go back and verify you have entered the assembly language instructions correctly before moving on.
If it's correct let's move on!

Using the Disassembler to Examine our Code

 
To disassemble our code use the following disassembler command.

 
 D 1000
    


Voila!

The Commodore 64 command SYS will execute our instruction. However, we need to specify the memory location of the start of our program at 0x1000. We are using the Commodore 64 BASIC interface and this uses base 10 decimal. Using your handy hex to decimal calculator, 0x1000 translates to 4096 base 10. To run our program, type SYS 4096!
 
 
 SYS 4096
    



Saving and Loading Code 

Obviously as the code becomes longer and more complex we will have the need to save and load code from virtual disk into the monitor memory. The commands S and L are used for saving and loading images respectively. 

To save our HELLOWORLD code, we will need to save the code and the array data. This spans from memory locations 0x1000 to 0x1040.

 
 S "HELLOWORLD",8,1000,1040
    

 To load HELLOWORLD from the virtual disk, we will need to specify the destination memory address to write the code to.

 
 L "HELLOWORLD",8,1000
    


That's All Folks!

Hopefully, this shed some light on 6502 programming and using virtual emulators. You don't need a physical Commodore 64 to write code and games. The emulators work just fine. I wouldn't mine having a physical Commodore 64 for my home artifacts. But for coding, I can get by with VICE. Anyone know how to write a cartridge and burn a ROM for it? I need to get this running on my Raspberry Pi 4 too. To be legit!

Mike
 



 

 

No comments:

Post a Comment