Hello, Blog!
If you have just stumbled upon my SPO600 series of blog posts, it has been created to document and share my learnings as I progress through my Software Portability and Optimization college course.
The semester is wrapping up, and I'm working to catch up on my backlog of blog posts - bringing you a sudden throwback to 6502 assembly.
In this post, I'll cover the second lab of the course. The main goal of this lab was getting familiar with performing math operations in 6502 assembly, specifically to animate the graphics on the screen.
Check out my 1-st post about 6502 Assembly: 6502 Assembly Intro
Find the 6502 emulator I used here: 6502 Emulator
Nice and colourful 6502 reference: Ultimate 6502 Reference
Let's take a look at the starter code
; ; 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 ; Set up the data structure ; The syntax #<LABEL returns the low byte of LABEL ; The syntax #>LABEL returns the high byte of LABEL LDA #<G_X ; POINTER TO GRAPHIC STA $10 LDA #>G_X STA $11 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 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 $0600 ; ========================================== ; ; 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 initial code creates a simple animation that moves the graphic diagonally from the top-left corner to the bottom-right. It uses a subroutine (DRAW
) to place the image at the correct screen address each frame, then erases the old position and updates XPOS
and YPOS
until it has moved 29 times.
Let's make it bounce
Our goal in this lab is to make the graphic the bitmapped display using 6502 math, instead of resetting after a set number of frames.
Steps taken:
1. Set the initial position for the graphic: I chose a non-corner starting point, so the graphic can properly bounce around the screen once I am finished with the logic
; Set initial position X=Y=0 LDA #$00 STA XPOS LDA #$05 STA YPOS
2. Select an X increment that is -1 or +1, and a Y increment that is -1 or +1: I created two new zero-page variables, XINCREMENT
and YINCREMENT
, and set them to 1. This means the image initially moves down-right. Using #$01
and #$FF
will allow us to move in different directions (#$01
- right/down; #$FF
- left/up)
; Zero-page variables define XINCREMENT $22 define YINCREMENT $23 ; Select the increments LDA #$01 STA XINCREMENT ; start moving right (+1) LDA #$01 STA YINCREMENT ; start moving down (+1)
3. Successively move the graphic and make it bounce around the screen:
Figuring out the logic took quite some time, but the final code essentially works as follows:
- Increment the X coordinate: on each iteration, the X position is adjusted by adding either +1 or -1, depending on the current horizontal increment value.
- Check horizontal boundaries: if the updated X position hits the right or left edge of the screen, the increment is reversed, causing the graphic to change direction horizontally.
- Increment the Y coordinate: next, the Y position is similarly updated based on its current vertical increment.
- Check vertical boundaries: if the Y position reaches the top or bottom edge, the vertical increment is also flipped, making the graphic bounce vertically as well.
; setup code ; start of the main loop ; Increment the X position ; --------------------------- ; If XINCREMENT is #$01, we move right by adding 1 ; If XINCREMENT is #$FF, we move left by adding -1 LDA XPOS CLC ; Clear carry before addition ADC XINCREMENT ; Add XINCREMENT to XPOS STA XPOS ; Check horizontal boundaries ; --------------------------- ; Check if XPOS == XBOUNDARY (right edge) LDA XPOS CMP #XBOUNDARY BNE CHECK_X_LEFT ; If not equal, jump to check left boundary ; If we hit the right boundary, reverse direction LDA #$FF STA XINCREMENT CHECK_X_LEFT: ; Check if XPOS == 0 (left edge) LDA XPOS CMP #0 BNE UPDATE_Y ; If not equal, jump to update Y coordinate ; If we hit the left boundary and direction == left, reverse direction LDA #$01 STA XINCREMENT UPDATE_Y: ; Update YPOS using YINCREMENT ; ---------------------------- ; If YINCREMENT is #$01, we move down by adding 1 ; If YINCREMENT is #$FF, we move up by adding -1 LDA YPOS CLC ; Clear carry before addition ADC YINCREMENT ; Add YINCREMENT to YPOS STA YPOS ; Check vertical boundaries ; --------------------------- ; Check if YPOS == YBOUNDARY (bottom edge) LDA YPOS CMP #YBOUNDARY BNE CHECK_Y_TOP ; If not equal, jump to check the top edge ; If we hit bottom edge, reverse direction LDA #$FF STA YINCREMENT CHECK_Y_TOP: ; Check if YPOS == 0 (top edge) LDA YPOS CMP #0 ; If no boundary hit, continue moving BNE MAINLOOP ; If we hit top edge, reverse direction LDA #$01 STA YINCREMENT JMP MAINLOOP ; Return to the main loop ; DRAW subroutine
Full Solution
If you’d like to see the complete implementation, feel free to check it out on GitHub.
Afterthoughts
Assembly programming is certainly very tedious, especially after stepping away from it for a while (the process of readjusting is very real!). However, the complete lack of abstraction also has its benefits: it forces you to gain an intimate understanding of every tiny detail of the code's logic.
Top comments (0)