Lab 2 - 6502 Math Lab
Introduction
This blog entry documents my journey in implementing a diagonal bouncing animation using 6502 Assembly. The objective was to make a 5x5 pixel graphic move diagonally across the screen and bounce off the edges in a smooth and continuous loop.
At first glance, this seemed like a simple task, but it turned out to be quite challenging, requiring:
• Dynamic coordinate updates for smooth movement
• Precise boundary detection
• Reversing direction correctly upon hitting an edge
• Maintaining perfect diagonal motion over multiple bounces
In this post, I will break down the development process, from the initial approach to debugging and optimizing the final solution.
Initial Approach
🔹 Initial Approach: Using provided code
At the start of this project, we were given a base code that allowed an object to move diagonally across the screen. The original implementation simply incremented both XPOS and YPOS values every frame, causing the object to move in a fixed diagonal direction.
;
; draw-image-subroutine.6502
;
; This is a routine that can place an arbitrary
; rectangular image on to the screen at given
; coordinates.
;
; Chris Tyler 2024-09-17
; Licensed under GPLv2+
;
;
; The subroutine is below starting at the
; label "DRAW:"
;
; Test code for our subroutine
; Moves an image diagonally across the screen
; Zero-page variables
define XPOS $20
define YPOS $21
START:
; Set up the width and height elements of the data structure
LDA #$05
STA $12 ; IMAGE WIDTH
STA $13 ; IMAGE HEIGHT
; Set initial position X=Y=0
LDA #$00
STA XPOS
STA YPOS
; Main loop for diagonal animation
MAINLOOP:
; Set pointer to the image
; Use G_O or G_X as desired
; The syntax #<LABEL returns the low byte of LABEL
; The syntax #>LABEL returns the high byte of LABEL
LDA #<G_O
STA $10
LDA #>G_O
STA $11
; Place the image on the screen
LDA #$10 ; Address in zeropage of the data structure
LDX XPOS ; X position
LDY YPOS ; Y position
JSR DRAW ; Call the subroutine
; Delay to show the image
LDY #$00
LDX #$50
DELAY:
DEY
BNE DELAY
DEX
BNE DELAY
; Set pointer to the blank graphic
LDA #<G_BLANK
STA $10
LDA #>G_BLANK
STA $11
; Draw the blank graphic to clear the old image
LDA #$10 ; LOCATION OF DATA STRUCTURE
LDX XPOS
LDY YPOS
JSR DRAW
; Increment the position
INC XPOS
INC YPOS
; Continue for 29 frames of animation
LDA #28
CMP XPOS
BNE MAINLOOP
; Repeat infinitely
JMP START
; ==========================================
;
; DRAW :: Subroutine to draw an image on
; the bitmapped display
;
; Entry conditions:
; A - location in zero page of:
; a pointer to the image (2 bytes)
; followed by the image width (1 byte)
; followed by the image height (1 byte)
; X - horizontal location to put the image
; Y - vertical location to put the image
;
; Exit conditions:
; All registers are undefined
;
; Zero-page memory locations
define IMGPTR $A0
define IMGPTRH $A1
define IMGWIDTH $A2
define IMGHEIGHT $A3
define SCRPTR $A4
define SCRPTRH $A5
define SCRX $A6
define SCRY $A7
DRAW:
; SAVE THE X AND Y REG VALUES
STY SCRY
STX SCRX
; GET THE DATA STRUCTURE
TAY
LDA $0000,Y
STA IMGPTR
LDA $0001,Y
STA IMGPTRH
LDA $0002,Y
STA IMGWIDTH
LDA $0003,Y
STA IMGHEIGHT
; CALCULATE THE START OF THE IMAGE ON
; SCREEN AND PLACE IN SCRPTRH
;
; THIS IS $0200 (START OF SCREEN) +
; SCRX + SCRY * 32
;
; WE'LL DO THE MULTIPLICATION FIRST
; START BY PLACING SCRY INTO SCRPTR
LDA #$00
STA SCRPTRH
LDA SCRY
STA SCRPTR
; NOW DO 5 LEFT SHIFTS TO MULTIPLY BY 32
LDY #$05 ; NUMBER OF SHIFTS
MULT:
ASL SCRPTR ; PERFORM 16-BIT LEFT SHIFT
ROL SCRPTRH
DEY
BNE MULT
; NOW ADD THE X VALUE
LDA SCRX
CLC
ADC SCRPTR
STA SCRPTR
LDA #$00
ADC SCRPTRH
STA SCRPTRH
; NOW ADD THE SCREEN BASE ADDRESS OF $0200
; SINCE THE LOW BYTE IS $00 WE CAN IGNORE IT
LDA #$02
CLC
ADC SCRPTRH
STA SCRPTRH
; NOTE WE COULD HAVE DONE TWO: INC SCRPTRH
; NOW WE HAVE A POINTER TO THE IMAGE IN MEM
; COPY A ROW OF IMAGE DATA
COPYROW:
LDY #$00
ROWLOOP:
LDA (IMGPTR),Y
STA (SCRPTR),Y
INY
CPY IMGWIDTH
BNE ROWLOOP
; NOW WE NEED TO ADVANCE TO THE NEXT ROW
; ADD IMGWIDTH TO THE IMGPTR
LDA IMGWIDTH
CLC
ADC IMGPTR
STA IMGPTR
LDA #$00
ADC IMGPTRH
STA IMGPTRH
; ADD 32 TO THE SCRPTR
LDA #32
CLC
ADC SCRPTR
STA SCRPTR
LDA #$00
ADC SCRPTRH
STA SCRPTRH
; DECREMENT THE LINE COUNT AND SEE IF WE'RE
; DONE
DEC IMGHEIGHT
BNE COPYROW
RTS
; ==========================================
; 5x5 pixel images
; Image of a blue "O" on black background
G_O:
DCB $00,$0e,$0e,$0e,$00
DCB $0e,$00,$00,$00,$0e
DCB $0e,$00,$00,$00,$0e
DCB $0e,$00,$00,$00,$0e
DCB $00,$0e,$0e,$0e,$00
; Image of a yellow "X" on a black background
G_X:
DCB $07,$00,$00,$00,$07
DCB $00,$07,$00,$07,$00
DCB $00,$00,$07,$00,$00
DCB $00,$07,$00,$07,$00
DCB $07,$00,$00,$00,$07
; Image of a black square
G_BLANK:
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
🔹 First Attempt: Simple Diagonal Movement
I started with a basic approach where the image moved diagonally by simply incrementing both X and Y positions:
This worked until the object reached a boundary—then it simply disappeared off-screen because there was no logic to reverse direction.
🔻 Problems Encountered
No bouncing behaviour. I needed a way to detect boundaries and reverse movement.
🔹 Second Attempt: Using Direction Flags (XFLAG, YFLAG)
To fix this, I introduced XFLAG and YFLAG variables:
• +1 to move right/down
• -1 to move left/up
- Updated Movement Logic
LDA XPOS
CLC
ADC XFLAG
STA XPOS
LDA YPOS
CLC
ADC YFLAG
STA YPOS
🔻 Problems Encountered
1. The object disappeared off-screen before reversing direction.
2. Inconsistent bouncing when hitting a boundary.
3. The object did not always follow a perfect diagonal path after multiple bounces.
Implementing Proper Bouncing
🔹 Third Attempt: Full Boundary Check & Direction Reversal
To ensure that the object never exceeded screen limits, I added explicit boundary checks and modified the direction when needed:
;; draw-image-subroutine.6502 ; ; This is a routine that can place an arbitrary ; rectangular image on to the screen at given ; coordinates. ; ; Chris Tyler 2024-09-17 ; Licensed under GPLv2+ ; ; ; The subroutine is below starting at the ; label "DRAW:" ; ; Test code for our subroutine ; Moves an image diagonally across the screen ; Zero-page variables define XPOS $20 define YPOS $21 define XFLAG $22 ; Flag for X direction movement (+1 or -1) define YFLAG $23 ; Flag for Y direction movement (+1 or -1) START: ; Set up the width and height elements of the data structure LDA #$05 STA $12 ; IMAGE WIDTH STA $13 ; IMAGE HEIGHT ; Set initial position X=2, Y=3 LDA #$02 STA XPOS LDA #$03 STA YPOS ; Set initial movement direction (X +1, Y +1) LDA #$01 STA XFLAG STA YFLAG ; Main loop for diagonal animation MAINLOOP: ; Set pointer to the image LDA #<G_O STA $10 LDA #>G_O STA $11 ; Place the image on the screen LDA #$10 ; Address in zeropage of the data structure LDX XPOS ; X position LDY YPOS ; Y position JSR DRAW ; Call the subroutine ; Delay to show the image LDY #$00 LDX #$50 DELAY: DEY BNE DELAY DEX BNE DELAY ; Set pointer to the blank graphic LDA #<G_BLANK STA $10 LDA #>G_BLANK STA $11 ; Draw the blank graphic to clear the old image LDA #$10 ; LOCATION OF DATA STRUCTURE LDX XPOS LDY YPOS JSR DRAW ; Update X and check boundaries UPDATE_X: LDA XPOS CLC ADC XFLAG ; Add XFLAG to XPOS STA XPOS ; Check X boundaries (1 to 28) CMP #$1C ; If XPOS > 28 (max boundary) BCC CHECK_X_MIN ; If XPOS <= 28, check lower bound ; Reverse X direction for max boundary LDA #$FF ; Reverse direction to -1 STA XFLAG LDA #$1B ; Set XPOS to 27 STA XPOS CHECK_X_MIN: CMP #$01 ; If XPOS < 1 (min boundary) BCS UPDATE_Y ; If XPOS >= 1, update Y position ; Reverse X direction for min boundary LDA #$01 ; Reverse direction to +1 STA XFLAG LDA #$02 ; Set XPOS to 2 STA XPOS ; Update Y and check boundaries UPDATE_Y: LDA YPOS CLC ADC YFLAG ; Add YFLAG to YPOS STA YPOS ; Check Y boundaries (1 to 27) CMP #$1B ; If YPOS > 27 (max boundary) BCC CHECK_Y_MIN ; If YPOS <= 27, check lower bound ; Reverse Y direction for max boundary LDA #$FF ; Reverse direction to -1 STA YFLAG LDA #$1A ; Set YPOS to 26 STA YPOS CHECK_Y_MIN: CMP #$01 ; If YPOS < 1 (min boundary) BCS MAINLOOP ; If YPOS >= 1, continue to next frame ; Reverse Y direction for min boundary LDA #$01 ; Reverse direction to +1 STA YFLAG LDA #$02 ; Set YPOS to 2 STA YPOS JMP MAINLOOP ; ========================================== ; ; DRAW :: Subroutine to draw an image on ; the bitmapped display ; ; Entry conditions: ; A - location in zero page of: ; a pointer to the image (2 bytes) ; followed by the image width (1 byte) ; followed by the image height (1 byte) ; X - horizontal location to put the image ; Y - vertical location to put the image ; ; Exit conditions: ; All registers are undefined ; ; Zero-page memory locations define IMGPTR $A0 define IMGPTRH $A1 define IMGWIDTH $A2 define IMGHEIGHT $A3 define SCRPTR $A4 define SCRPTRH $A5 define SCRX $A6 define SCRY $A7 DRAW: ; SAVE THE X AND Y REG VALUES STY SCRY STX SCRX ; GET THE DATA STRUCTURE TAY LDA $0000,Y STA IMGPTR LDA $0001,Y STA IMGPTRH LDA $0002,Y STA IMGWIDTH LDA $0003,Y STA IMGHEIGHT ; CALCULATE THE START OF THE IMAGE ON ; SCREEN AND PLACE IN SCRPTRH ; ; THIS IS $0200 (START OF SCREEN) + ; SCRX + SCRY * 32 ; ; WE'LL DO THE MULTIPLICATION FIRST ; START BY PLACING SCRY INTO SCRPTR LDA #$00 STA SCRPTRH LDA SCRY STA SCRPTR ; NOW DO 5 LEFT SHIFTS TO MULTIPLY BY 32 LDY #$05 ; NUMBER OF SHIFTS MULT: ASL SCRPTR ; PERFORM 16-BIT LEFT SHIFT ROL SCRPTRH DEY BNE MULT ; NOW ADD THE X VALUE LDA SCRX CLC ADC SCRPTR STA SCRPTR LDA #$00 ADC SCRPTRH STA SCRPTRH ; NOW ADD THE SCREEN BASE ADDRESS OF $0200 ; SINCE THE LOW BYTE IS $00 WE CAN IGNORE IT LDA #$02 CLC ADC SCRPTRH STA SCRPTRH ; NOTE WE COULD HAVE DONE TWO: INC SCRPTRH ; NOW WE HAVE A POINTER TO THE IMAGE IN MEM ; COPY A ROW OF IMAGE DATA COPYROW: LDY #$00 ROWLOOP: LDA (IMGPTR),Y STA (SCRPTR),Y INY CPY IMGWIDTH BNE ROWLOOP ; NOW WE NEED TO ADVANCE TO THE NEXT ROW ; ADD IMGWIDTH TO THE IMGPTR LDA IMGWIDTH CLC ADC IMGPTR STA IMGPTR LDA #$00 ADC IMGPTRH STA IMGPTRH ; ADD 32 TO THE SCRPTR LDA #32 CLC ADC SCRPTR STA SCRPTR LDA #$00 ADC SCRPTRH STA SCRPTRH ; DECREMENT THE LINE COUNT AND SEE IF WE'RE ; DONE DEC IMGHEIGHT BNE COPYROW RTS ; ========================================== ; 5x5 pixel images ; Image of a blue "O" on black background G_O: DCB $00,$0e,$0e,$0e,$00 DCB $0e,$00,$00,$00,$0e DCB $0e,$00,$00,$00,$0e DCB $0e,$00,$00,$00,$0e DCB $00,$0e,$0e,$0e,$00 ; Image of a yellow "X" on a black background G_X: DCB $07,$00,$00,$00,$07 DCB $00,$07,$00,$07,$00 DCB $00,$00,$07,$00,$00 DCB $00,$07,$00,$07,$00 DCB $07,$00,$00,$00,$07 ; Image of a black square G_BLANK: DCB $00,$00,$00,$00,$00 DCB $00,$00,$00,$00,$00 DCB $00,$00,$00,$00,$00 DCB $00,$00,$00,$00,$00 DCB $00,$00,$00,$00,$00
This method ensured that:
• The object bounces upon touching the boundary instead of disappearing.
• It maintains a consistent diagonal path.
However, as the animation repeated, I noticed that the motion gradually lost its perfect diagonal shape due to slight inconsistencies in boundary handling.
Final Optimized Code
After multiple iterations, I realized that my initial approach had some fundamental flaws:
1. The object would sometimes disappear off-screen before reversing direction.
2. Inconsistent bouncing behaviour, especially when hitting boundaries.
3. The diagonal path would slightly distort over multiple bounces.
To fix these issues, I implemented a structured boundary-checking system using XFLAG and YFLAG.
Instead of simply increasing and decreasing the coordinates, I used these flags to properly reverse direction when needed.
🛠️ Key Improvements
Initially, I considered simply adding conditions to prevent XPOS/YPOS from going out of bounds, but this approach caused abrupt direction changes. Instead, I used sign-flipping (XFLAG/YFLAG) to ensure a natural bounce effect.
- Precise boundary detection: Ensuring the object never leaves the visible area.
- Consistent diagonal movement: Keeping a perfect 45-degree bounce behavior.
- Dynamic limits: Using MAXPOS instead of hardcoded values for easier adjustments.
This final version ensures smooth diagonal bouncing, maintaining a predictable pattern no matter how long it runs.
;
; draw-image-subroutine.6502
;
; This is a routine that can place an arbitrary
; rectangular image on to the screen at given
; coordinates.
;
; Chris Tyler 2024-09-17
; Licensed under GPLv2+
;
;
; The subroutine is below starting at the
; label "DRAW:"
;
; Test code for our subroutine
; Moves an image diagonally across the screen
; Zero-page variables
define XPOS $20
define YPOS $21
define XFLAG $22 ; X-axis movement flag (+1 or -1)
define YFLAG $23 ; Y-axis movement flag (+1 or -1)
define MAXPOS $24 ; Maximum screen boundary value (25)
define MOVEDIR $25 ; Movement direction flag (0 or 1)
START:
; Initialize movement direction
LDA #$00
STA MOVEDIR
; Set maximum boundary value (25)
LDA #$19
STA MAXPOS
; Set up image width & height
LDA #$05
STA $12
STA $13
; Set initial position X=Y=0
LDA #$00
STA XPOS
STA YPOS
MAINLOOP:
; Set pointer to the image
LDA #<G_O
STA $10
LDA #>G_O
STA $11
; Place the image on the screen
LDA #$10
LDX XPOS
LDY YPOS
JSR DRAW
; Delay to show the image
LDY #$00
LDX #$50
DELAY:
DEY
BNE DELAY
DEX
BNE DELAY
; Set pointer to the blank image
LDA #<G_BLANK
STA $10
LDA #>G_BLANK
STA $11
; Clear previous image position
LDA #$10
LDX XPOS
LDY YPOS
JSR DRAW
; Movement logic based on direction
LDA MOVEDIR
BEQ INCREMENT_POS
DEC XPOS
DEC YPOS
JMP CHECK_DIRECTION_CHANGE
INCREMENT_POS:
INC XPOS
INC YPOS
CHECK_DIRECTION_CHANGE:
LDA MAXPOS
CMP XPOS
BNE CHECK_IF_AT_START
; Change direction
LDA #$01
STA MOVEDIR
JMP CONTINUE_LOOP
CHECK_IF_AT_START:
LDA XPOS
CMP #$00
BNE CONTINUE_LOOP
LDA #$00
STA MOVEDIR
CONTINUE_LOOP:
; Continue for 29 frames of animation
LDA #28
CMP XPOS
BNE MAINLOOP
; Repeat infinitely
JMP START
; ==========================================
;
; DRAW :: Subroutine to draw an image on
; the bitmapped display
;
; Entry conditions:
; A - location in zero page of:
; a pointer to the image (2 bytes)
; followed by the image width (1 byte)
; followed by the image height (1 byte)
; X - horizontal location to put the image
; Y - vertical location to put the image
;
; Exit conditions:
; All registers are undefined
;
; Zero-page memory locations
define IMGPTR $A0
define IMGPTRH $A1
define IMGWIDTH $A2
define IMGHEIGHT $A3
define SCRPTR $A4
define SCRPTRH $A5
define SCRX $A6
define SCRY $A7
DRAW:
; SAVE THE X AND Y REG VALUES
STY SCRY
STX SCRX
; GET THE DATA STRUCTURE
TAY
LDA $0000,Y
STA IMGPTR
LDA $0001,Y
STA IMGPTRH
LDA $0002,Y
STA IMGWIDTH
LDA $0003,Y
STA IMGHEIGHT
; CALCULATE THE START OF THE IMAGE ON
; SCREEN AND PLACE IN SCRPTRH
;
; THIS IS $0200 (START OF SCREEN) +
; SCRX + SCRY * 32
;
; WE'LL DO THE MULTIPLICATION FIRST
; START BY PLACING SCRY INTO SCRPTR
LDA #$00
STA SCRPTRH
LDA SCRY
STA SCRPTR
; NOW DO 5 LEFT SHIFTS TO MULTIPLY BY 32
LDY #$05 ; NUMBER OF SHIFTS
MULT:
ASL SCRPTR ; PERFORM 16-BIT LEFT SHIFT
ROL SCRPTRH
DEY
BNE MULT
; NOW ADD THE X VALUE
LDA SCRX
CLC
ADC SCRPTR
STA SCRPTR
LDA #$00
ADC SCRPTRH
STA SCRPTRH
; NOW ADD THE SCREEN BASE ADDRESS OF $0200
; SINCE THE LOW BYTE IS $00 WE CAN IGNORE IT
LDA #$02
CLC
ADC SCRPTRH
STA SCRPTRH
; NOTE WE COULD HAVE DONE TWO: INC SCRPTRH
; NOW WE HAVE A POINTER TO THE IMAGE IN MEM
; COPY A ROW OF IMAGE DATA
COPYROW:
LDY #$00
ROWLOOP:
LDA (IMGPTR),Y
STA (SCRPTR),Y
INY
CPY IMGWIDTH
BNE ROWLOOP
; NOW WE NEED TO ADVANCE TO THE NEXT ROW
; ADD IMGWIDTH TO THE IMGPTR
LDA IMGWIDTH
CLC
ADC IMGPTR
STA IMGPTR
LDA #$00
ADC IMGPTRH
STA IMGPTRH
; ADD 32 TO THE SCRPTR
LDA #32
CLC
ADC SCRPTR
STA SCRPTR
LDA #$00
ADC SCRPTRH
STA SCRPTRH
; DECREMENT THE LINE COUNT AND SEE IF WE'RE
; DONE
DEC IMGHEIGHT
BNE COPYROW
RTS
; ==========================================
; 5x5 pixel images
; Image of a blue "O" on black background
G_O:
DCB $00,$0e,$0e,$0e,$00
DCB $0e,$00,$00,$00,$0e
DCB $0e,$00,$00,$00,$0e
DCB $0e,$00,$00,$00,$0e
DCB $00,$0e,$0e,$0e,$00
; Image of a yellow "X" on a black background
G_X:
DCB $07,$00,$00,$00,$07
DCB $00,$07,$00,$07,$00
DCB $00,$00,$07,$00,$00
DCB $00,$07,$00,$07,$00
DCB $07,$00,$00,$00,$07
; Image of a black square
G_BLANK:
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
DCB $00,$00,$00,$00,$00
Results & Reflection
✅ Results
• Smooth diagonal bouncing animation without disappearing.
• Accurate boundary detection prevents unexpected motion errors.
• Maintains direction consistency across multiple bounces.
💡 Reflection
Looking back, this project was not just about making an object move—it was about structuring logic in an efficient way. The experience taught me how important planning and debugging are in low-level programming.
Initially, I underestimated the complexity of implementing smooth diagonal motion. However, through multiple iterations, I realized that even seemingly simple tasks require careful design, especially in Assembly, where direct memory manipulation and arithmetic operations play a crucial role. Managing screen boundaries effectively without causing unpredictable behavior was a key challenge, and refining the logic for movement reversal helped reinforce my understanding of structured programming in Assembly.
This project also gave me a deeper appreciation for optimization in low-level programming. Since resources are limited, every instruction and calculation must be deliberate. Even minor inefficiencies can lead to unexpected behaviour or performance issues. I now understand why old-school programmers were so focused on efficiency and memory management.
Honestly, this was way harder than I expected. There were so many small bugs that drove me crazy, but in the end, getting it to work felt really satisfying. I finally understand why proper memory handling and well-structured logic are essential in Assembly. While I may not be eager to jump into another Assembly project soon, this experience has made it feel much less intimidating than before.
Comments
Post a Comment