Week 4A - 6502 Strings & Lab 3

Hamza TeliHamza Teli
13 min read

Hello! In this article, I will be talking about strings inside the 6502 processor. Before I dive into this aspect, let’s discuss what we mean by a string exactly.

What is a String?

A string is a data type that contains characters enclosed in double quotes. If you have experience programming, you are familiar with this concept. The text you see on the screen right now are strings. To send a message across, we communicate with words. Therefore, it makes sense that humans developed such machines capable of displaying strings on a screen. Likewise, I will discuss how strings are implemented inside the 6502 processor and also discuss lab 3. For lab 3, I will be creating a program that utilizes strings and the graphical display! I won’t tell you exactly what it is now. Stay tuned!

Strings inside a 6502 processor

In the 6502 processor, ASCI characters can be placed on a different range of pages, from $f0 to $f7 inside the emulator. Before we go into how strings are printed within the 6502 emulator. Let’s look into how to write a single character in the emulator to get a feel.

How to write a single character on the screen?

To write a single character, you must take the value of the ASCI character in hexadecimal and assign it at the designated memory address. As I’ve stated before, the characters can be mapped from address $f0 to $f7. Therefore, we can use an ASCI table that converts each value into its hexadecimal format and store it within those ranges. The example below loads the accumulator with the character A

; Here's an example of loading A
LDA #$41

; Now store it at the memory location we want
STA $f000

You will see the following output:

See how easy it is to print a letter on the screen. However, if we want to print an entire word it becomes very redundant printing letter by letter. We will end up with hundreds of lines of code. Also incrementing the memory location for the screen and keeping track of it for each letter is another redundant task. Thankfully we have loops and built in ROM routines to simply this.

ROM Routines

What are ROM routines?

ROM stands for Read-Only Memory. So, ROM routines are simply routines that are built inside the processors ROM that perform some type of functionality. An example of this can be system input/output. Can you imagine if you had to write the code for output again and again each time? ROM routines make our lives easier

Examples of ROM routines

There are a few ROM routines that Professor Chris Tyler went over in this weeks lecture. Please see below

; 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
  • SCINIT → simply initializes and clears the graphical display

  • CHRIN → takes an input character from your keyboard

  • CHROUT → outputs the character to the screen

  • SCREEN → returns the screen size

  • PLOT → Can be used to get or set the cursor coordinates

Lab 3 - Simple Game

In Lab 3, I created a simple program that lets the user guess a number from 0-9 and if it’s the correct number, it displays a green checkmark on the GUI. If it’s wrong, then it displays a red X on the GUI and prompts the user again until they get it right. Yes, it is a very simple game and can be improved a lot. I started off very simple and will explain the code structure as I go along. However, at the end, I will also explain how I can improve it, and what possible ways I have of adding such improvements/features.

Here is the code, it’s a bit long but do not worry, I will explain it bit by bit:

; Hamza's guess a number from 0-9 game
; In this game, the user enters a number. 
; --- If it's right, then a green checkmark will appear 
;     on the bit map.
; --- If it's wrong, then a red x will appear
;     on the bit map

; ----------------------------------------
; Please note that the ROM routines and logic for the input were taken from 
; Professor Chris Tyler's Week 4A Video on 6502 Strings. !!!!

; Define Zero page memory location
define COUNT $00
define INPUTTED_CHARACTER_LOCATION $01 ; Used to store the character input

; Character constants
define ENTER $0D
define BACKSPACE $08
define SPACE $20 ; White space
define BLACKSPACE $A0 ; Used for black space

; Define the number to guess
define NUMBER $02

; ROM Routines taken from SPO Wiki (Credit to Prof Chris Tyler)
 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
 define LEFT $83 ; Cursor left


; Step 1 (Print the welcome message)
PRINT_WELCOME_MESSAGE:
  JSR SCINIT
  LDY #$00
  LOOP_WELCOME_MESSAGE:
   LDA WELCOME_MESSAGE, Y
   BEQ GENERATE_RANDOM_NUMBER ; If String hits 0, we move on
   JSR CHROUT
   INY
   BNE LOOP_WELCOME_MESSAGE

; Step 2: Generate a random number (0-9) 
GENERATE_RANDOM_NUMBER:
 LDA $FE
 CHECK_RANDOM_NUMBER_LOOP:
 CMP #$0A
 BCC RANDOM_NUMBER_GENERATED
 CLC
 SBC #$0A
 JMP CHECK_RANDOM_NUMBER_LOOP

RANDOM_NUMBER_GENERATED:
 STA NUMBER
 JSR PRINT_INPUT_MESSAGE

; Step 3 (Print the input message)  
PRINT_INPUT_MESSAGE: 
 LDY #$00 ; Reset the Y register to 0
 LOOP_PRINT_INPUT_MESSAGE:
  LDA INPUT_MESSAGE, Y
  BEQ GET_NUMBER
  JSR CHROUT
  INY
  BNE LOOP_PRINT_INPUT_MESSAGE

