DEV Community

JP
JP

Posted on

Lab 3 - Wrapping up 6502 Assembly

Introduction

Welcome back! We’ll be concluding the 6502 series by creating a program that covers 3 main topics:

  1. Character output
  2. Character input
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. SCINIT — initialize screen, clearing data in the bitmap display
  2. CHRIN — character in, input keyboard characters
  3. CHROUT — output character to the screen
  4. 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:

  1. Check if input ranges from 0-9
  2. Handling the ENTER key
  3. 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  
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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).

Image description

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)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs