Writing PE6502 Assembler code - Useful tips

Post your PE6502 usage suggestions/questions, and also topics related to PE6502 software here.

Moderator: Corneleous

Writing PE6502 Assembler code - Useful tips

Postby Voyager_sput » Wed May 01, 2019 2:10 pm

Hello everyone,

I really like the PE6502 and am using the opportunity to learn how to program in the most basic level possible. However I've hit several problems while programming. There are some good tutorials out there on how to write 6502 assembly, but not all of it is as helpful as it might appear. explanations on how certain functions work, vary from one 6502 based computer to another. For example a book on the Apple II I found, says the output to monitor register is $FDED, which does nothing on the PE6502. I'll write down a few issues i've had while learing to code for the PE6502. Hopefully some of you will find some part of this usefull! Note that i'm not explaining 6502 opcodes, just my way on how to use them for some cool stuff :mrgreen:

Writing text on screen:
The first problem I ran into was how to put some text on the screen. Writing values in registry, manipulating them and moving them around is one thing, but how do you actually print something?
The short anwser for the PE6502 is: $FFEF

Just dump the ASCII value of the character you want to put on the screen in the Accumulator (A) and do a JSR to $FFEF. This will read whatever is in the Accumulator and print it in screen. Like this:
Code: Select all
     LDA #'A'
     JSR $FFEF

Note, that LDA #'A' is already something that has to be interpreted by the assembler of your choice. I use Win2C64 (http://www.aartbik.com/MISC/c64.html). This is a lightweight assembler for the 65XX family and works well for my needs, but others are CC65 and VASM. Your mileage might vary with the example above. But you could also just do LDA #$C1 (which is equal to the ASCII value of A).

Now, dumping one letter on the screen is nice, but what about a whole sentence, or a carriage return?

Carriage return is easy, just dump $0D to $FFEF and voila!

Now a whole sentence is harder, you can repeat the LDA and JSR commands but that looks horrible and cluttered. Plus what if you want to print something twice? You don't want to dump the same print commands in multiple times!

Now I solve this with a small function that starts reading at a given memory address and keeps printing characters, until it hits a 0. I use this:
Code: Select all
   
        CLV             ;CLEAR OVERFLOW FLAG
   BVC LPRINTZ         ;BRANCH ON OVERFLOW CLEAR (WHICH IT ALWAYS IS!)
LPRINT
ZPADDR   .EQU   $00      ;POINTER TO SET THE START BYTE LOCATION FOR THE 2 BYTE MEM ADDRESS (WHICH WILL STORE THE STRINGS MEMORY START AT $0000 AND $0001)
   STX ZPADDR         ;TEMP STORE START MEM ADDRESS OF TEXT IN $00 AND $01
   STY ZPADDR+1
   LDY #0            ;RESET Y FOR LOOPING THROUGH THE STRING
LPRINTA
   LDA (ZPADDR),Y      ;LOAD A STRING CHARACTER FROM ADDRESS STORED IN POINTER ZPADDR AND ZPADDR+1 ($00 AND $01 IN THIS CASE)
   CMP #0
   BEQ LPRINTY         ;IF EQUAL TO 0, DONE PRINTING TEXT
   JSR CHROUT         ;PRINT ON SCREEN
   INY               ;NEXT LETTER
   CLV
   BVC LPRINTA         ;ALWAYS JUMP
LPRINTY               ;SUBROUTINE TERMINATOR (GO BACK WHERE YOU CAME FROM BEFORE!)
   RTS
LPRINTZ               ;JUST SKIPPING OVER FUNCTION, NO RTS OTHERWISE BACK TO PROMPT!

And you call for the command using

Code: Select all
   
        LDX #<OPENA         ;PRINT THE INTRO TEXT ON SCREEN
   LDY #>OPENA
   JSR LPRINT


And here's the sentence itself:
Code: Select all
OPENA      .BYTE   "HANGMAN",CR,"REWRITTEN FOR 6502 ASSEMBLER",CR,CR,CR,0


Now this takes a bit of explaining. First of all, I use the .BYTE command in my assembler to define a label/pointer (OPENA) and then put some text in with some Carriage returns in there as well (CR is a simple label for $0D), ending with a 0. When you want to print you have two problems, you don't know where in memory the BYTE command will be stored in the compiled code. The second problem is that you can store values in and from memory but the registers in the 6502 are 8-bits (one byte) and the memory bus is 16-bits (two bytes). This poses a interesting issue on how to find the memory address, store the memory address and restore it in the function to write some text on the screen.

When I want to print the text I run LDX #<OPENA and LDY #>OPENA. Which temporarily stores the low-byte of the memory address where OPENA starts in register X and the high-byte of the memory address of OPENA in register Y.

