Last week, on my Software Portability and Optimization, we had our lesson about branching and control flow on the 6502. While I understood the basics, it is still difficult to think of a more structured way to write code when you have several limitations and you are used to high level constructs in modern programming languages. Even C, what many people consider a low level programming language, still provides so many abstractions over the actual architecture of the processor.
Either way, I managed to somewhat finish one exercise that I got assigned with a team last week. There were 4 tasks we could choose from, and we chose the kaleidoscope effect.
The Kaleidoscope Effect
Essentially, we need to let a pen cursor draw on one of the quarters of the screen, and that drawing should be mirrored on the other quadrants of the screen.
A nice example is the following image:
We can see that the upper left quadrant is actually being mirrored on the X and Y axis to produce the effect. The top right quadrant is derived by mirroring on the Y axis, while the bottom left is derived by mirroring on the X axis, and the bottom right is derived by mirroring on both axes.
Mirroring
The code that is in charge of such mirroring is the following:
LDA POINTER ;; save the pointer to the
PHA ;; original location in top_left_quad
LDA POINTER_H
PHA
LDA #$10 ;; mirror on the y axis (drawing top right quad)
CLC
SBC COL
CLC
ADC #$10
TAY
LDA DOT
STA (POINTER),y
TYA
PHA ; save the y offset
;; start mirroring on x axis (bottom left quad)
lda #$10 ; load POINTER with start-of-row
CLC
SBC ROW
CLC
ADC #$10
TAY
lda table_low,y
sta POINTER
lda table_high,y
sta POINTER_H
ldy COL ; store CURSOR at POINTER plus COL
lda DOT
sta (POINTER),y
PLA ;; reusing the prev calculations to mirror on
TAY ;; both axes now
lda DOT
sta (POINTER),y
PLA
STA POINTER_H
PLA
STA POINTER
It seems long, mainly because of the heavy math we have to do, but overall, it is very straight to the point.
Another feature of this code is that we can draw with several pen colours. This is what the line lda DOT
. We have a cell that is labelled with DOT
, which stores the current pen colour. This acts very similarly to how a variable works on a high level programming language, but it is very actually very primitive. In reality, this is a macro directive that associates the string DOT
with the string $01
, which is the address of the cell. Variables on programming languages behave a little bit more differently, since they have scope associated with them, among other features that we are used to.
There is also the instructions PLA
and PHA
, which seem to be new instructions we haven't seen before.
PLA
and PHA
pop and push out of the stack, respectively. If you are familiar with concepts related to computer architecture, you may have heard the concept of a 'stack' and a 'heap'. Essentially, the stack is a region of memory managed in a really special way. We use something called a 'stack pointer', which points at the top of the stack, and this pointer keeps track of how much memory the stack is currently taking. This is how high level code keeps track of variables. If a function calls another function, we need a way to save the state of the caller so that it does not get lost after the callee finishes its execution.
I used the stack to save the pointer that I was using to draw on the top left quadrant, since it also gets used on another part of the program.
Pen Colour
Returning back to the colour of the pen, the way we change the colour is by reading a specific place in memory that stores the key pressed. If you press a number from 0 to 9, it changes the colour of the pen depending on the number.
The code that deals with that specific part is this:
getkey: lda $ff ; get a keystroke
ldx #$00 ; clear out the key buffer
stx $ff
cmp #$30
bmi getkey
cmp #$40
bpl continue
sec
sbc #$30
tay
lda color_pallete, y
sta DOT
This specific implementation of the emulator encodes keys with the ASCII encoding. Thus, we make sure that our key is within the ranges 0x30
to 0x39
, which are the values for the characters 0
to 9
. If it is, we want to get our values from a table, that being the following:
color_pallete:
dcb $01,$02,$03,$04,$05,$06,$07,$08,$09,$0a
I declare an array of constant bytes, where each value corresponds to a specific color value. While I could simply subtract a specific offset from the key code value to calculate the color value, this would restrict us to only getting color values that have to be next to each other. If we wanted to specify another order, we wouldn't be able to, unless we use the table approach. In another hand, if we don't use this table, we would save 10 bytes of memory, which could be very valuable.
The Whole Program
The code of the program was derived from a sample program the professor gave us as an aid. The whole program is the following:
; zero-page variable locations
define ROW $20 ; current row
define COL $21 ; current column
define POINTER $10 ; ptr: start of row
define POINTER_H $11
; constants
define DOT $01 ; dot colour location
define CURSOR $04 ; purple colour
setup: lda #$0f ; set initial ROW,COL
sta ROW
sta COL
LDA #$01
STA DOT
draw: jsr draw_cursor
getkey: lda $ff ; get a keystroke
ldx #$00 ; clear out the key buffer
stx $ff
cmp #$30
bmi getkey
cmp #$40
bpl continue
sec
sbc #$30
tay
lda color_pallete, y
sta DOT
jmp done
continue: cmp #$43 ; handle C or c
beq clear
cmp #$63
beq clear
cmp #$80 ; if not a cursor key, ignore
bmi getkey
cmp #$84
bpl getkey
pha ; save A
lda DOT ; set current position to DOT
sta (POINTER),y
jsr draw_on_quads
pla ; restore A
cmp #$80 ; check key == up
bne check1
dec ROW ; ... if yes, decrement ROW
jmp done
check1: cmp #$81 ; check key == right
bne check2
inc COL ; ... if yes, increment COL
jmp done
check2: cmp #$82 ; check if key == down
bne check3
inc ROW ; ... if yes, increment ROW
jmp done
check3: cmp #$83 ; check if key == left
bne done
dec COL ; ... if yes, decrement COL
clc
bcc done
clear: lda table_low ; clear the screen
sta POINTER
lda table_high
sta POINTER_H
ldy #$00
tya
c_loop: sta (POINTER),y
iny
bne c_loop
inc POINTER_H
ldx POINTER_H
cpx #$06
bne c_loop
done: clc ; repeat
bcc draw
draw_cursor:
lda ROW ; ensure ROW is in range 0:31
and #$0f
sta ROW
lda COL ; ensure COL is in range 0:31
and #$0f
sta COL
ldy ROW ; load POINTER with start-of-row
lda table_low,y
sta POINTER
lda table_high,y
sta POINTER_H
ldy COL ; store CURSOR at POINTER plus COL
lda #CURSOR
sta (POINTER),y
rts
draw_on_quads:
LDA POINTER ;; save the pointer to the
PHA ;; original location in top_left_quad
LDA POINTER_H
PHA
LDA #$10
CLC
SBC COL
CLC
ADC #$10
TAY
LDA DOT
STA (POINTER),y
TYA
PHA ; save the y offset
lda #$10 ; load POINTER with start-of-row
CLC
SBC ROW
CLC
ADC #$10
TAY
lda table_low,y
sta POINTER
lda table_high,y
sta POINTER_H
ldy COL ; store CURSOR at POINTER plus COL
lda DOT
sta (POINTER),y
PLA
TAY
lda DOT
sta (POINTER),y
PLA
STA POINTER_H
PLA
STA POINTER
RTS
; these two tables contain the high and low bytes
; of the addresses of the start of each row
table_high:
dcb $02,$02,$02,$02,$02,$02,$02,$02
dcb $03,$03,$03,$03,$03,$03,$03,$03
dcb $04,$04,$04,$04,$04,$04,$04,$04
dcb $05,$05,$05,$05,$05,$05,$05,$05,
table_low:
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
color_pallete:
dcb $01,$02,$03,$04,$05,$06,$07,$08,$09,$0a
I would have to say that there are a lot of room for improvements, such as improving the whole code structure. Right now, the only way to understand the program is by reading the program from start to finish, which is fine for the size of the program, but it is still somewhat difficult to do.
Another improvement is the responsiveness of the keyboard. Right now, the program seems to take a little bit of time to move the cursor or to change the colour of the pen, which can be rather frustrating.
In any case, this is the best I can do with the lab without spending too much time on it. Thank you if you read all the way and I hope you read other of my posts!
Top comments (0)