DEV Community

JP
JP

Posted on

SPO600 Lab 2 - Animating on the Bitmap Display

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

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

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

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

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

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

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

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

Enter fullscreen mode Exit fullscreen mode

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

Calculation Steps:

  1. Clear the accumulator, this is where the calculation will take place
  2. Load the accumulator with the image’s Y-coordinate
  3. 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
    
  4. The MULT routine shifts the the bits to left via ASL and ROL rotates the carry into the SCRPTRH, Y registry is then decremented and repeats until Y = 0.

LDA SCRX
CLC
ADC SCRPTR
STA SCRPTR
LDA #$00
ADC SCRPTRH
STA SCRPTRH

Enter fullscreen mode Exit fullscreen mode
  1. 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.
  2. At this point, SCRPTR and SCRPTRH hold the final addresses for the transformed image.

Inserting the Transformed Image

LDA #$02
CLC
ADC SCRPTRH
STA SCRPTRH
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

Next let’s implement the case where the image hits the edge with a subroutine:

  1. load the value corresponding to the edge
  2. compare the current XPOS with the edge value
  3. call CHECK_START to check if it hits the top left corner
    1. 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
Enter fullscreen mode Exit fullscreen mode
CHECK_START:
  LDA XPOS
  CMP #$00
  BNE CONTINUE_LOOP
  LDA #$00
  STA CURDIR
Enter fullscreen mode Exit fullscreen mode

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

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)