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.


Reference




Comments

Popular posts from this blog

SPO600 2025 Winter Project - Stage 1: Create a Basic GCC Pass (part1)

SPO600 2025 Winter Project - Stage 2: GIMPLE Level Clone Analysis and Pruning (part4)

Lab 1 - 6502 Assembly Language