Then we move to the function. The first part is easy and can be ignored:
Code: Select all
        CLV             
   BVC LPRINTZ

This is just my way of creating a function/subroutine, some piece of code that won't run until I want to run it. These two commands just make sure that when the code gets run and it hits the function, i'll just skip right over the whole function. LPRINTZ is a label at the end of the function. There might be better ways to do this, but by clearing the overflow flag in the 6502 with CLV and then check if the overflow flag is cleared with BVC we always branch to label LPRINTZ in this case.

The next part is a little more interesting:
Code: Select all
LPRINT
ZPADDR   .EQU   $00      ;POINTER TO SET THE START BYTE LOCATION FOR THE 2 BYTE MEM ADDRESS (WHICH WILL STORE THE STRINGS MEMORY START AT $0000 AND $0001)
   STX ZPADDR         ;TEMP STORE START MEM ADDRESS OF TEXT IN $00 AND $01
   STY ZPADDR+1

LPRINT is just the label I JSR to when I want to run the code in the function. The next is a pointer definition, telling the assembler that pointer ZPADDR will be equal to memory address $00. In the next two commands take the values we stored in STX (the low-byte) in ZPADDR (which is $0000) and the high-byte from Y using STY and put it in ZPADDR+1 (which is $0001). Now if you were to run this through the PE6502 you might get confused because its in backwards... For example when i assemble this, the text starts at memory address $1164, but i'm storing $6411 in my zero page memory $0000 and $0001. This is intended actually, since the 6502 is a little-endian system, the memory address has to be reversed in this case of it doesn't read the correctly data later on!

The last part we can do in one go:
Code: Select all
   LDY #0            ;RESET Y FOR LOOPING THROUGH THE STRING
LPRINTA
   LDA (ZPADDR),Y      ;LOAD A STRING CHARACTER FROM ADDRESS STORED IN POINTER ZPADDR AND ZPADDR+1 ($00 AND $01 IN THIS CASE)
   CMP #0
   BEQ LPRINTY         ;IF EQUAL TO 0, DONE PRINTING TEXT
   JSR CHROUT         ;PRINT ON SCREEN
   INY               ;NEXT LETTER
   CLV
   BVC LPRINTA         ;ALWAYS JUMP
LPRINTY               ;SUBROUTINE TERMINATOR (GO BACK WHERE YOU CAME FROM BEFORE!)
   RTS
LPRINTZ               ;JUST SKIPPING OVER FUNCTION, NO RTS OTHERWISE BACK TO PROMPT!

First we reset the Y register, which I will use in this case as a counter for the memory we have to read. Then we get a label LPRINTA which is nothing more then where our loop will return to while processing all the text. Now we load the text from memory. For this I use the Y register specifically, because I want to use Postindexed Indirect Addressing (Don't ask me to explain it to deeply, I barely understand myself, but read here http://www.chibiakumas.com/6502/ for some excellent explanation!). Basically i'm starting at the memory address located in ZPADDR ($0000 and $0001, but not read that byte specifically, but offset by Y. So in my example the first time it reads the value in $1164, the next loop it reads the value in $1165 and so forth.

Then all it does is compare it to the number 0 and if the value is a 0, it ends the function by branching to LPRINTY using CMP #0 and BEQ LPRINTY. If it keeps on going (value is not 0), it will print the letter on screen (JSR CHROUT, which is a pointer for $FFEF), increment the Y register for the next letter to read and do the same trick with CLV and BVC to branch back to LPRINTA to do the loop again. LPRINTY only has one opcode after it, RTS, just to go back to when I JSR to the function afterwards!

That's it!

A simple function to print whatever text you want, as long as your assembler understands labels and pointers like .BYTE. The exact syntax might vary per assembler!

Returning to WozMon
One last small thing, I have been faced with my code running fine but the PE6502 getting stuck afterwards, not accepting any key combinations and me having to reset the CPU or Propeller with the switches to keep going. I noticed this was because the RAM seems to be full of gabled data. When I run a Apple 1 emulator, like Pom1, all the ram is filled with nice 00's everywhere, but there's random data in the PE6502's RAM everywhere. I hardly think this is by design, and the emulator is probably just too clean.

Just to make sure you can keep using the PE6502 after running your program, try jumping to the starting address of WozMon as your last command. Wozmon starts at $FF00.

Code: Select all
JMP $FF00


That's it, I hope this helps anyone else. If there are no objections i'll add more topics like this for other functions, with capturing keyboard input next!

Take care, Marc
Voyager_sput
 
Posts: 2
Joined: Thu Apr 25, 2019 1:33 pm

Re: Writing PE6502 Assembler code - Useful tips

Postby Voyager_sput » Wed May 08, 2019 9:03 am

Adding a little more to this topic, the next part is Reading the keyboard buffer.

The keyboard buffer consists of two memory addresses, $D010 and $D011. How it works is that when you press a key on your keyboard the $D010 address will store the actual key that you pressed, but reading that memory address when you want to capture the input of a user is not very useful as when you press the A key, it will capture something closer to AAAAAAAAAAAAAAAAAA.

Now, when a key is pressed, and not "read" by programming code, $D011 is also set. To capture keyboard input all you'll need to do is check if the $D011 address contains any value of $80 or higher, this means it's an ASCII character. If $D011 contains a value of less then $80, then no key is actually pressed.

Now the act of actually reading the keyboard at $D010 will reset $D011. So you'll just have to keep watching $D011, if the value is less then $80, keep checking, if it's $80 or more, read the key from $D010 and process it as you wish.

Putting this into something practical, I'm using a subroutine to capture user input, show the key on screen (so the user has a some feedback), store the keys pressed in a part of zero page memory as a 'character buffer' until the enter key is pressed and then it returns from the zero page and you can do whatever you want with the input in your program. Here's the code:

Code: Select all
CHRBUF   .EQU     $10
CHROUT   .EQU     $FFEF
   CLV             ;CLEAR OVERFLOW FLAG
   BVC LINPUTZ         ;BRANCH ON OVERFLOW CLEAR (WHICH IT ALWAYS IS!)
LINPUT
   LDX #0            ;RESET THE X COUNTER, KEEP TRACK OF NUMBER OF LETTERS ENTERED
LINPUTA
   LDA $D011         ;READ KEYBOARD STROBE!
   CMP #$80         ;CHECK IF THE BUFFER IS AT LEAST $80 (KERNEL ADDS $80 TO ASCII CHARACTER INPUTTED, ANYTHING BELOW $80 IS NO KEYBOARD INPUT)
   BCC LINPUTA         ;LOOP BACK TO CHECK BUFFER IF BELOW $80
   
   LDA $D010         ;LOAD THE KEY PRESSED IN ACCUMULATOR

   CMP #$8D         ;CHECK IF ENTER KEY IS ENTERED AS KEY, IF SO FINISH BY ADDING 00 AND LEAVE FUNCTION
   BEQ LINPUTB
   
   STA CHRBUF,X      ;IF ANYTHING ELSE THEN ENTER KEY. STORE KEY IN MEMORY BUFFER, OFFSET BY X VALUE
   INX               ;INCREMENT NUMBER OF STORED CHARACTERS X
   CPX #30            ;CHECK IF THIS WAS CHARACTER NUMBER 30, THEN WE FINISH AS WELL
   BEQ LINPUTB
   JSR CHROUT         ;OTHERWISE PRINT KEY ON SCREEN AND RETURN TO LOOP TO CAPTURE ANOTHER KEY
   CLV
   BVC LINPUTA         ;ALWAYS JUMP
LINPUTB
   LDA #0            ;FINISH OFF THE CHARACTER BUFFER BY ADDING 00 BEHIND THE LAST LETTER IN MEMORY
   STA CHRBUF,X
   STX CHRBUFC         ;AND STORE NUMBER OF CHARACHTERS IN LAST BYTE
LINPUTY               ;SUBROUTINE TERMINATOR (GO BACK WHERE YOU CAME FROM BEFORE!)
   RTS
LINPUTZ               ;JUST SKIPPING OVER FUNCTION, NO RTS OTHERWISE BACK TO PROMPT!


Now the first part is just the definitions of the memory to put letters in to print to screen as discussed above (CHROUT = $FFEF), I've reserved $10 as a place to start the keyboard input. Like the example above I start with some CLV and BVC commands to jump over the subroutine ones the program is run, so it will only start capturing the keyboard input when I actually do a JSR to LINPUT.

the rest of the code really doesn't need much more explanation then already in the comment next to the code itself. The only thing to note is that I end my keyboard input by putting a 0 at the end of the text. That way, when I want to read the input later on, I dont have to keep track of how long the input is, just keep reading until a 0 is read, then stop :)

Actually using this subroutine is as simple as jumping to it. For a practical example (also adding a question for the user to enter the date using the LPRINT function from above):

Code: Select all
        LDX #<ASKDATE      ;PRINT ASK FOR THE DATE
   LDY #>ASKDATE
   JSR LPRINT

   JSR LINPUT         ;CAPTURE USER INPUT

ASKDATE      .BYTE   "PLEASE ENTER THE DATE: ",CR,CR,0
Voyager_sput
 
Posts: 2
Joined: Thu Apr 25, 2019 1:33 pm


Return to PE6502 Single Board Computer - Usage and Software

Who is online

Users browsing this forum: No registered users and 0 guests

cron