; Step 4 (Get an input from user)
GET_NUMBER:
 LDA COUNT
 CMP #$00
 BEQ ACCEPT_ONE_DIGIT  ; If no digit has been accepted, get one.
 ; If COUNT is nonzero, a digit has already been accepted.
 JMP CHECK_INPUTTED_CHARACTER

ACCEPT_ONE_DIGIT:
 ; Black space functionality
 LDA #BLACKSPACE
 JSR CHROUT
 LDA #LEFT
 JSR CHROUT

 JSR CHRIN ; Get input
 CMP #$00 ; CMP If its 0
 BEQ GET_NUMBER

 ; This validation ensures number entered is 0-9 (Used logic from Week 3 video)
 CMP #$30 ; Minimum number 0
 BMI GET_NUMBER
 CMP #$3A ; Max number
 BPL GET_NUMBER

 STA INPUTTED_CHARACTER_LOCATION ; Store the input in the macro above
 JSR CHROUT
 INC COUNT
 JMP CHECK_INPUTTED_CHARACTER

; Step 5: Check if character is right 
CHECK_INPUTTED_CHARACTER:
 LDA INPUTTED_CHARACTER_LOCATION
 SEC
 SBC #$30 
 CMP NUMBER
 BEQ PERFORM_CORRECT_ACTION
 JMP PERFORM_INCORRECT_ACTION

; Printing actions --------------------------
PRINT_CORRECT_MESSAGE:
 LDY #$00
 LOOP_PRINT_CORRECT_MESSAGE:
  LDA CORRECT_INPUT_MESSAGE, Y
  BEQ EXIT
  JSR CHROUT
  INY
  JMP LOOP_PRINT_CORRECT_MESSAGE

PRINT_INCORRECT_MESSAGE:
 ; Reset the counter 
 LDA #$00
 STA COUNT
 LDY #$00
 LOOP_PRINT_INCORRECT_MESSAGE:
  LDA INCORRECT_MESSAGE, Y
  BEQ PRINT_INPUT_MESSAGE
  JSR CHROUT
  INY
  BNE LOOP_PRINT_INCORRECT_MESSAGE
;-------------------------------------------

; This performs the incorrect message and draws a red X  
PERFORM_INCORRECT_ACTION:
; This draws a red X on the screen (pretty inefficient way)
DRAW_X:
 LDA #$a ; Red color
 ; Diagnoal Line 1
 STA $0200
 STA $0221
 STA $0242
 STA $0263
 STA $0284

 ; Diagonal Line 2
 STA $0204
 STA $0223
 STA $0261
 STA $0280
 JSR PRINT_INCORRECT_MESSAGE

; This performs the correct message and draws a green checkmark
PERFORM_CORRECT_ACTION:
; Reset previous checkmark to black first
LDA #$00
; Diagnoal Line 1
 STA $0200
 STA $0221
 STA $0242
 STA $0263
 STA $0284
 ; Diagonal Line 2
 STA $0204
 STA $0223
 STA $0261
 STA $0280

; This draws a checkmark
DRAW_CHECKMARK:
 LDA #$5
 STA $0284
 STA $02a5

 ; Part 2 of checkmark
 STA $0286
 STA $0267
 STA $0248
 STA $0229
 JSR PRINT_CORRECT_MESSAGE


; Exit the break
EXIT: brk




WELCOME_MESSAGE:
dcb "W", "e", "l", "c", "o", "m", "e", 32, "T", "o", 32, "H", "a", "m", "z", "a", "'", "s", 32, "G", "a", "m", "e", $0D, $0D, "I", "n", 32, "t", "h", "i", "s", 32, "g", "a", "m", "e", 32, "y", "o", "u", 32, "s", "i", "m", "p", "l", "y", 32, "g", "u", "e", "s", "s", 32, "a", 32, "n", "u", "m", "b", "e", "r", 32, "f", "r", "o", "m", 32, "0", "-", "9", $0D, $0D, 00 

INPUT_MESSAGE:
dcb "E", "n", "t", "e", "r", 32, "a", 32, "n", "u", "m", "b", "e", "r", 32, "(", "0", "-","9", ")", ":", 32, 00

CORRECT_INPUT_MESSAGE:
dcb $0D,$0D, "C","o","r","r","e","c", "t","!", 00

INCORRECT_MESSAGE:
dcb $0D, "W", "r", "o", "n", "g", 32, "N", "u", "m", "b", "e", "r", $0D, 00

Let’s break the code down!. To make things simpler, I have created steps to identify the flow of the program which are listed below. There are 5 main steps for this program.

  1. Print the welcome message

  2. Generate a random number from 0-9

  3. Print input message

  4. Get input from user

  5. Check if inputted character is right

