Ship on Screen
Display a hardware sprite using VIC-II registers — your first machine code program on the Commodore 64.
A white spaceship against black. Nothing moves, nothing makes sound — it just sits there.
That’s all you need. This single image proves you can talk to the VIC-II chip, the C64’s graphics engine. Every sprite, every animation, every game you’ll ever write on this machine starts with the same two instructions you’ll learn here: LDA (load a value) and STA (store it somewhere). One sprite, five registers, and the satisfaction of knowing you put it there with machine code.
The BASIC Stub
Every C64 program needs a way to start. The first two bytes of any .prg file tell the C64 where to load the program in memory. After that, we need a BASIC line that launches our machine code:
; BASIC stub: 10 SYS 2061
; These bytes encode a single BASIC line that jumps
; to our machine code at address $080D (decimal 2061)
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
The magic bytes encode 10 SYS 2061 — a BASIC line that calls the machine code starting at address $080D (decimal 2061). When you type RUN, BASIC executes this line and jumps to your code.
You don’t need to memorise these bytes. They’re the same for every program that starts at $080D.
LDA and STA
The 6510 processor works with three registers: A (the accumulator), X, and Y. For now, you only need A.
Two instructions do most of the work:
LDA #value— LoaD the Accumulator with a valueSTA address— STore the Accumulator to a memory address
The # symbol means “this exact value” (called immediate mode). Without it, the number would be treated as a memory address to read from.
lda #$01 ; Put the value 1 into A
sta $d027 ; Write A to address $D027 (sprite 0 colour)
That’s it. Load a value, store it somewhere. The entire VIC-II chip — sprites, colours, screen layout — is controlled by storing values to specific memory addresses. This is called memory-mapped I/O: the hardware responds to writes at certain addresses as if they were physical switches.
Clearing the Screen
Before showing our ship, we clear away the BASIC startup text. This short loop fills screen memory with space characters:
; Clear the screen (fill with spaces)
ldx #$00
- lda #$20 ; Space character
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne -
Screen memory lives at $0400–$07E7 (1,000 bytes — one per character cell on the 40×25 screen). The loop uses the X register as a counter: LDX loads it, INX increments it, and BNE loops back until X wraps from 255 to 0. We’ll explore loops properly in Unit 2. For now, just know this gives us a clean black canvas.
VIC-II Sprite Registers
The VIC-II chip controls all graphics on the C64. To display a sprite, it needs four pieces of information:
; Tell VIC-II where sprite 0's graphic data lives
; Block number = address / 64. $2000 / 64 = 128.
lda #128
sta $07f8 ; Sprite 0 data pointer
; Position sprite 0 near centre-bottom of screen
lda #172 ; X position
sta $d000 ; Sprite 0 X position
lda #220 ; Y position
sta $d001 ; Sprite 0 Y position
; Set sprite 0 colour to white
lda #$01
sta $d027 ; Sprite 0 colour
; Enable sprite 0
lda #%00000001 ; Bit 0 = sprite 0
sta $d015 ; Sprite enable register
-
Where’s the graphic? — Register
$07F8holds a block number pointing to the sprite’s pixel data. Block 128 means address $2000 (128 × 64 = 8192 = $2000). -
Where on screen? — Registers
$D000(X) and$D001(Y) set the sprite’s position. X runs left-to-right, Y runs top-to-bottom. -
What colour? — Register
$D027sets sprite 0’s colour. The C64 has 16 colours (0–15). White is 1. -
Is it switched on? — Register
$D015is the sprite enable register. Each bit controls one sprite. Bit 0 = sprite 0, bit 1 = sprite 1, and so on up to 7. Set the bit to 1 to make the sprite visible.
Sprite Data
A C64 sprite is 24 pixels wide and 21 pixels tall. Each row is stored as 3 bytes (24 bits), making 63 bytes per sprite. With one padding byte, that’s 64 bytes — which is why sprite data must sit at 64-byte boundaries in memory.
Each bit represents one pixel. Bit 7 (leftmost) of the first byte is the leftmost pixel in that row. A 1 bit draws the pixel in the sprite’s colour; a 0 bit is transparent.
The hex values on the left correspond to the three !byte values in each row of the code below:
; Sprite data: 24 pixels wide x 21 rows
; Each row is 3 bytes. Bit 7 of each byte = leftmost pixel.
; Total: 63 bytes (+ 1 padding byte = 64-byte alignment)
*= $2000
; Pixels
!byte $00,$18,$00 ; ##
!byte $00,$3c,$00 ; ####
!byte $00,$3c,$00 ; ####
!byte $00,$7e,$00 ; ######
!byte $00,$7e,$00 ; ######
!byte $00,$ff,$00 ; ########
!byte $00,$ff,$00 ; ########
!byte $01,$ff,$80 ; ##########
!byte $03,$ff,$c0 ; ############
!byte $07,$ff,$e0 ; ##############
!byte $07,$ff,$e0 ; ##############
!byte $07,$e7,$e0 ; ###..####..###
!byte $03,$c3,$c0 ; ##....##....##
!byte $01,$ff,$80 ; ##########
!byte $00,$ff,$00 ; ########
!byte $00,$ff,$00 ; ########
!byte $00,$db,$00 ; ##.##.##
!byte $00,$db,$00 ; ##.##.##
!byte $00,$66,$00 ; ##..##
!byte $00,$24,$00 ; #..#
!byte $00,$00,$00 ;
This data draws a pointed ship with flared wings and engine exhausts.
The Complete Code
; Starfield - Unit 1: Ship on Screen
; Assemble with: acme -f cbm -o starfield.prg starfield.asm
; ------------------------------------------------
; BASIC stub — launches machine code with SYS 2061
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00
; ------------------------------------------------
; Main program
; ------------------------------------------------
*= $080d
; Black screen
lda #$00
sta $d020 ; Border colour
sta $d021 ; Background colour
; Clear the screen (fill with spaces)
ldx #$00
- lda #$20 ; Space character
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne -
; Tell VIC-II where sprite 0's graphic data lives
; Block number = address / 64. $2000 / 64 = 128.
lda #128
sta $07f8 ; Sprite 0 data pointer
; Position sprite 0 near centre-bottom of screen
lda #172 ; X position
sta $d000 ; Sprite 0 X position
lda #220 ; Y position
sta $d001 ; Sprite 0 Y position
; Set sprite 0 colour to white
lda #$01
sta $d027 ; Sprite 0 colour
; Enable sprite 0
lda #%00000001 ; Bit 0 = sprite 0
sta $d015 ; Sprite enable register
; Loop forever
- jmp -
; ------------------------------------------------
; Sprite data at $2000 (block 128)
; 24 pixels wide x 21 rows = 63 bytes
; Each row: 3 bytes, bit 7 = leftmost pixel
; ------------------------------------------------
*= $2000
; Pixels
!byte $00,$18,$00 ; ##
!byte $00,$3c,$00 ; ####
!byte $00,$3c,$00 ; ####
!byte $00,$7e,$00 ; ######
!byte $00,$7e,$00 ; ######
!byte $00,$ff,$00 ; ########
!byte $00,$ff,$00 ; ########
!byte $01,$ff,$80 ; ##########
!byte $03,$ff,$c0 ; ############
!byte $07,$ff,$e0 ; ##############
!byte $07,$ff,$e0 ; ##############
!byte $07,$e7,$e0 ; ###..####..###
!byte $03,$c3,$c0 ; ##....##....##
!byte $01,$ff,$80 ; ##########
!byte $00,$ff,$00 ; ########
!byte $00,$ff,$00 ; ########
!byte $00,$db,$00 ; ##.##.##
!byte $00,$db,$00 ; ##.##.##
!byte $00,$66,$00 ; ##..##
!byte $00,$24,$00 ; #..#
!byte $00,$00,$00 ;
If It Doesn’t Work
- No sprite? Check
$D015— it must have bit 0 set. A value of$00means all sprites are off. - Wrong position?
$D000is X (horizontal),$D001is Y (vertical). Easy to swap them. - Wrong colour? Sprite colour registers go from
$D027(sprite 0) to$D02E(sprite 7). Values 0–15. - Garbled sprite? The pointer at
$07F8must match where you placed the data. Block 128 = address $2000. If you move the data, update the pointer. - Screen not black? Make sure you’re writing to both
$D020(border) and$D021(background).
Try This: Change the Colour
Find this line:
lda #$01 ; White
sta $d027
Change $01 to any value from 0 to 15:
| Value | Colour |
|---|---|
$00 | Black |
$01 | White |
$02 | Red |
$03 | Cyan |
$05 | Green |
$07 | Yellow |
$0a | Light red |
$0e | Light blue |
Reassemble and reload. Your ship changes colour instantly.
Try This: Move the Ship
Change the X and Y position values:
lda #172 ; Try 24, 160, or 255
sta $d000
lda #220 ; Try 50, 150, or 230
sta $d001
Notice that X only goes up to 255 — but the screen is 320 pixels wide. For positions beyond 255, you’ll need an extra register ($D010). We’ll tackle that in a later unit.
Try This: Design Your Own Sprite
Click pixels to draw, then copy the output straight into your code. The byte values update as you draw.
What You’ve Learnt
- LDA and STA — Load a value into the accumulator, store it to a memory address. These two instructions control the entire machine.
- Memory-mapped I/O — The VIC-II chip responds to writes at addresses
$D000–$D02E. No function calls, no APIs — just poke values into memory. - Sprite enable register ($D015) — Each bit enables one of eight hardware sprites. Bit 0 = sprite 0.
- Sprite pointer ($07F8) — Tells the VIC-II where in memory to find the sprite’s pixel data. Value × 64 = address.
- Sprite data format — 24 × 21 pixels, 3 bytes per row, 63 bytes total. One bit per pixel.
- The BASIC stub — A tiny BASIC program that launches your machine code with
SYS.
What’s Next
The ship sits still. In Unit 2, you’ll read the joystick and make it move — left, right, up, down — using the CIA chip and your first conditional branches.