Introduction
Welcome back! We’ll be concluding the 6502 series by creating a program that covers 3 main topics:
- Character output
- Character input
- A form of arithmetic (e.g. add, subtract, bitwise operations, etc.)
I decided to create a “simple” addition calculator. I quote “simple” because adding is on pencil and paper, and in most programming languages goes something like this:
1 + 1 = 2
When performing addition on paper, we add the ones, carry the ten (if any), add the tens, etc. Luckily 6502 assembly follows a similar process but with added complexity of memory and bit management. I’ll explain how the addition works along the way. Let’s jump straight into the program!
Addition Calculator
Overview
Here’s the entirety of the code, feel free to use it in the 6502 emulator. Don’t worry, I’ll explain how it works in the following sections!
Note: the character in and out sections of the code were taken from SPO600 week 4 class demonstration and can be found here.
; Predefined ROM Routines, found in their respective addresses
define SCINIT $ff81
define CHRIN $ffcf
define CHROUT $ffd2
define SCREEN $ffed
define PLOT $fff0
; Zero page memory locations
define INPUT1 $0010
define INPUT2 $0011
define RESULT $0012
; ASCII Character constants
define BACKSPACE $08
define ENTER $0D
define SPACE $20
define BLACK $A0
define LEFT $83
; CODE TO TEST INPUT
START:
JSR SCINIT ; Initialize screen
LDY #$00 ; Index for first number
LDX #$00 ; Index for second number
; Display the initial message only once
LOOPFIRST:
LDA PROMPTFIRST,Y
BEQ GETFIRST
JSR CHROUT
INY
JMP LOOPFIRST
GETFIRST:
JSR INPUTFIRST ; Get first digit
LDY #$00
LOOPSECOND:
LDA PROMPTSECOND,Y
BEQ GETSECOND
JSR CHROUT
INY
JMP LOOPSECOND
GETSECOND:
JSR INPUTSECOND ; Get second digit
LDY #$00
; Convert ASCII to number and add
LDA INPUT1
SEC
SBC #$30 ; Convert ASCII to numerical value
STA INPUT1 ; Store numerical value
LDA INPUT2
SEC
SBC #$30 ; Convert ASCII to numerical value
STA INPUT2 ; Store numerical value
CLC ; Clear carry flag before addition
LDA INPUT1
ADC INPUT2 ; Add the two numbers
STA RESULT ; Store result
LDY #$00
LOOPCONFIRM:
LDA CONFIRM,Y
BEQ PRINT_RESULT
JSR CHROUT
INY
JMP LOOPCONFIRM
PRINT_RESULT:
LDA RESULT ; Load the result
CMP #10 ; Check if the result is greater than or equal to 10
BPL TWO_DIGITS ; If yes -> handle two digits
; Single-digit result
CLC ; Clear carry flag before ASCII conversion
ADC #$30 ; Convert to ASCII
JSR CHROUT ; Display the result
JMP END_PROGRAM
; Handle two-digit result
TWO_DIGITS:
SEC
SBC #10 ; Subtract 10 to get the units digit
PHA ; Save the units digit
LDA #$31 ; Load ASCII for '1' (tens digit)
JSR CHROUT ; Display the tens digit
PLA ; Restore the units digit
CLC ; Clear carry flag before ASCII conversion
ADC #$30 ; Convert to ASCII
JSR CHROUT ; Display the units digit
END_PROGRAM:
BRK ; End program
; PROMPTS
PROMPTFIRST:
dcb "S", "P", "O", "6", "0", "0", 32, "L", "a", "b", 32, "3", 32, "-", 32, "A", "d", "d", 32, "2", 32, "N", "u", "m", "b", "e", "r", "s", $0D
dcb "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", "=", $0D
dcb "E", "N", "T", "E", "R", 32, "F", "I", "R", "S", "T", 32, "D", "I", "G", "I", "T", 32, "(", "0", "-", "9", ")", ":", 32, $00
PROMPTSECOND:
dcb $0D, "E", "N", "T", "E", "R", 32, "S", "E", "C", "O", "N", "D", 32, "D", "I", "G", "I", "T", 32, "(", "0", "-", "9", ")", ":", 32, $00
CONFIRM:
dcb $0D, $0D, "S", "U", "M", ":", 32, $00
; Subroutine to input first number
INPUTFIRST:
LDY #$00
GETKEY1:
LDA #BLACK ; Black cursor color
JSR CHROUT
LDA #LEFT
JSR CHROUT
JSR CHRIN ; Get character from input
CMP #$00 ; Check if key pressed
BEQ GETKEY1
CMP #BACKSPACE ; Check for backspace
BNE CHECKENTER1 ; If not backspace, proceed
; Handle backspace
CPY #$00 ; If nothing to delete, ignore
BEQ GETKEY1
DEY ; Move the cursor back by one
LDA #SPACE ; Clear the previous character using the SPACE character
JSR CHROUT ; Display the space to erase the character
LDA #LEFT ; Move cursor back one space
JSR CHROUT ; Apply cursor move
JSR CHROUT ; Redraw the cursor
JMP GETKEY1
CHECKENTER1:
CMP #ENTER
BEQ CHECKCOUNT1
CHECKLETTER1:
CMP #$30 ; Check if character is less than '0'
BMI GETKEY1 ; If less, reject it
CMP #$3A ; Check if character is greater than '9' ($39 + 1)
BPL GETKEY1 ; If greater, reject it
CPY #$01 ; Check if we already have one digit
BEQ GETKEY1 ; If yes, reject additional input
STA INPUT1,Y ; Store the valid digit
JSR CHROUT ; Display the character
INY
JMP GETKEY1
CHECKCOUNT1:
CPY #1 ; Check if we have exactly one digit
BNE GETKEY1 ; If not, continue waiting
RTS
; Subroutine to input second number
INPUTSECOND:
LDY #$00
GETKEY2:
LDA #BLACK
JSR CHROUT
LDA #LEFT
JSR CHROUT
JSR CHRIN ; Get character from input
CMP #$00 ; Check if key pressed
BEQ GETKEY2 ; If no key pressed, continue waiting
CMP #BACKSPACE ; Check for backspace
BNE CHECKENTER2 ; If not backspace, proceed
; Handle backspace
CPY #$00 ; If nothing to delete, ignore
BEQ GETKEY2
DEY ; Move the cursor back by one
LDA #SPACE ; Clear the previous character using the SPACE character
JSR CHROUT ; Display the space to erase the character
LDA #LEFT ; Move cursor back one space
JSR CHROUT ; Apply cursor move
JSR CHROUT ; Redraw the cursor
JMP GETKEY2
CHECKENTER2:
CMP #ENTER
BEQ CHECKCOUNT2
CHECKLETTER2:
CMP #$30 ; Check if character is less than '0'
BMI GETKEY2 ; If less, reject it
CMP #$3A ; Check if character is greater than '9' ($39 + 1)
BPL GETKEY2 ; If greater, reject it
CPY #$01 ; Check if we already have one digit
BEQ GETKEY2 ; If yes, reject additional input
STA INPUT2,Y ; Store the valid digit
JSR CHROUT ; Display the character
INY
JMP GETKEY2
CHECKCOUNT2:
CPY #1 ; Check if we have exactly one digit
BNE GETKEY2 ; If not, continue waiting
RTS
Macros
Let’s start with understanding the macros in the program, those with the define
keyword.
; Predefined ROM Routines, found in their respective addresses
define SCINIT $ff81
define CHRIN $ffcf
define CHROUT $ffd2
define SCREEN $ffed
define PLOT $fff0
; Zero page variables
define INPUT1 $0010
define INPUT2 $0011
define RESULT $0012
; ASCII Character constants
define BACKSPACE $08
define ENTER $0D
define SPACE $20
define BLACK $A0
define LEFT $83
ROM Routines
ROM routines are predefined routines that can be accessed by their respective addresses. It is similar to the standard libraries in C/C++
where functions are written to handle tedious processes.
Let’s quickly go through each ROM routine, listed here:
-
SCINIT
— initialize screen, clearing data in the bitmap display -
CHRIN
— character in, input keyboard characters -
CHROUT
— output character to the screen -
PLOT
— get/set cursor coordinates
Zero Page Variables
Zero page variables are special because they are located in the first 256 bytes of memory ($0000 - $00FF
) and therefore have faster access times relative to non-zero page variables. INPUT1
, INPUT2
, and RESULT
will hold the first number, second number, and sum respectively.
ASCII Characters
Characters are read in their hexadecimal representation. To read user input, the value can be compared to one of the ASCII characters and the appropriate action can be executed (e.g. BACKSPACE
removes a character from the screen). In the code, each key is encoded as their respective hexadecimal value.
Reference of ASCII characters: Link
User Input
There are a couple of key checks in the user input:
- Check if input ranges from 0-9
- Handling the
ENTER
key - Handling the
BACKSPACE
key
; Subroutine to input first number
INPUTFIRST:
LDY #$00
GETKEY1:
LDA #BLACK ; Black cursor color
JSR CHROUT
LDA #LEFT
JSR CHROUT
JSR CHRIN ; Get character from input
CMP #$00 ; Check if key pressed
BEQ GETKEY1
CMP #BACKSPACE ; Check for backspace
BNE CHECKENTER1 ; If not backspace, proceed
; Handle backspace
CPY #$00 ; If nothing to delete, ignore
BEQ GETKEY1
DEY ; Move the cursor back by one
LDA #SPACE ; Clear the previous character using the SPACE character
JSR CHROUT ; Display the space to erase the character
LDA #LEFT ; Move cursor back one space
JSR CHROUT ; Apply cursor move
JSR CHROUT ; Redraw the cursor
JMP GETKEY1
CHECKENTER1:
CMP #ENTER
BEQ CHECKCOUNT1
CHECKLETTER1:
CMP #$30 ; Check if character is less than '0'
BMI GETKEY1 ; If less, reject it
CMP #$3A ; Check if character is greater than '9' ($39 + 1)
BPL GETKEY1 ; If greater, reject it
CPY #$01 ; Check if we already have one digit
BEQ GETKEY1 ; If yes, reject additional input
STA INPUT1,Y ; Store the valid digit
JSR CHROUT ; Display the character
INY
JMP GETKEY1
CHECKCOUNT1:
CPY #1 ; Check if we have exactly one digit
BNE GETKEY1 ; If not, continue waiting
RTS
The Y
register acts as an index in order to access the characters in the PROMPT
instructions, as well as keeping track of input length.
Note: the input is being compared in their respective hexadecimal values.
GETKEY
GETKEY
validates the input in the accumulator. First it prints out the black cursor, then waits until a character is inputted. If the input is a backspace, then it checks if the Y
register is 0
, if it’s 0 then it means that there is nothing to delete and loops back to itself. Otherwise it will decrement Y
and replace the character with a space to clear out the character in the display.
CHECKENTER
CHECKENTER
checks if user presses the ENTER
key, and directs the program to CHECKCOUNT
where it checks if there is a character.
CHECKCOUNT
CHECKCOUNT
checks if there is exactly 1 character, this is needed because when pressing enter, it is not guaranteed there is any inpu*t. This means that we are limited to single digits for the numbers being added.* If input is empty, the program returns to GETKEY
waiting until the user inputs a character.
CHECKLETTER
CHECKLETTER
is the final step in input validation, at this point, we can assume that the input is non-empty. Next is to validate that it is a character ranging from 0-9.
The same process is repeated for getting the second number.
Performing Addition
Converting ASCII to Numerical Value
At this point, the inputted values have been represented as ASCII values. However the ASCII value of a number is not equal to its numerical value. For example, the number 1 is equal to 31 in hexadecimal, 2 is equal to 32, etc. Therefore, there is an offset of 30.
To convert the ASCII into it’s proper numerical value, 30 will needs to be subtracted from its ASCII value.
GETSECOND:
JSR INPUTSECOND ; Get second digit
LDY #$00
; Convert ASCII to number and add
LDA INPUT1
SEC
SBC #$30 ; Convert ASCII to numerical value
STA INPUT1 ; Store numerical value
LDA INPUT2
SEC
SBC #$30 ; Convert ASCII to numerical value
STA INPUT2 ; Store numerical value
CLC ; Clear carry flag before addition
LDA INPUT1
ADC INPUT2 ; Add the two numbers
STA RESULT ; Store result
LDY #$00
Once converted, the value is stored into it’s respective zero page variable defined previously.
Performing Addition
CLC ; Clear carry flag before addition
LDA INPUT1
ADC INPUT2 ; Add the two numbers
STA RESULT ; Store result
The carry flag may be set from previous operations, and needs to be cleared before addition occurs. Once cleared, INPUT1
is loaded into the accumulator and ADC INPUT2
adds INPUT2
to INPUT1
and the carry flag will be set if the result exceeds 255. The result is then stored into the RESULT
memory address from the accumulator.
Printing the Result
At this point, the result is stored in its numerical representation. However when displaying with CHROUT
, the result needs to be in its ASCII representation. For example, if the result was 9, without converting to its ASCII, it would display a horizontal tab.
CHROUT
prints the character in the accumulator and must be in it’s ASCII representation. Since ASCII is limited to single digits e.g. there is no ASCII for 10, 11, 123, etc.) each number has be printed individually (e.g. printing the tens, ones, etc.).
PRINT_RESULT:
LDA RESULT ; Load the result
CMP #10 ; Check if the result is greater than or equal to 10
BPL TWO_DIGITS ; If yes -> handle two digits
; Single-digit result
CLC ; Clear carry flag before ASCII conversion
ADC #$30 ; Convert to ASCII
JSR CHROUT ; Display the result
JMP END_PROGRAM
; Handle two-digit result
TWO_DIGITS:
SEC
SBC #10 ; Subtract 10 to get the units digit
PHA ; Save the units digit
LDA #$31 ; Load ASCII for '1' (tens digit)
JSR CHROUT ; Display the tens digit
PLA ; Restore the units digit
CLC ; Clear carry flag before ASCII conversion
ADC #$30 ; Convert to ASCII
JSR CHROUT ; Display the units digit
In PRINT_RESULT
, if RESULT
is greater than 9, then we need to handle the printing where the tens is printed first followed by the ones. Otherwise, 30 is added to the sum to convert it into its ASCII form.
Printing 2 Digit Sum
In the case of a 2 digit sum, TWO_DIGITS
the carry flag is set SEC
in order to carry out subtraction SBC
. The result is then subtracted by 10 then the difference is pushed onto the stack PHA
- this represents the number in the “ones” column.
; Handle two-digit result
TWO_DIGITS:
SEC
SBC #10 ; Subtract 10 to get the units digit
PHA ; Save the units digit
LDA #$31 ; Load ASCII for '1' (tens digit)
JSR CHROUT ; Display the tens digit
PLA ; Restore the units digit
CLC ; Clear carry flag before ASCII conversion
ADC #$30 ; Convert to ASCII
JSR CHROUT ; Display the units digit
The ASCII value for 1 is then printed, note that the max sum is 18, therefore, 1 is the largest value in the tens column. The “ones” column value is restored into the accumulator via PLA
and then printed. Using the stack was inspired by user “barrym95838” from the 6502 forum (Link).
Conclusion
The open ended nature of this lab - where I choose what program to create was both a blessing and a curse. I was confident in managing the bits using the LDA
, STA
, etc. methods, with both immediate and absolute addressing modes - used to get and set the values.
Where I had the most issue is dealing with outputting the 2 digit results. My first attempt resulted in the “ones” column being printed (i.e. without the “tens” column). For example 9 + 9
would print out 8
, the second digit of the sum of 18. Researching online has led to an interesting solution previously mentioned with the use of the stack.
Overall I am happy with the program and excited to start with 64-bit compilers.
Top comments (0)