Step 0: Defining Macros and Initial Setup

; Define Zero page memory location
define COUNT $00
define INPUTTED_CHARACTER_LOCATION $01 ; Used to store the character input

; Character constants
define ENTER $0D
define BACKSPACE $08
define SPACE $20 ; White space
define BLACKSPACE $A0 ; Used for black space

; Define the number to guess
define NUMBER $02

; ROM Routines taken from SPO Wiki (Credit to Prof Chris Tyler)
 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
 define LEFT $83 ; Cursor left

So what are all these define words? This part of the program simply allows us to create MACROS that either store a value at a location or where we can simply refer a macro to an area in memory. For example, I defined a macro/variable called COUNT and INPUTTED_CHARACTER_LOCATION. The COUNT keeps track of how many characters were entered, INPUTTED_CHARACTER_LOCATION holds the value of the character inputted. So far, pretty straightforward right? Likewise, I created a variable to store the number that the program will randomly generate in NUMBER. These are the main macros that I will discuss, the rest have been explained before.

Step 1: Printing the messages

In the previous section of this article, I described how to print messages in code. In this program there are a total of 4 messages that I print: WELCOME_MESSAGE, INPUT_MESSAGE, CORRECT_INPUT_MESSAGE, INCORRECT_MESSAGE . These messages are printed using the same logic. Below is an example of one:

; Step 1 (Print the welcome message)
PRINT_WELCOME_MESSAGE:
  JSR SCINIT ; This clears the screen
  LDY #$00 ; Load Y register with 0
  LOOP_WELCOME_MESSAGE:
   LDA WELCOME_MESSAGE, Y
   BEQ GENERATE_RANDOM_NUMBER ; If String hits 0, we move on to step 2!
   JSR CHROUT
   INY
   BNE LOOP_WELCOME_MESSAGE ; Continue loop if WELCOME_MESSAGE ISN'T DONE!

Step 2: Generate a random number

In this step, I tell the processor to generate a random number from 0-9 and store it inside the NUMBER macro defined at the top.

; Step 2: Generate a random number (0-9) 
GENERATE_RANDOM_NUMBER:
 LDA $FE
 CHECK_RANDOM_NUMBER_LOOP:
 CMP #$0A
 BCC RANDOM_NUMBER_GENERATED
 CLC
 SBC #$0A
 JMP CHECK_RANDOM_NUMBER_LOOP

RANDOM_NUMBER_GENERATED:
 STA NUMBER
 JSR PRINT_INPUT_MESSAGE

So how exactly does this work? We first load the accumulator with data from address $FE as in the SPO600 wiki, Professor Chris Tyler mentioned this is where random bytes are available. I then created a loop that compares the random value inside the accumulator with less than 10. BCC means the condition is met, we simply store the value at NUMBER and move one to Step 3. If not, it simply clears the carry flag and subtracts 10 until the condition is met.

Step 3: Print Input Message

For this step, I will not go into detail as it is essentially the same logic as Step 1.

; Step 3 (Print the input message)  
PRINT_INPUT_MESSAGE: 
 LDY #$00 ; Reset the Y register to 0
 LOOP_PRINT_INPUT_MESSAGE:
  LDA INPUT_MESSAGE, Y
  BEQ GET_NUMBER
  JSR CHROUT
  INY
  BNE LOOP_PRINT_INPUT_MESSAGE

Step 4: Get Input


; Step 4 (Get an input from user)
GET_NUMBER:
 LDA COUNT
 CMP #$00
 BEQ ACCEPT_ONE_DIGIT  ; If no digit has been accepted, get one.
 ; If COUNT is nonzero, a digit has already been accepted.
 JMP CHECK_INPUTTED_CHARACTER

ACCEPT_ONE_DIGIT:
 ; Black space functionality
 LDA #BLACKSPACE
 JSR CHROUT
 LDA #LEFT
 JSR CHROUT

 JSR CHRIN ; Get input
 CMP #$00 ; CMP If its 0
 BEQ GET_NUMBER

 ; This validation ensures number entered is 0-9 (Used logic from Week 3 video)
 CMP #$30 ; Minimum number 0
 BMI GET_NUMBER
 CMP #$3A ; Max number
 BPL GET_NUMBER

 STA INPUTTED_CHARACTER_LOCATION ; Store the input in the macro above
 JSR CHROUT
 INC COUNT
 JMP CHECK_INPUTTED_CHARACTER

Do not get scared looking at the code above. I did when I started coding, but I built it piece by piece and it all made sense after. Lets break it down together. We start off by getting the value of COUNT and comparing it to see what we need to do next. Remember, our program only takes 1 value, therefore if COUNT is greater than 0. We move on and check the inputted character!

