RE-POST February: SPO600 Week 3 Part 2

This post would be a continuation of what I have learned during my third week of Software Portability and Optimization (SPO600) class.

String Basics

In this 6502 emulator, the graphic display consists of 32x32 memory locations. the first memory location on the screen is at $0200

In the wiki 6502 emulator, there is a reference sheet that shows the number of each memory location on the display.

Besides the graphic display, there is also the text display, which works similarly to the pixel display:
The character display is located on pages $f0-$f7.

Example: Load the accumulator with the letter A.
To find the code for the letter A, in a Linux system, go to the terminal and type:
man ascii

However, since I am using Windows. In my Powershell terminal, I have to use the following command to get the ASCII table:
0..127 | ForEach-Object { [PSCustomObject]@{ Decimal = $_; Character = [char]$_; Hex = "{0:X2}" -f $_ } } | Format-Table -AutoSize

Decimal Character Hex
65 A 41
90 Z 5A
97 a 61
122 z 7A
13 ENTER 0D
32 SPACE 20
8 BACKSPACE 08

Then in the 6502 emulator:

    
LDA #$41
STA $F000


To display a message, the defined constant bytes can be used:

  
; Load Y register with 0 - start with 1st char
LDY #$00
LOOP:
    LDA MSG,Y
    STA $F000,Y ;store at start of the screen
    INY ; increment the register
    CPY #$08 ; compare the register Y with the number of characters that will be display
    BNE LOOP ; if it doesn't match, then branch back to loop
    BRK ; stop if we don't loop back

; label so that we can refer to this location memory
MSG:
    dcb "H","i",$20,"t","h","e","r","e"

We can take advantage of the ROM (Read Only Memory) routine instead of calculating the memory location manually to have another sentence below the first line.

The keyword define used in the emulator is like macros.

  
define SCINIT $ff81 ; initialize/clear screen
define CHRIN $ffcf ; input character from keyboard
define CHROUT $ffd2 ; output character to screen
define SCREEN $ffed ; get screen size
define PLOT $fff0 ; get/set cursor coordinates

JSR SCINIT ; jump to the subroutine
; Load Y register with 0 - start with 1st char
LDY #$00
LOOP:
    LDA MSG, Y
    JSR CHROUT ; output character to screen
    INY ; increment the register
    CPY #$08 ; compare the register Y with the number of characters that will be display
    BNE LOOP ; if it doesn't match, then branch back to loop
    BRK ; stop if we don't loop back

; label so that we can refer to this location memory
MSG:
    dcb "H","i",$20,"t","h","e","r","e"
Now to get a user input:


  
define SCINIT $ff81 ; initialize/clear screen
define CHRIN $ffcf ; input character from keyboard
define CHROUT $ffd2 ; output character to screen
define SCREEN $ffed ; get screen size
define PLOT $fff0 ; get/set cursor coordinates

JSR SCINIT ; jump to the subroutine
PRINT:
; Load Y register with 0 - start with 1st char
    LDY #$00
LOOP:
    LDA MSG, Y
    JSR CHROUT ; output character to screen
    INY ; increment the register
    CPY #$09 ; compare the register Y with the number of characters that will be display
    BNE LOOP ; if it doesn't match, then branch back to loop

GET_A_CHARACTER:
    JSR CHRIN ; get input character from user
    CMP #$00
    BEQ GET_A_CHARACTER ; will get an infinite loop until user type something

; When user press a key
    JMP PRINT ; the message would be printed everytime the user press a key

; label so that we can refer to this location memory
MSG:
    dcb "H","i",$20,"t","h","e","r","e",$0D

Note that the CPY is counting the number of character by hand. To avoid doing so, there are some options:
put a null character at the end of our string, which you can then get rid of the CPY line, instead check each character that comes in and load it from memory. If it's equal to 0, we jump to done.
  
define SCINIT $ff81 ; initialize/clear screen
define CHRIN $ffcf ; input character from keyboard
define CHROUT $ffd2 ; output character to screen
define SCREEN $ffed ; get screen size
define PLOT $fff0 ; get/set cursor coordinates

JSR SCINIT ; jump to the subroutine
PRINT:
; Load Y register with 0 - start with 1st char
    LDY #$00
LOOP:
    LDA MSG, Y
    BEQ DONE
    JSR CHROUT ; output character to screen
    INY ; increment the register

    BNE LOOP ; if it doesn't match, then branch back to loop

DONE:
    GET_A_CHARACTER:
        JSR CHRIN ; get input character from user
        CMP #$00
        BEQ GET_A_CHARACTER ; will get an infinite loop until user type something

; When user press a key
    JMP PRINT ; the message would be printed everytime the user press a key

; label so that we can refer to this location memory
MSG:
    dcb "H","i",$20,"t","h","e","r","e",$0D, $00

String Input

We would be creating a sort of wordle game:
  • User can input character
  • Add cursor block to make it more user-friendly
  • Convert lowercase to uppercase
  • Count character using the Y register which will be stored in memory location

  • Handle ENTER key
  • Handle BACKSPACE key

