Learning 6502 assembly

From Wikiversity
Jump to navigation Jump to search

This page by Dan Polansky guides the reader through first steps of learning 6502 assembly. The approach is this: first point the reader to good online sources describing the assembly language, then provide exercises.

We will use Easy 6502 website, which contains a nice introduction together with an online 6502 emulator written in JavaScript. In that environment, one can manipulate a screen of 32 x 32 pixels and 16 colors, starting at $200, one byte per pixel. Furthermore, pseudorandom 8-bit numbers can be obtained from address $fe. One can also get the ASCII code of the last key pressed from $ff. This environment is good to start practicing, although it does not offer ASCII output, which is inconvenient for, say, numerical exercises.

If you never programmed in assembly, the Easy 6502 web site is a good start as a read. The processor is also described in Wikibooks. You may either read Easy 6502 right now, or you can start with exercises and only consult the guide on an as needed basis.

The following exercises are meant for Easy 6502 online emulator. The sequence of exercises is significant, where the latter ones incrementally build on the earlier ones. To learn most from this page, you have to do the exercises yourself rather than merely reading the solutions.

Motivation: Get a feel for assembly programming with a simple processor that was much in use in many home computers (Apple II, Commodore 64, Atari 8-bit, etc.) and appreciate the convenience of high-level languages, C included, and cherish the ingenuity and talent of all those 6502 programmers who created the 8-bit games on very limited machines.

License: All code snippets here are released into the public domain.

Cheat sheet

[edit | edit source]

Here is a quick cheat sheet. Exercises follow in the next sections.

Instructions:

  • LDA, STA, LDX, STX, LDY, STY
  • TAX, TAY, TXA, TYA, TXS, TSX
  • CMP, CPX, CPY
  • BEQ, BNE, BCC, BCS, BPL, BMI
  • JMP, JSR, RTS
  • PLA, PHA
  • ADC, SBC, INC, INX, INY, DEC, DEX, DEY
  • CLC, SEC
  • ASL, LSR, AND, ORA, EOR, BIT, ROL, ROR
  • Etc.

Addressing modes:

  • LDA #$80  ; Acc := $80; called immediate
  • LDA $80  ; Acc := Peek($80)
  • LDA $8000  ; Acc := Peek($8000)
  • LDA ($80), Y  ; Acc := Peek(Deek($80)+Y) ; only for zero page
  • LDA ($80, X)  ; Acc := Peek(Deek($80+X)) ; only for zero page

Numerical literals (exemplified in use with LDA immediate):

  • LDA #170 ;decimal
  • LDA #$AA ;hexadecimal
  • LDA #%10101010 ;binary

Flags of the status register:

  • N: Negative
  • V: Overflow
  • -: ignored
  • B: Break
  • D: Decimal: use BCD
  • I: Interrupt
  • Z: Zero
  • C: Carry

Links:

Limitations of the CPU

[edit | edit source]

To get a better idea about the character of 6502 programming, let us look at its limitations compared e.g. to Motorola 68000 ("68k"):

  • There are only 3 main registers: A, X and Y. By contrast, 68k has 8 data registers and 8 address registers. As a consequence, 6502 programming needs to use memory location for loop indices and other temporary needs much more often.
  • All three main registers are 8-bit. Thus, even such a simple thing as making an x coordinage loop from 0 to 319 (typical for some 8-bit computers using the CPU) cannot be handled by incrementing a single register with no additional work. By contrast, 68k has 32-bit data and address registers. Even 16-bit registers would make a huge difference.
  • Some operations are only possible with the accumulator, not with the index registers X and Y. If one wants to use these operations on, say, X, one can use TXA, then operate, and then TAX, thereby losing the value that was in A.
  • 6502 has no integer multiplication and division instructions, only addition and subtraction. 68k has multiplication.

Small block of pixels without a loop

[edit | edit source]

Assignment: fill a small block of pixels, say, 3 pixels, with colors 0 through 2.

Solution:

LDA #0     ;Load accumulator
STA $200   ;Store accumulator
LDA #1
STA $201
LDA #2
STA $202

What we have learned: we can use LDA and STA to manipulate memory. There is the immediate addressing mode indicated using the hash character (#) and the other addressing mode. The string character ($) indicates the value is hexadecimal; it is decimal without it.

Block of pixels using a loop

[edit | edit source]

Assignment: fill a 256-long block of pixels (spanning mutliple lines on the 32x32 screen) with color 1.

Hint: use indirect addressing mode with register Y, using Y as a loop variable.

Solution:

LDA #1
LDY #0
LOOP:
STA $200, Y ;Indexed addressing with Y; store to $200+Y
INY
BNE LOOP ;Branch on not equal (here implicitly not equal to zero)

What we have learned:

  • The increment instruction affects the zeor flag.

Fill screen with vertical stripes

[edit | edit source]

Assignment: fill the 32x32 screen with vertical color stripes. Which colors are to be used is left unspecified.

Hint: we may build on the ideas from previous exercises. We can no longer do with using Y as index register alone since we need to cover 32x32 = 4 * 256 bytes.

Solution:

LDA #00
STA $80
LDA #02
STA $81
LDA #1
LDY #0
LDX #4    ;Four pages to process
LOOP:
TYA
STA ($80), Y
INY
BNE LOOP
INC $81
DEX
BNE LOOP

What we have learned:

  • We can use the indirect addressing indexed with Y for the purpose. The base address needs to be stored at zero page ($0-$FF). We picked $80 arbitrarily. We note that 6502 uses little endian addressing: the lower byte is stored at $80 and the upper byte in $81 rather than the other way around.
  • There is TYA (A := Y) and similar for X and the other way around.

Fill screen with horizontal stripes

[edit | edit source]

Assignment: Fill screen with horizontal stripes.

Solution:

LDA #00
STA $80
LDA #02
STA $81
LDA #0
LDY #0
LDX #4
LOOP:
STA ($80), Y
STA $82 ;Temporary color store
INY
TYA
AND #$1F
BNE KEEPCOLOR
LDA $82
CLC
ADC #1
STA $82
KEEPCOLOR:
LDA $82
CPY #0
BNE LOOP
INC $81
DEX
BNE LOOP

What we have learned: there is no increment instruction for accumulator, but we can emulate it with CLC (clear the carry bit) followed by ADC (add with carry). We need to use additional memory to store values given how few registers we have.

Fill screen with random color

[edit | edit source]

Assignment: fill the 32x32 screen with random color. This is a minor variation on filling the screen with a fixed color.

Hint: obtain a random byte from address $fe.

LDA #00
STA $80
LDA #02
STA $81
LDY #0
LDX #4
LOOP:
LDA $FE
STA ($80), Y
INY
BNE LOOP
INC $81
DEX
BNE LOOP

Plot a secondary diagonal

[edit | edit source]

Assignment: plot a secondary diagonal using color 1. That is, start at upper righthand corner and end at lower lefthand corner. (Plotting the main diagonal is a bit easier and is left out.)

Solution:

LDA #00
STA $80
LDA #02
STA $81
LDA #1
LDY #31
LDX #32   ;Pixel count
LOOP:
LDA #1
STA ($80), Y
TYA
CLC
ADC #31
TAY
BCC SKIP ;Branch on carry clear
INC $81
SKIP:
DEX
BNE LOOP

What we have learned: we cannot do addition on Y, only on A. The carry bit set by ADC is not erased by TAY.

Random pixels in a rectangle

[edit | edit source]

Assignment: bombard a rectangle (e.g. 20 x 25 pixels) starting at the top left corner with random pixels. Thus, pseudorandomly generate pixel x and y coordinates in the appropriate ranges and plot pixel with color 1 at that location, doing so repeatedly. Have the rectangle width and height parametrized.

Solution:

LDA #20  ;Rectangle width
STA $82
LDA #25  ;Rectangle height
STA $83
LDX #0   ;Pixel count $100
PIXELLOOP:
LDA #0   ;$80-81 := screen address
STA $80
LDA #2
STA $81
RNDX:
LDA $FE  ;x := random in [0, xmax-1]
AND #$1F
CMP $82
BCS RNDX
STA $84
RNDY:
LDA $FE  ;y := random in [0, ymax-1]
AND #$1F
CMP $83
BCS RNDY
ASL ;Multiply by 32 with carry into $81
ASL
ASL
ASL
BCC SKIP1
INC $81
INC $81
CLC
SKIP1:
ASL
BCC SKIP2
INC $81
SKIP2:
CLC
ADC $84
BCC PLOT
INC $81
PLOT:
TAY
LDA #1
STA ($80), Y
DEX
BNE PIXELLOOP

What we have learned:

  • How to compare 8-bit numbers for less than or less than or equal using unsigned 8-bit integer semantics.
  • How to multiply by a power of 2 (here, 32, the screen width) via ASL and using carry bit to reflect the operation in the upper byte.

Links:

Keyboard controlled pixels

[edit | edit source]

Assignment: read key events and when a key is pressed, place random color to pixel that is offset from the screen beginning by the ASCII code of the key pressed. Read the ASCII code of the last key (or key combination, e.g. "a" with shift) from $ff. When no key is being pressed, keep setting random color to pixel with offset zero.

Solution:

LOOP:
LDY $ff ;Last key pressed
LDA $fe ;Random byte
STA $200, Y
CPY #0
BEQ LOOP
LDY #0
STY $FF ;Erase last key pressed or else it will stick
BEQ LOOP

What we have learned: How to read key events and erase last key pressed. How the keys map to codes, visually; thus, e.g. "a" is $61, and other lowercase letters alphabetically follow.

Keyboard-controlled moving dot

[edit | edit source]

Assignment: implement a keyboard-controlled moving dot representing a person, represented as a single pixel of color 1 (white). Use color 5 (green) to first fill the screen as a background. Mark the screen boundary using color 8 (brown). As the player moves around, leave empty space (color 0, black) behind. Do not allow the player to enter the screen boundary filled with brown. Use keys A, S, D, and W to control the player.

Note: This was inspired by the snake game at Easy 6502. Like in the snake, one has to figure out how to receive keyboard events. However, it is simpler than the snake, not requiring keeping track of the locations of the snake body.

Solution:

; ---- Memory variables
; $80-81: screen address
; $82-83: player position as pixel address on the screen
; $84-85: temporary copy of the above to be modified and conditionally copied to the actual position
; ---- Fill screen with color 5, green
LDA #00
STA $80
LDA #02
STA $81
LDA #5
LDY #0
LDX #4    ;Four pages to process
LOOP1:
STA ($80), Y
INY
BNE LOOP1
INC $81
DEX
BNE LOOP1
; ---- Plot boundary with color 8, brown
LDA #02
STA $81
LDA #8
LDY #0
LDX #32
LOOP2:
STA ($80), Y
INY
DEX
BNE LOOP2
;
LDA #$E0 ; Let $80 start at $200 + 31 * 32 = 5E0
STA $80
LDA #$05
STA $81
LDA #8
LDY #0
LDX #32
LOOP3:
STA ($80), Y
INY
DEX
BNE LOOP3
;
LDA #00
STA $80
LDA #02
STA $81
LDY #0
LDX #32 ;Pixel count
LOOP4:
LDA #8
STA ($80), Y
TYA
CLC
ADC #32
TAY
BCC SKIP
INC $81
SKIP:
DEX
BNE LOOP4
;
LDA #00
STA $80
LDA #02
STA $81
LDY #31
LDX #32 ;Pixel count
LOOP5:
LDA #8
STA ($80), Y
TYA
CLC
ADC #32
TAY
BCC SKIP2
INC $81
SKIP2:
DEX
BNE LOOP5
; ---- Player control and key event loop
LDA #$EF
STA $82   ;Player position as pixel address; $200 + 15 * 32 + 15
STA $84
LDA #$03
STA $83
STA $85
;
LOOP6:
LDA $84
STA $82
LDA $85
STA $83
;
LOOP7:
LDA #1
LDY #0
STA ($82), Y
;
LDY $FF   ;Last key pressed
CPY #$61  ;"a", left
BNE TESTD
LDA #0
STA $FF
LDA $82
STA $84
LDA $83
STA $85
DEC $84
LDA $84
CMP #$FF
BNE SKIP3
DEC $85
SKIP3:
LDY #0
LDA ($84), Y
CMP #8 ;brown
BEQ LOOP7
LDA #0
STA ($82), Y
CLC
BCC LOOP6
;
TESTD:
LDY $FF   ;Last key pressed
CPY #$64  ;"d", right
BNE TESTW
LDA #0
STA $FF
LDA $82
STA $84
LDA $83
STA $85
INC $84
LDA $84
CMP #0
BNE SKIP4
INC $85
SKIP4:
LDY #0
LDA ($84), Y
CMP #8 ;brown
BEQ LOOP7
LDA #0
STA ($82), Y
CLC
BCC LOOP6
;
TESTW:
LDY $FF   ;Last key pressed
CPY #$77  ;"w", up
BNE TESTS
LDA #0
STA $FF
LDA $82
STA $84
LDA $83
STA $85
LDA $84
SEC
SBC #32
STA $84
BCS SKIP5
DEC $85
SKIP5:
LDY #0
LDA ($84), Y
CMP #8 ;brown
BEQ LOOP7C
LDA #0
STA ($82), Y
JMP LOOP6
LOOP7C:
JMP LOOP7
;
TESTS:
LDY $FF   ;Last key pressed
CPY #$73  ;"s", down
BNE LOOP7C
LDA #0
STA $FF
LDA $82
STA $84
LDA $83
STA $85
LDA $84
CLC
ADC #32
STA $84
BCC SKIP7
INC $85
SKIP7:
LDY #0
LDA ($84), Y
CMP #8 ;brown
BEQ LOOP7C
LDA #0
STA ($82), Y
JMP LOOP6

What we have learned:

  • Far jumps are not possible using relative branch instructions Bxx; one has to use e.g. JMP.
  • To subtract the value V using SBC #V, one has to make sure the carry flag is set (via SEC) rather than cleared (via CLC).

Online assemblers

[edit | edit source]

One can asseble the 6502 assembly source code using online assemblers:

The Masswerk assembler is too sensitive in that it does not compile "LDA ($80), Y" because of the space after the comma. Moreover, it assembles "LDA ($80),X", which is invalid, into "LDA $80,X". Easy 6502 seems better in those two regards.

[edit | edit source]