When you first start the program, COUNT will be 0. The program will execute the section of code prompting for user input. This section is quite simple. We first load the accumulator with the value for BLACKSPACE and output it using the ROM routine CHROUT. We also do the same for the LEFT cursor to provide the user with a true input experience. We then get the input using CHRIN and check if its 0, if so we retry by calling GET_NUMBER again.

The program then checks if the value entered was 0-9, it does this using the branch BMI to check if its below 0, and BPL if its above 9. If either of these conditions are met, it calls GET_NUMBER again.

If the number inputted is valid, we simply store this value at INPUTTED_CHARACTER_LOCATION, display the character using CHROUT, increment COUNT, and move on to the subroutine CHECK_INPUTTED_CHARACTER.

Step 5: Check if inputted character is right

In this step, the program checks if the inputted character matches the value stored at NUMBER. If so, it prints Correct and displays a green checkmark. Otherwise, it displays an invalid message and displays a red X on the GUI.


; Step 5: Check if character is right 
CHECK_INPUTTED_CHARACTER:
 LDA INPUTTED_CHARACTER_LOCATION
 SEC
 SBC #$30 
 CMP NUMBER
 BEQ PERFORM_CORRECT_ACTION
 JMP PERFORM_INCORRECT_ACTION

; Printing actions --------------------------
PRINT_CORRECT_MESSAGE:
 LDY #$00
 LOOP_PRINT_CORRECT_MESSAGE:
  LDA CORRECT_INPUT_MESSAGE, Y
  BEQ EXIT
  JSR CHROUT
  INY
  JMP LOOP_PRINT_CORRECT_MESSAGE

PRINT_INCORRECT_MESSAGE:
 ; Reset the counter 
 LDA #$00
 STA COUNT
 LDY #$00
 LOOP_PRINT_INCORRECT_MESSAGE:
  LDA INCORRECT_MESSAGE, Y
  BEQ PRINT_INPUT_MESSAGE
  JSR CHROUT
  INY
  BNE LOOP_PRINT_INCORRECT_MESSAGE
;-------------------------------------------

; This performs the incorrect message and draws a red X  
PERFORM_INCORRECT_ACTION:
; This draws a red X on the screen (pretty inefficient way)
DRAW_X:
 LDA #$a ; Red color
 ; Diagnoal Line 1
 STA $0200
 STA $0221
 STA $0242
 STA $0263
 STA $0284

 ; Diagonal Line 2
 STA $0204
 STA $0223
 STA $0261
 STA $0280
 JSR PRINT_INCORRECT_MESSAGE

; This performs the correct message and draws a green checkmark
PERFORM_CORRECT_ACTION:
; Reset previous checkmark to black first
LDA #$00
; Diagnoal Line 1
 STA $0200
 STA $0221
 STA $0242
 STA $0263
 STA $0284
 ; Diagonal Line 2
 STA $0204
 STA $0223
 STA $0261
 STA $0280

; This draws a checkmark
DRAW_CHECKMARK:
 LDA #$5
 STA $0284
 STA $02a5

 ; Part 2 of checkmark
 STA $0286
 STA $0267
 STA $0248
 STA $0229
 JSR PRINT_CORRECT_MESSAGE


; Exit the break
EXIT: brk

The code above may seem complex, but really it’s not. All its doing is, comparing the value at NUMBER with INPUTTED_CHARACTER_LOCATION. If they match it calls the routine PERFORM_CORRECT_ACTION. If not, it calls the routine PERFORM_INCORRECT_ACTION. Both actions, work in similar ways, whereby it draws a figure on the GUI and prints the corresponding message. However, note that the PERFORM_CORRECT_ACTION resets the GUI by filling in the resetting the X checkmark with the color black then prints the green checkmark.

Here are pictures of the program.

  1. When you enter a wrong input:

  2. When you enter the right input

Reflection

This lab was a great learning experience. I highly recommend all readers to attempt to create a program that utilizes both the character input and output as well as the GUI. Reason being is as programmers we are so used to creating programs in different ways. Coding the 6502 processor is an entirely different experience. Yes, it will take you long (you may cry in between). But when you finish, you will be extremely proud even if it is the simplest of programs like mine. In conclusion, I learned quite a bit by completing this lab. I took it step by step and built something simple but it works.

Future Improvements

I have a few ideas for enhancements for my program that would make it a lot more interesting and fun to play:

  1. I would implement a health counter and for each value you input wrong, the health counter decrements and the GUI is also updated

  2. Increase the range of numbers to guess to 0-100

  3. Align the figures to the center of the screen

  4. Add a high score counter

References

Image by Pexels from Pixabay

Professor Chris Tyler’s Examples of Code that helped me substantially can be found here. I highly recommend you use this to learn! (Don’t forget to give credit where it’s due though!)

0
Subscribe to my newsletter

Read articles from Hamza Teli directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Hamza Teli
Hamza Teli