Introduction
Welcome back, let’s continue with more 6502 assembly. In the last blog post, the bitmap display was introduced and it will filled with different colours. From that it was discovered that the bitmap display spans from page 2 to page 5 (i.e. $0200 - $0500
).
Today an image will be animated through the bitmap display. Below is the starter code provided by Chris Tyler, the instructor of SPO600 course. We’ll continue using the 6502 emulator.
;
; 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
The starter code animates a blue circle at the top left corner of the bitmap display then moves it diagonally to the bottom right corner and repeats indefinitely. To understand how this works, let’s breakdown each routine/subroutine then explain how they work together.
Initial Setup
Zero-Page Variables
; Zero-page variables
define XPOS $20
define YPOS $21
define
is a special keyword that defines constants. In this case, XPOS
and YPOS
represent the respective memory locations for the image’s coordinates.
Positional Setup
START:
LDA #$05
STA $12 ; IMAGE WIDTH
STA $13 ; IMAGE HEIGHT
LDA #$00
STA XPOS
STA YPOS
The image has dimensions of 5x5
and has the initial coordinates of 0,0
, the top left corner of the bitmap display.
Main Animation Loop
MAINLOOP:
LDA #<G_O
STA $10
LDA #>G_O
STA $11
The main animation loops loads the image’s address into the accumulator then stores its low then high byte into the memory locations $10
and $11
respectively.
; 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
The low byte of the image is loaded in the X and Y coordinates - this is where the DRAW
subroutine will start drawing the image into the bitmap display.
Delay
LDY #$00
LDX #$50
DELAY:
DEY
BNE DELAY
DEX
BNE DELAY
The DELAY
routine slows down the DRAW
routine to display a “solid” image. Without DELAY
, the image would flash rapidly. There are 2 loops - an outer loop that increments Y 256 times, and an inner loop that decrements X 80 times. Totalling 80 * 256 = 20480
iterations.
Animating the Image
; 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
LDX XPOS
LDY YPOS
JSR DRAW
; Increment the position
INC XPOS
INC YPOS
LDA #28
CMP XPOS
BNE MAINLOOP
...
; 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
To “animate” the image, the current image turns “blank” by replacing the image with black pixels. The low byte is then loaded into the accumulator, the X and Y position of the image is loaded in their respective registers where they are incremented. The image gets drawn in its new X, Y position - completing the animation. The completes after 29 “movements” then the same animation indefinitely.
DRAW
Subroutine
The DRAW
subroutine is the most important part of the program - it calculates where the image goes at each iteration.
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:
STY SCRY
STX SCRX
TAY
LDA $0000,Y
STA IMGPTR
LDA $0001,Y
STA IMGPTRH
LDA $0002,Y
STA IMGWIDTH
LDA $0003,Y
STA IMGHEIGHT
It starts by loading the position of the screen boundaries, and the image properties (X, Y coordinates, height, and width) from their respective memory addresses and stores them into the predefined register addresses - these values will be accessed later when calculating the position for the next image.
Calculating Screen Position
LDA #$00
STA SCRPTRH
LDA SCRY
STA SCRPTR
LDY #$05
MULT:
ASL SCRPTR
ROL SCRPTRH
DEY
BNE MULT
Calculation Steps:
- Clear the accumulator, this is where the calculation will take place
- Load the accumulator with the image’s Y-coordinate
-
The Y-coordinate is then loaded with
5
, the number of shifts to move the pixel down since each row is 32 bytes long.
1 | 2 | 4 | 8 | 16 | 32 ^ | 2 | 3 | 4 | 5 | 6 current
The
MULT
routine shifts the the bits to left viaASL
andROL
rotates the carry into theSCRPTRH
, Y registry is then decremented and repeats until Y = 0.
LDA SCRX
CLC
ADC SCRPTR
STA SCRPTR
LDA #$00
ADC SCRPTRH
STA SCRPTRH
- Calculating the X-coordinate introduces addition - where the low byte is added first followed by the high byte. The
ADC SCRPTR
adds the low byte to the accumulator with carry. This carry value is then added to the high byte with the carry. - At this point,
SCRPTR
andSCRPTRH
hold the final addresses for the transformed image.
Inserting the Transformed Image
LDA #$02
CLC
ADC SCRPTRH
STA SCRPTRH
The bitmap display base address of 02
is loaded, with the carry flag cleared. The screen pointer high byte is set by adding the SCRPTRH
calculated in the last step.
COPYROW:
LDY #$00
ROWLOOP:
LDA (IMGPTR),Y
STA (SCRPTR),Y
INY
CPY IMGWIDTH
BNE ROWLOOP
The bytes are copied from IMGPTR
to the accumulator, then into SCRPTR
which is the memory corresponding the location on the bitmap display. The loop stops when the Y register reaches the end of the image row.
LDA IMGWIDTH
CLC
ADC IMGPTR
STA IMGPTR
LDA #$00
ADC IMGPTRH
STA IMGPTRH
LDA #32
CLC
ADC SCRPTR
STA SCRPTR
LDA #$00
ADC SCRPTRH
STA SCRPTRH
To advance to the next row, the image width is loaded into the accumulator. IMGPTR
is then added to the image width, advancing the next row in the image. The updated image pointer is stored in IMGPTR
. 32
is by added to SCRPTR
in the accumulator then stored back into SCRPTR
. The same processes is done with the high byte, with the carry byte.
Bouncing Graphic
Let’s begin by changing the starting position. By loading the accumulator with different values and storing it in XPOS
and YPOS
.
START:
LDA #$05
STA $12 ; IMAGE WIDTH
STA $13 ; IMAGE HEIGHT
LDA #$0F
STA XPOS
LDA #$0A
STA YPOS
Next let’s make the graphic bounce once it makes contact with the wall of the bitmap display. To do this we could invert the sign (e.g. +/-
) of the X and Y direction when the graphic makes contact with the wall.
To do this let’s define a constant value to represent the edge of the screen:
; Zero-page variables
define XPOS $20
define YPOS $21
define MAXP $24
Next we need to track the image direction, is it moving “forwards” or “backwards”? In the case it moves “backwards”, the direction can be inverted, and vice versa.
; Zero-page variables
define XPOS $20
define YPOS $21
define MAXP $24
define CURDIR $25
The X and Y sign also need to be tracked to know which direction the respective axes needs to move.
; Zero-page variables
define XPOS $20
define YPOS $21
define MAXP $22
define CURDIR $23
define XFLAG $24
define YFLAG $25
We have the required variables track the movement, now let’s implement the logic of inverting the sign when it reaches the edge.
First let’s initialize CURDIR
to have a value of 0
to indicate the forward direction and MAXP
#$19
, the edge of the screen.
START:
LDA #$00
STA CURDIR
LDA #$19
STA MAXP
Next let’s implement the case where the image hits the edge with a subroutine:
- load the value corresponding to the edge
- compare the current
XPOS
with the edge value - call
CHECK_START
to check if it hits the top left corner- since the signs are inverted, it will always reach the left corner given it’s new starting position.
CHECK_DIRECTION:
LDA MAXP
CMP XPOS
BNE CHECK_START
CHECK_START:
LDA XPOS
CMP #$00
BNE CONTINUE_LOOP
LDA #$00
STA CURDIR
CONTINUE_LOOP
will check if the animation has finished, and repeat it indefinitely. Let’s look at the code all together.
;
; 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 MAXP $22
define CURDIR $23
define XFLAG $24
define YFLAG $25
START:
LDA #$00
STA CURDIR
LDA #$19
STA MAXP
; Set up image width & height
LDA #$05
STA $12
STA $13
; Set initial position X=Y=0
LDA #$09
STA XPOS
LDA #$0A
STA YPOS
MAINLOOP:
; Set pointer to the image
LDA #<G_O
STA $10
LDA #>G_O
STA $11
; Put image on bitmap display
LDA #$10
LDX XPOS
LDY YPOS
JSR DRAW
; Delay to show the image
LDY #$00
LDX #$50
DELAY:
DEY
BNE DELAY
DEX
BNE DELAY
; Put pointer to blank image
LDA #<G_BLANK
STA $10
LDA #>G_BLANK
STA $11
; Clear previous image position
LDA #$10
LDX XPOS
LDY YPOS
JSR DRAW
LDA CURDIR
BEQ INCREMENT_POS
DEC XPOS
DEC YPOS
JMP CHECK_DIRECTION
INCREMENT_POS:
INC XPOS
INC YPOS
CHECK_DIRECTION:
LDA MAXP
CMP XPOS
BNE CHECK_START
LDA XPOS
STA CURDIR
JMP CONTINUE_LOOP
CHECK_START:
LDA XPOS
CMP #$00
BNE CONTINUE_LOOP
LDA #$00
STA CURDIR
CONTINUE_LOOP:
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
Conclusion
The logic of the lab is simple, but found implementation difficult because of how verbosity of assembly. The verbose nature of assembly has made me appreciate high level languages and how much is hidden.
From the lab, I learned more about the use-cases for subroutines, how and when to implement subroutines into other routines, and BNE
statements. This is similar to using “for” loops and “if” statements in high level programming languages.
Overall, I had a difficult time with this lab but in a good way - I questioned what I understood about assembly to this point and challenged my analyzing skills, a transferable skill that goes beyond assembly code. Thanks for reading! See you soon!
Top comments (0)