In assembler, the conditionals are written backward compared to most of the other programming languages where the if statement would skip the block of code under it, instead of running it.

Then let's get a prompt before starting to type:
There is a script folder where the file name make-dcb is located.
This can be used instead of manually typing the prompt with all the commas, parentheses, and letters separately.

First, go to a terminal, then type:
    
cd ~/git/6502js-code/scripts/
ll
echo "Enter a 5-letter word: " | ./make-dcb

Copy and paste the line that is given.

For diagnostic purposes, let's dump the input on our screen:
To figure out how much code we have, go to a linux terminal, in my case I am using Bash.
Then create a temporary file and put the code in:

  	
vi /tmp/a
wq
grep . /tmp/a -c
The code that was copy and pasted was:

    
; ROM routines
define SCINIT $ff81 ; initialize/clear screen
define CHRIN $ffcf ; input character from keyboard
define CHROUT $ffd2 ; output character to screen
define SCREEN $ffed ; get screen size
define PLOT $fff0 ; get/set cursor coordinates

; Memory locations
define INPUT $2000 ; input buffer (up to 5 chars)
JSR SCINIT

LDY #$00 ;initialize Y to 0

PROMPT_CHAR:
LDA PROMPT_TEXT,Y ; load A with the first character of our prompt
BEQ DONE_PROMPT ; if not null
JSR CHROUT ; print to screen
INY ; increment Y register
BNE PROMPT_CHAR ; if we haven't reached 256 char, we go back to the prompt

DONE_PROMPT:
JSR GET_INPUT

LDA #$0D
JSR CHROUT

LDY #$00
INPUT_PRINT:
LDA INPUT,Y
JSR CHROUT
INY
CPY #$05
BNE INPUT_PRINT

BRK

PROMPT_TEXT:
dcb "E","n","t","e","r",32,"a",32,"5","-","l","e"
dcb "t","t","e","r",32,"w","o","r","d",":",32, 00

; --------------------------------------------------------------------

GET_INPUT:
; Getting the black box on the screen
LDA #$A0 ; code for black space
JSR CHROUT

; Reposition cursor to be on top of the black box
LDA #$83 ; code to move cursor left one position
JSR CHROUT
LDY #$00 ; keep track of the count of char typed

GET_CHAR:
JSR CHRIN
CMP #$00
BEQ GET_CHAR

CMP #$08 ; compare input char with BACKSPACE
BNE CHECK_ENTER ; if not equal check for ENTER

CPY #$00 ; compare char count with 0
BEQ GET_CHAR ; if equal, get another char

; Dealing with the BACKSPACE black box
DEY ; decrement the Y register
LDA #$20 ; load A with white SPACE to remove the black box
JSR CHROUT
LDA #$83 ; code to move cursor left one position
JSR CHROUT
JSR CHROUT
LDA #$A0 ; code for black space
JSR CHROUT
LDA #$83 ; code to move cursor left one position
JSR CHROUT
CHECK_ENTER:
CMP #$0D ; compare input char with ENTER
BNE CHECK_LETTER ; if not enter, check for letters
CPY #$05 ; see if we have 5 char
BEQ DONE_INPUT ; if have 5 char and ENTER, we are done

CHECK_LETTER:
; Conversion of lowercase to uppercase
CMP #97 ; compare input char with 'a'
BCC UPPERCASE ; if lower, check if it's uppercase
CMP #123 ; compare input char with 'z' + 1
BCS GET_CHAR ; if higher, get another character

; difference between A and a is 32 decimal
SEC ; set carry flag
SBC #32 ; subtract 32 to convert lowercase

UPPERCASE:
CMP #65 ; letter A in decimal ; C=1 IF A>=65 ; C=0 IF A<65
BCC GET_CHAR ; branch carry cleared - get another character if lower than A
CMP #91 ; letter Z + 1 in decimal ; C=0 IF Z>=91 ; C=1 IF Z<91
BCS GET_CHAR ; branch if carry set - get another character if higher than Z
CPY #$05 ; check the count of characters (max 5)
BEQ GET_CHAR ; if it's 5, do not accept another char
STA INPUT,Y ; store character
JSR CHROUT ; print received letter on the screen
LDA #$A0 ; code for black space
JSR CHROUT
LDA #$83 ; code to move cursor left one position
JSR CHROUT
INY ; When received a valid character, increment Y count
JMP GET_CHAR

DONE_INPUT:
LDA #$20 ; load A with white SPACE to remove the black box
JSR CHROUT
RTS
This gives me a result of 93 lines.

The above code is the start of the wordle game. The code for the actual game can be found: https://github.com/ctyler/6502js-code/blob/master/wordle.6502


6502 Assembly Hack

When using the subroutines for printing, it is not necessary to use a pointer. The system figured out how to print the subroutine which is grabbed from the stack, the location in memory of our data, dealt with that data, and then set things to resume after that string when it returns from the subroutine.

Comments

Popular posts from this blog